ionic-team / ionic-team/capacitor

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

Open Beginner friendly
#8,530 2 comments 0 reactions 0 assignees View on GitHub
triage
Dominant language
TypeScript
Stars
16.7k
Forks
1.3k
Avg merge
3d 15h
Merged PRs (30d)
10

Description

### 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:

```java
"""
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:

```js
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

```Shell

```

### Platforms Affected

- [ ] iOS
- [x] 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_

Contributor guide

Open the contributing guide

Research direction

Start at SystemBars.injectSafeAreaCSS(), around line 250 in Capacitor 8.4.1, and inspect its setOnApplyWindowInsetsListener caller and injected script. Reproduce with an Android app targeting SDK 36 on a physical Android 16 device; done means startup emits no safe-area CSS error and the layout and CSS variables remain unchanged.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, css, java, javascript
Domain
mobile-dev
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.