Skip to content

fix(mobile): render the Android navigation bar in the app's surface colour - #2024

Open
Ur-imazing wants to merge 1 commit into
mainfrom
fix/mobile-android-nav-bar-theme
Open

fix(mobile): render the Android navigation bar in the app's surface colour#2024
Ur-imazing wants to merge 1 commit into
mainfrom
fix/mobile-android-nav-bar-theme

Conversation

@Ur-imazing

Copy link
Copy Markdown
Contributor

Problem

On Android the system navigation bar showed the platform contrast scrim instead of the app's surface. With the system in light appearance the bar measured #e9e8e8 on a Galaxy S20 (API 33) — a near-white bar under a near-black app. In dark appearance it measured #110f0e, which is the platform's #66000000 over #1c1917.

Why setting a colour cannot work

React Native forces navigationBarColor to transparent at every React Activity creation (WindowUtil.kt, enableEdgeToEdge), after the theme is read. The one attribute it reads and obeys is android:enforceNavigationBarContrast. Setting it false does two things at once: the platform stops drawing its scrim, and React Native stops overwriting the icon appearance from the system dark-mode setting — which is why the paired android:windowLightNavigationBar=false survives. The two items ship together.

What changed

  • New config plugin plugins/withAndroidNavigationBar.js writes both items onto AppTheme and Theme.App.SplashScreen. MainActivity's manifest theme is the splash one and it does not inherit AppTheme, so the launch window needs its own copy.
  • Order requirement, pinned by a test. The plugin must stay before expo-splash-screen in app.json. That plugin replaces the splash style rather than merging (addSplashScreenStyle filters it out and pushes a fresh one), and Expo runs mods last-registered-first. Reversed, both items are wiped with the suite still green. A test runs the real vendor mod in both orders.
  • Shared helpers plugins/androidStyleXml.js. setItem and the required-style lookup were byte-identical copies in the two plugins that mutate the same AppTheme; they now have one home so they cannot drift.

Verification

Measured by sampling pixels (adb exec-out screencap -p -> ffmpeg -> read bytes), not by eye:

Surface Before After
S20 (API 33), light appearance #e9e8e8 #1c1917
S20 (API 33), dark appearance #110f0e #1c1917
S20 splash / launch window light scrim #1c1917
Pixel 9a emulator (API 35), 3-button #1c1917

Tests 160 suites / 2384 passing; tsc --noEmit, eslint and prettier clean. A fresh expo prebuild --clean confirms both items land on both styles with the cast plugin's five AppTheme items intact.

Known limitations

  • The scrim was doing a job. It guaranteed button contrast over arbitrary content. That guarantee is now gone app-wide, so a surface drawing light pixels behind the bar — a bright fullscreen video frame — can still hide the light buttons. No replacement scrim ships here; the exposure is documented in apps/mobile/CLAUDE.md.
  • API 31-32 unmeasured. On API 31+ androidx re-parents Theme.SplashScreen to Theme.DeviceDefault.DayNight and its values-v33 sets both attributes true, so these items deliberately reverse an androidx default. API 33 was measured and is correct; 31-32 were not.
  • API 26-28 unfixable. enableEdgeToEdge takes an else-branch there and assigns the colour in code, so no theme lever reaches it. minSdk is 24. Recorded in the plugin's SCOPE comment.
  • The API 35 emulator reading came from an emulator that was not cold-booted, which this repo's notes warn against for Android rendering claims. The S20 result is the stronger evidence.
  • Needs a new native build. This moves the fingerprint runtime version, so an OTA update cannot deliver it.

🤖 Generated with Claude Code

…olour

On Android the system navigation bar showed the platform contrast scrim
instead of the app's surface. With the system in LIGHT appearance the bar
measured #e9e8e8 on a Galaxy S20 (API 33) — a near-white bar under a
near-black app.

You cannot fix this by setting a colour. React Native forces
`navigationBarColor` to transparent at every React Activity creation
(`WindowUtil.kt`, `enableEdgeToEdge`). The one attribute it reads and obeys
is `android:enforceNavigationBarContrast`. Setting it false stops the
platform scrim AND stops React Native overwriting the icon appearance from
the system dark-mode setting, so the paired
`android:windowLightNavigationBar=false` survives. The two ship together.

A new config plugin writes both items onto `AppTheme` and onto
`Theme.App.SplashScreen`. MainActivity's manifest theme is the splash one,
and it does not inherit AppTheme, so the launch window needs its own copy.
The plugin must stay before `expo-splash-screen` in app.json: that plugin
replaces the splash style rather than merging, and Expo runs mods
last-registered-first. A test pins that order against the real vendor mod.

`setItem` and the required-style lookup move to `plugins/androidStyleXml.js`
so the two plugins that mutate AppTheme cannot drift apart.

Measured by sampling pixels, not by eye: after the change the bar reads
#1c1917 on the S20 (API 33, physical) in both system appearances, on the
splash window, and on a Pixel 9a emulator (API 35). API 31-32 are not
measured, and the emulator was not cold-booted.

Known limitation: the scrim guaranteed button contrast over arbitrary
content. That guarantee is now gone app-wide, so a bright fullscreen video
frame can still hide the light buttons. No replacement scrim ships here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant