Skip to content

SystemBars: safe area CSS injection throws "Cannot read properties of null" on startup (Android 16) #8530

Description

@ckrishn

Capacitor Version

Hit this while upgrading to Capacitor 8 and targeting SDK 36. Every app launch logs:

E Capacitor/Console: File:  - Line 6 - Msg: Error injecting safe area CSS: TypeError: Cannot read properties of null (reading 'style')

Environment: @capacitor/android 8.4.1, compileSdk/targetSdk 36, AGP 8.13.0, Gradle 8.14.3, JDK 21, Samsung device on Android 16.

What's happening

It's coming from SystemBars.injectSafeAreaCSS() (~line 250 in 8.4.1). The injected script does:

"""
try {
  document.documentElement.style.setProperty("--safe-area-inset-top", "%dpx");
  document.documentElement.style.setProperty("--safe-area-inset-right", "%dpx");
  document.documentElement.style.setProperty("--safe-area-inset-bottom", "%dpx");
  document.documentElement.style.setProperty("--safe-area-inset-left", "%dpx");
} catch(e) { console.error('Error injecting safe area CSS:', e); }
"""

document.documentElement is null when this runs. The "Line 6" in the logcat output is the catch line inside that text block, which lines up.

As far as I can tell, injectSafeAreaCSS gets called from the setOnApplyWindowInsetsListener callback, and that fires before the WebView document exists on first layout. The bridge.getWebView() != null guard passes — the WebView object is there — but there's no document in it yet. Later passes work fine, which is why the CSS variables do end up set correctly and nothing visibly breaks.

Why I couldn't just work around it

Tried insetsHandling: "disable" first, which was a mistake — on Android 16 SystemBars is the only thing actually insetting content below the status bar. StatusBar.setOverlaysWebView() uses setSystemUiVisibility, which is a no-op at API 35+. So disabling the plugin to kill the error breaks the layout the plugin is there to provide. Ended up patching the framework instead, which isn't great to have to carry.

Fix

Null guard in the injected script:

try {
  if (document.documentElement) {
    document.documentElement.style.setProperty("--safe-area-inset-top", "%dpx");
    // ...etc
  }
} catch(e) { console.error('Error injecting safe area CSS:', e); }

No-op when the DOM's already there, so no behaviour change. Ran this on a physical Android 16 device — error's gone, layout is identical, no other console errors.

Repro

Cap 8.4.1 Android app, targetSdk 36, viewport-fit=cover in the viewport meta, launch on an Android 16 device, watch the console.

Happy to put up a PR if it's wanted.

Other API Details

Platforms Affected

  • iOS
  • Android
  • Web

Current Behavior

On every app launch, an error is logged to the console:

Error injecting safe area CSS: TypeError: Cannot read properties of null (reading 'style')

Full logcat line:
E Capacitor/Console: File: - Line 6 - Msg: Error injecting safe area CSS: TypeError: Cannot read properties of null (reading 'style')

It comes from SystemBars.injectSafeAreaCSS(). The injected script calls
document.documentElement.style.setProperty(...), but document.documentElement is
null at that moment — the "Line 6" in the log is the catch line inside that Java
text block, which lines up with the setProperty calls throwing.

As far as I can tell, injectSafeAreaCSS gets called from the
setOnApplyWindowInsetsListener callback, and that fires before the WebView
document exists on first layout. The bridge.getWebView() != null guard passes
— the WebView object is there — but there's no document in it yet.

Later passes succeed, so the --safe-area-inset-* variables do end up set
correctly and nothing visibly breaks. It's just an error on every startup.

Worth noting: insetsHandling: "disable" isn't a viable workaround. On Android 16
SystemBars is the only thing actually insetting content below the status bar —
StatusBar.setOverlaysWebView() uses setSystemUiVisibility, which is a no-op at
API 35+. So disabling the plugin to silence the error breaks the layout the
plugin is there to provide.

Expected Behavior

The injected script should no-op when the document isn't available yet, rather
than throwing and logging an error on every launch.

A null guard is enough:

try {
if (document.documentElement) {
document.documentElement.style.setProperty("--safe-area-inset-top", "%dpx");
...
}
} catch(e) { console.error('Error injecting safe area CSS:', e); }

It's a no-op whenever the DOM already exists, so no behaviour change. I applied
exactly this locally and verified on a physical Android 16 device — the error is
gone, no other console errors, and the layout is unchanged.

Happy to open a PR if that's useful.

Project Reproduction

No standalone repro repo — but this should reproduce on any Capacitor 8.4.1 Android app targeting SDK 36 with viewport-fit=cover in the viewport meta tag. Launch on an Android 16 device and watch the console on startup. The throw is inside Capacitor's own injected script, so I don't think app code is involved.

Additional Information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions