Skip to content

feat(navigation-bar): modernize to Material Design 3#5006

Open
burczu wants to merge 15 commits into
callstack:mainfrom
burczu:feat/navigation-bar-md3
Open

feat(navigation-bar): modernize to Material Design 3#5006
burczu wants to merge 15 commits into
callstack:mainfrom
burczu:feat/navigation-bar-md3

Conversation

@burczu

@burczu burczu commented Jun 18, 2026

Copy link
Copy Markdown

Motivation

BottomNavigation.Bar had 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:

  • Active label used the wrong color role (onSurface instead of secondary), 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.
  • shifting is a Material Design 2 pattern with no MD3 equivalent.
  • The flexible navigation bar (horizontal items for medium windows) wasn't supported, and the component name (BottomNavigation.Bar) no longer matches the spec's "navigation bar".

Related issue

Closes #4975.

What changed

  • New top-level NavigationBar export — the MD3 flexible navigation bar. BottomNavigation.Bar is kept as a deprecated alias; the router-aware BottomNavigation wrapper is unchanged.
  • Spec fixes (token-driven): active label onSurfacesecondary, bar height 56 → 64dp, no-label container 80 → 64dp. Values extracted to NavigationBar/tokens.ts.
  • Visible MD3 state layers: pill-shaped hover (8%) / focus / press (10%) layers via the shared getStateLayer util, replacing the suppressed ripple. Keyboard focus is represented by the focus state layer.
  • Motion: durations/easings now come from the motion tokens, and the active indicator animates with the M3-Expressive spring on selection.
  • Flexible variant prop ('stacked' | 'horizontal'): horizontal lays the icon beside the label inside a content-hugging indicator pill, for medium-width windows. No-op without labels.
  • Removed the MD2 shifting mode from the bar (kept as a deprecated no-op on the wrapper for scene-transition animation only).
  • Internal cleanup: collapsed a vestigial active/inactive cross-fade, deduped item rendering across both layouts, and consolidated color tokens — all behavior-preserving and guarded by characterization tests.
  • Example + docs: new NavigationBarExample with a responsive stacked↔horizontal toggle; JSDoc for NavigationBar including the flexible-vs-original migration note.

Test plan

  • yarn test — 737 passing / 168 snapshots (extended BottomNavigation.test.tsx with the variant layouts, state-layer opacities, per-state label/indicator colors, the deprecated-shifting no-op, and badge rendering).
  • yarn typescript, yarn lint — clean.
  • Verified on the iOS simulator (iPhone 17 Pro):
    • Stacked: MD3 colors/sizing, indicator springs on tab change, state layers on interaction.
    • Horizontal: icon beside label with a hugging pill; indicator follows selection.
    • Deprecated BottomNavigation.Bar still renders.

Videos

navbar_ios_small.mp4
navbar_and_small.mp4
navbar_web.mp4

burczu added 13 commits June 17, 2026 09:12
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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 NavigationBar component with token-driven sizing/colors, MD3 state layers, updated motion, and a new variant prop (stacked/horizontal).
  • Deprecated BottomNavigation.Bar by re-exporting NavigationBar, and removed the MD2-only shifting behavior 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.

Comment on lines +2 to +5
* @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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Resolved in 13d34e9 — the BottomNavigationBar module is deleted entirely (no deprecated alias).

Comment on lines +604 to +608
/**
* @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.
*/

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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));

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 13d34e9 — comment now reads "Each tab renders a single label (no cross-fade layers)."

@satya164 satya164 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.

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.

Comment thread docs/docusaurus.config.js
Comment on lines 86 to +152
@@ -148,6 +147,9 @@ const config = {
MenuItem: 'Menu/MenuItem',
},
Modal: 'Modal',
NavigationBar: {
NavigationBar: 'NavigationBar/NavigationBar',
},

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.

Is the BottomNavigation component still needed? it'd be inconsistent to have a mix of old and new APIs

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

Comment on lines +604 to +608
/**
* @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.
*/

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.

There shouldn't be any deprecations. Remove any old code entirely

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 13d34e9 — removed the BottomNavigation.Bar alias, the BottomNavigationBar module, and the dead no-op shifting prop. No deprecated surface left.

Comment on lines +3 to +4
Animated,
Easing,

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.

Lets move to reanimated

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

Comment on lines +192 to +195
/**
* The scene animation Easing.
*/
animationEasing?: EasingFunction | undefined;

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.

not sure this needs to be customizable. lets remove it

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

burczu added 2 commits June 18, 2026 15:57
- 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.
@burczu

burczu commented Jun 18, 2026

Copy link
Copy Markdown
Author

Thanks @satya164 — addressed the round:

Standalone bar. The bar is standalone: NavigationBar is its own module and the top-level export; BottomNavigation just consumes it. The earlier re-export shim made it look merged — that's gone now, and I migrated the React Navigation example to use NavigationBar directly to show the standalone path.

Scene-transition view. Left BottomNavigation in place — it's still useful when not relying on React Navigation's own tab transitions. Happy to drop it in a follow-up if you'd rather not ship both.

No deprecations / old code. Removed the .Bar alias, the BottomNavigationBar module, the no-op shifting prop, and animationEasing (13d34e9, c6bc35f).

Reanimated. Motion is fully on react-native-reanimated now (c6bc35f).

Videos (android/ios/web) — added to the description.

@burczu burczu requested a review from satya164 June 19, 2026 06:49
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.

feat: modernize BottomNavigationBar

3 participants