feat(navigation-bar): modernize to Material Design 3#5006
Conversation
Move BottomNavigationBar into a new NavigationBar module and expose it as a top-level `NavigationBar` export. `BottomNavigation.Bar` is kept as a deprecated alias re-exporting the same component. No behavior change. Re callstack#4975
Extract NavigationBar/tokens.ts with resolved M3 spec values. Fix bar height (56->64), no-label container height (80->64) and active label color (onSurface->secondary, per M3 / Compose Material3 1.4.0). Re callstack#4975
Remove the `shifting` prop and all its branches from NavigationBar; it is a Material Design 2 pattern with no MD3 equivalent. The wrapper no longer forwards it and keeps `shifting` as a deprecated no-op prop. Scene transition animation (sceneAnimationType) is unaffected. Re callstack#4975
Extract NavigationBarItem and render a pill-shaped state-layer overlay driven by hover/focus/press interaction state via getStateLayer (8% hover, 10% focus/press), replacing the suppressed ripple feedback. Re callstack#4975
Replace hardcoded durations/easings with motion tokens and animate the active indicator with the M3-Expressive spatial spring (Animated.spring). A custom animationEasing still opts into timed movement; reduce-motion jumps instantly. Re callstack#4975
Add a `variant` prop ('stacked' | 'horizontal'). The horizontal arrangement
places the icon beside the label inside a content-hugging indicator pill, for
medium-width windows. It is a no-op without labels. The indicator springs its
opacity/scale in place.
Re callstack#4975
Add a NavigationBarExample with a responsive stacked/horizontal toggle, label toggle and badges, registered in the example list. Rewrite the NavigationBar JSDoc with variant docs and a migration note from the deprecated BottomNavigation.Bar. Re callstack#4975
Stacked labels were collapsed to zero width by `alignItems: center` on the item container; remove it (the icon centers via alignSelf). The active indicator was mounted only while focused and driven by a native value, so it never appeared on a newly-selected tab; always mount it and drive opacity from `active` so it cross-fades between tabs. Re callstack#4975
`shifting` is now a no-op, so the "shifting"/"non-shifting" test pairs were duplicates. Merge them, remove the dead `shifting` props, rename to neutral titles, and prune obsolete snapshots. Keep the deprecation test and the scene-animation coverage. Re callstack#4975
Add a -active-indicator testID and tests pinning the per-state label colors, active indicator color, and badge rendering, so the upcoming item refactor is guarded by explicit behavior rather than snapshots alone. Re callstack#4975
The stacked active/inactive layers were toggled instantly (not animated), so render a single icon + label using the focused color, matching the horizontal layout. Extract shared icon/badge/label rendering across both layouts and merge the vestigial v3* duplicate styles. Behavior-preserving; characterization tests unchanged. Re callstack#4975
Move the color resolvers from BottomNavigation/utils into the NavigationBar module (reversing the cross-module dependency) and express them via colorRoles. Use colors[colorRoles.activeIndicator] for the indicator background instead of a hardcoded role. Same resolved colors; no behavior change. Re callstack#4975
Register NavigationBar in the docs component map and drop the deprecated BottomNavigationBar entry (now a re-export that react-docgen can't parse). Point the BottomNavigation JSDoc link at NavigationBar. Re callstack#4975
There was a problem hiding this comment.
Pull request overview
This PR modernizes the existing MD3 bottom navigation bar implementation to align with the Material Design 3 “Navigation bar” (including the M3 Expressive updates) by introducing a new top-level NavigationBar export (with a flexible horizontal variant) and deprecating BottomNavigation.Bar as an alias.
Changes:
- Added a new
NavigationBarcomponent with token-driven sizing/colors, MD3 state layers, updated motion, and a newvariantprop (stacked/horizontal). - Deprecated
BottomNavigation.Barby re-exportingNavigationBar, and removed the MD2-onlyshiftingbehavior from the bar (kept as deprecated no-op on the wrapper). - Updated tests/examples/docs plumbing to cover the new component, variant behavior, and updated color/state-layer expectations.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/index.tsx | Exports NavigationBar and its public types from the library entrypoint. |
| src/components/NavigationBar/utils.ts | Updates tint/label color resolution to use MD3 color-role tokens. |
| src/components/NavigationBar/tokens.ts | Introduces centralized spec/token constants (sizes, roles) for the NavigationBar. |
| src/components/NavigationBar/NavigationBar.tsx | Adds the new MD3 NavigationBar implementation (state layers, motion tokens, horizontal variant). |
| src/components/NavigationBar/index.tsx | Provides the component barrel export and type re-exports. |
| src/components/BottomNavigation/BottomNavigationBar.tsx | Deprecates the old module by re-exporting NavigationBar for backwards compatibility. |
| src/components/BottomNavigation/BottomNavigation.tsx | Deprecates shifting and updates docs + wiring to the new bar behavior. |
| src/components/tests/BottomNavigation.test.tsx | Extends/updates characterization tests for new NavigationBar behaviors and updated color expectations. |
| example/src/Examples/NavigationBarExample.tsx | Adds a new example demonstrating stacked↔horizontal behavior and label toggling. |
| example/src/ExampleList.tsx | Registers the new NavigationBar example in the example app list. |
| docs/docusaurus.config.js | Updates the docs generation page map to include NavigationBar and remove the old bar page entry. |
| * @deprecated Use `NavigationBar` instead. `BottomNavigation.Bar` is the M3 | ||
| * "original" navigation bar and has been superseded by the flexible | ||
| * `NavigationBar`. This module re-exports `NavigationBar` for backwards | ||
| * compatibility and will be removed in a future major version. |
There was a problem hiding this comment.
Resolved in 13d34e9 — the BottomNavigationBar module is deleted entirely (no deprecated alias).
| /** | ||
| * @deprecated Use the top-level `NavigationBar` export instead. | ||
| * `BottomNavigation.Bar` is the M3 "original" navigation bar, superseded by the | ||
| * flexible `NavigationBar`. Kept as an alias for backwards compatibility. | ||
| */ |
There was a problem hiding this comment.
Resolved in 13d34e9 — the deprecation block is removed; BottomNavigation renders NavigationBar directly.
| }) | ||
| ).start(() => { | ||
| // Workaround a bug in native animations where this is reset after first animation | ||
| tabsAnims.map((tab, i) => tab.setValue(i === index ? 1 : 0)); |
There was a problem hiding this comment.
Fixed in c6bc35f — that whole animateToIndex path (and the .map side-effect) is gone with the reanimated migration.
| /> | ||
| ); | ||
|
|
||
| // Each tab renders an active and inactive label layer, so both match. |
There was a problem hiding this comment.
Fixed in 13d34e9 — comment now reads "Each tab renders a single label (no cross-fade layers)."
satya164
left a comment
There was a problem hiding this comment.
Looks like this now removes the separate bar component and combines both the view and bar to a single component. It's important that the bar is standalone so it can be easily used in react navigation.
The view that animates screen transitions may not even be needed as react navigation also has transition animations for tab switches.
Also since you're working on UI components, please share videos on android, ios and web.
| @@ -148,6 +147,9 @@ const config = { | |||
| MenuItem: 'Menu/MenuItem', | |||
| }, | |||
| Modal: 'Modal', | |||
| NavigationBar: { | |||
| NavigationBar: 'NavigationBar/NavigationBar', | |||
| }, | |||
There was a problem hiding this comment.
Is the BottomNavigation component still needed? it'd be inconsistent to have a mix of old and new APIs
There was a problem hiding this comment.
Kept intentionally. BottomNavigation is the router-integrated scene component (owns tab screens + transitions); NavigationBar is the standalone bar for use with React Navigation's own navigator — same layering split as React Navigation itself, so it isn't an old-vs-new API mix. Every deprecation/alias is gone (13d34e9), so there's no MD2 surface remaining.
| /** | ||
| * @deprecated Use the top-level `NavigationBar` export instead. | ||
| * `BottomNavigation.Bar` is the M3 "original" navigation bar, superseded by the | ||
| * flexible `NavigationBar`. Kept as an alias for backwards compatibility. | ||
| */ |
There was a problem hiding this comment.
There shouldn't be any deprecations. Remove any old code entirely
There was a problem hiding this comment.
Done in 13d34e9 — removed the BottomNavigation.Bar alias, the BottomNavigationBar module, and the dead no-op shifting prop. No deprecated surface left.
| Animated, | ||
| Easing, |
There was a problem hiding this comment.
Done in c6bc35f — migrated the bar's motion to react-native-reanimated (shared values + worklets). This also let me delete the per-tab Animated.Value array, animateToIndex, and the old native-driver reset workaround; each item now springs its own selection progress from its focused prop.
| /** | ||
| * The scene animation Easing. | ||
| */ | ||
| animationEasing?: EasingFunction | undefined; |
There was a problem hiding this comment.
not sure this needs to be customizable. lets remove it
There was a problem hiding this comment.
Done in c6bc35f — removed animationEasing (and its BottomNavigation passthrough). Selection always uses the M3-expressive spring now; reduce-motion is honored via reanimated's ReduceMotion.
- remove deprecated BottomNavigation.Bar alias + BottomNavigationBar module - remove dead no-op shifting prop from BottomNavigation - BottomNavigation renders NavigationBar directly - migrate react-navigation example to top-level NavigationBar Addresses satya164: no deprecations, remove old code.
- replace RN Animated with react-native-reanimated (shared values + worklets) - each item springs its own selection progress from its focused prop, removing the central tabsAnims array, animateToIndex, and the native-driver reset workaround - keyboard slide uses withTiming + useAnimatedStyle on a wrapper view - honor reduce motion via ReduceMotion (drops theme.animation.scale gate) - remove customizable animationEasing prop (+ its BottomNavigation passthrough) Addresses satya164: move to reanimated, drop animationEasing. Also Copilot: map-for-side-effects removed with animateToIndex.
|
Thanks @satya164 — addressed the round: Standalone bar. The bar is standalone: Scene-transition view. Left No deprecations / old code. Removed the Reanimated. Motion is fully on react-native-reanimated now (c6bc35f). Videos (android/ios/web) — added to the description. |
Motivation
BottomNavigation.Barhad drifted from the current Material Design 3 navigation bar spec, including the M3 Expressive update. As part of the v6 MD3 modernization effort (alongside TextInput, Switch, Checkbox, FAB), this brings it up to spec, renames it, and adds the flexible (horizontal) variant:onSurfaceinstead ofsecondary), and the bar height (56dp) and no-label container height (80dp) didn't match the 64dp spec.rippleColor: 'transparent'suppressed all interaction feedback — MD3 requires visible state layers.shiftingis a Material Design 2 pattern with no MD3 equivalent.BottomNavigation.Bar) no longer matches the spec's "navigation bar".Related issue
Closes #4975.
What changed
NavigationBarexport — the MD3 flexible navigation bar.BottomNavigation.Baris kept as a deprecated alias; the router-awareBottomNavigationwrapper is unchanged.onSurface→secondary, bar height 56 → 64dp, no-label container 80 → 64dp. Values extracted toNavigationBar/tokens.ts.getStateLayerutil, replacing the suppressed ripple. Keyboard focus is represented by the focus state layer.variantprop ('stacked' | 'horizontal'): horizontal lays the icon beside the label inside a content-hugging indicator pill, for medium-width windows. No-op without labels.shiftingmode from the bar (kept as a deprecated no-op on the wrapper for scene-transition animation only).NavigationBarExamplewith a responsive stacked↔horizontal toggle; JSDoc forNavigationBarincluding the flexible-vs-original migration note.Test plan
yarn test— 737 passing / 168 snapshots (extendedBottomNavigation.test.tsxwith thevariantlayouts, state-layer opacities, per-state label/indicator colors, the deprecated-shiftingno-op, and badge rendering).yarn typescript,yarn lint— clean.BottomNavigation.Barstill renders.Videos
navbar_ios_small.mp4
navbar_and_small.mp4
navbar_web.mp4