ionic-team / ionic-team/capacitor
SystemBars: safe area CSS injection throws "Cannot read properties of null" on startup (Android 16)
- 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
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