Skip to content

mobile: Color the TabBar's selected tab with the theme's primary - #741

Open
jt-wang wants to merge 4 commits into
longbridge:mainfrom
jt-wang:tabbar-tint
Open

jt-wang wants to merge 4 commits into
longbridge:mainfrom
jt-wang:tabbar-tint

Conversation

@jt-wang

@jt-wang jt-wang commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Description

An app with its own palette colors its buttons and its stack header, but the tab bar under them keeps the system accent: blue on iOS, the wallpaper's color on Android. In a black-and-white app it is the one blue thing on screen.

The selected tab now takes the theme's primary, the color the app's buttons already use. There is nothing to set: an app with a theme in nsh.json gets it, and a theme set while the app runs (set_tokens) reaches the bar too. The other tabs, the bar's material and its height stay the system's.

How the color reaches the screen:

  1. The core's TabBar reads primary from tokens() for the appearance shown and sends it as the bar's tint prop (#RRGGBB). Reading the tokens and the appearance subscribes, so a new theme or a switch to dark sends it again.
  2. iOS sets it as the UITabBar's tintColor, in which UIKit draws the selected item.
  3. Android's TabBarComponent draws the selected icon and label in it, over the active indicator in the same color at 12% opacity (Material's state-layer opacity).

The first version of this PR read tint-* from the bar's style. Following review, it reads primary instead, and the style plumbing it had added (ComponentBox, the iOS component container, the docs of tint-*) is gone.

Screenshots

A theme with primary #FF2D55 (light) and #0A84FF (dark), and an app with no theme of its own:

iOS 26.3 simulator Android 15 emulator
Before
Theme, light
Theme, dark
No theme, light
No theme, dark

Read from the pixels, on both platforms: the selected label is #FF2D55 in light and #0A84FF in dark with the theme, #171717 and #E5E5E5 (nsh-base's default primary) without one. On iOS, switching the simulator to dark while the app runs turns it from #FF2D55 to #0A84FF. (The Android icons are blank because the test app names SF Symbols.)

Public API

None. TabBar has no new prop; its native component gets tint from the core.

Breaking Changes

Every TabBar's selected tab changes color. An app without a theme of its own now gets the default primary (near black in light, near white in dark) where it had system blue on iOS and the wallpaper's accent on Android. On iOS 26 that near black is also the other tabs' color, so the glass pill alone shows the selection (row "No theme, light").

If apps without a theme should keep the system accent, the core can send no tint while the tokens are the default theme's; #742 does that for dialogs. Say which you prefer and I'll make it so.

How to Test

  • cargo test -p nsh-engine --test engine tab_bar: the_tab_bar_takes_the_themes_primary_color checks the tint prop on iOS and Android, in light and then in dark.
  • cargo test -p nsh-platform-ios --target aarch64-apple-ios-macabi --lib tint: the iOS bar reads the prop's hex.
  • An app whose nsh.json theme sets primary, with a <TabBar> and no class: nsh run App.tsx --target ios and --target android, in light and dark mode.

Ran: cargo test --workspace (passes), cargo clippy --all-targets -p nsh-core -p nsh-engine -- -D warnings, the Catalyst clippy, cargo xtask clippy-android (all clean), cargo xtask android --release --dev --abi arm64-v8a, rustfmt +nightly --check on the changed Rust files.

Checklist

  • I have read the CONTRIBUTING document and followed the guidelines.
  • Reviewed the changes in this PR and confirmed AI generated code (if any) is accurate.
  • Passed cargo test --workspace, cargo +nightly fmt --all and clippy (including the Catalyst and Android runs for platform code).
  • Changes to what apps use keep what released versions shipped and come with a case in tests/compat/ (cargo xtask compat): no API change, so no case.
  • Ran the related examples/gallery demos on iOS and Android (if the change is visible or platform-specific).

An app with a theme of its own needs the system tab bar's selected tab
in its colors, as `NavPage`'s `headerStyle` already allows with
`tintColor`. The core already hands `tint-*` to the bar's native view;
this test holds it there for the platforms to color the selected tab
with, and the compat case uses `tint-foreground` on the bar.

Today both platforms ignore it: on an iOS 26 simulator a bar with
`tint-[#ff2d55]` still shows its selected tab in the system blue
(#367ced in a screenshot).
`nsh/mobile`'s `TabBar` showed its selected tab in the system accent
(blue on iOS), whatever the app's theme: an app in black and white could
not make the bar match its own header, which takes `tintColor` through
`headerStyle`. The bar now takes the `tintColor` of its style
(`tint-<color>`, Rust `.tint_color(..)`) for the selected tab; the other
tabs and the bar itself keep the system's look.

iOS: a native component's container carries the style's tint as UIKit's
`tintColor`, which the `UITabBar` inside inherits, as UIKit views do.
Android: the component's box hands the tint to the Material 3 bar, which
draws the selected icon and label in it over an active indicator of it
at 12% opacity (Material's state-layer opacity).

@huacnlee huacnlee left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static review only; no device test performed.

Reviewed by ChatGPT on behalf of @huacnlee.

}
if (v instanceof ImageBox) ((ImageBox) v).setTint(hasTint ? tint : 0);
else if (v instanceof ComponentBox) ((ComponentBox) v).setTint(hasTint ? tint : 0);
applyDynamic(v, d);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Preserve whether a tint was explicitly supplied. 0 is also a valid transparent color, so tint-transparent currently restores the system-selected color on Android instead of making it transparent. Pass hasTint separately (or use a nullable color) through ComponentBox and cover explicit transparent tint versus removing the tint.

Reviewed by ChatGPT on behalf of @huacnlee.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 813d0a1. The tint now travels as a nullable Integer through ComponentBox and TabBarComponent: tint-transparent is a color, no tint is null. The indicator takes 12% of the tint's own alpha, so a transparent tint draws none.

  • a_transparent_tint_reaches_the_tab_bar_as_a_color checks the core sends tint-transparent as Color(0), not as no tint.
  • Android 15 emulator, read from the screenshots: before the fix, tint-transparent drew the system's #414659 indicator and #E3E2E9 label, the same as no tint. After it, neither shows; without a tint the tab is #414659 / #E3E2E9 as before.

ImageBox keeps its own 0-means-none, which this PR doesn't touch.

ComponentBox and TabBarComponent took 0 for "no tint", so tint-transparent
gave the selected tab the system's colors. The tint now travels as a
nullable color: transparent is a color, null the system's. The
indicator keeps 12% of the tint's own alpha, so a transparent tint draws
none.
@huacnlee

Copy link
Copy Markdown
Member

The tintColor is not cover the all platforms, please remove and use existing primary color instead

`tint-*` is not a style every platform takes, so the bar no longer reads
it. The core's `TabBar` sends the theme's `primary`, for the appearance
shown, as the bar's `tint` prop; iOS sets it as the `UITabBar`'s
`tintColor`, Android draws the selected icon and label in it over an
indicator of it at 12% opacity. The style plumbing for a component's
tint and its docs are gone.
@jt-wang jt-wang changed the title mobile: Color the TabBar's selected tab with its tintColor mobile: Color the TabBar's selected tab with the theme's primary Oct 31, 2026
@jt-wang

jt-wang commented Oct 31, 2026

Copy link
Copy Markdown
Contributor Author

Done in 53d93b4: TabBar no longer reads tint-*. The core sends the theme's primary (for the appearance shown) to the bar, and iOS and Android draw the selected tab in it; the tint plumbing through ComponentBox and the iOS component container is removed. The description has the new screenshots, light and dark, on both platforms.

One choice left for you: an app without its own theme now gets the default primary (near black / near white) instead of the system accent. If you'd rather keep the system accent there, as #742 does for dialogs, I'll send no tint when the tokens are the default theme's.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants