Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/6.x/docs/guides/bottom-navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ title: Using BottomNavigation with React Navigation
Build a Material Design bottom tab bar by combining two pieces:

- `@react-navigation/bottom-tabs` handles routing, state, and screen options.
- `BottomNavigation.Bar` renders the Material 3 tab bar (ripple, badges, shifting/labeled modes).
- `BottomNavigation.Bar` renders the Material 3 Expressive tab bar (active indicator, badges, optional shifting labels, and vertical or horizontal items).

<img src="/react-native-paper/screenshots/material-bottom-tabs.gif" style={{ width: '420px', maxWidth: '100%', margin: '16px 0' }} />

Expand Down Expand Up @@ -36,6 +36,7 @@ function MyTabs() {
<BottomNavigation.Bar
navigationState={state}
safeAreaInsets={insets}
itemLayout="auto"
onTabPress={({ route, preventDefault }) => {
const event = navigation.emit({
type: 'tabPress',
Expand Down
26 changes: 26 additions & 0 deletions docs/6.x/docs/guides/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ The following props now accept animated styles returned from `useAnimatedStyle`.
- `Appbar.Action` and `Appbar.BackAction`: `style`
- `Badge`: `style`
- `Banner`: `style`
- `BottomNavigation`: `barStyle`, and `BottomNavigation.Bar`: `style`
- `Button`: `style`
- `Card`: `style`
- `Chip`: `style`
Expand Down Expand Up @@ -109,6 +110,31 @@ Some components now accept explicit `testID` props for their interactable elemen

## Components

### BottomNavigation

The bar follows the Material Design 3 Expressive navigation bar spec.

- Height is 64dp (was 80dp when unlabeled, and the label used a 56dp height constant).
- The active indicator is 56×32 with a full corner, and it now stays mounted so the pill can scale and fade with a spatial spring.
- Active labels use the `secondary` color and `labelMediumEmphasized`. Horizontal items (medium windows) place the label on the indicator and use `onSecondaryContainer`.
- Destinations use visible state layers. The previous `rippleColor: 'transparent'` treatment is gone.
- `itemLayout` selects `vertical`, `horizontal`, or `auto` (switches at 600dp). The default is `auto`.
- `shifting` no longer translates icons or requires two tabs. It only fades inactive labels in place.
- Scene and bar animations use Reanimated. `barStyle` / `BottomNavigation.Bar` `style` accept Reanimated animated styles, not `Animated.Value`. `sceneAnimationEasing` is an `(value: number) => number` function.
- Screens still lazy-mount on first visit. Route updates only remount a destination when its `key` changes; inactive screens keep their mounted state.

```diff
<BottomNavigation
navigationState={{ index, routes }}
onIndexChange={setIndex}
renderScene={renderScene}
- sceneAnimationEasing={Easing.ease}
+ itemLayout="auto"
+ sceneAnimationEnabled
+ sceneAnimationType="opacity"
/>
```

### Appbar

The `style` props for `Appbar` and `Appbar.Header` no longer accept `Animated.Value` or `Animated.AnimatedInterpolation`. They only accept static styles.
Expand Down
2 changes: 2 additions & 0 deletions docs/src/data/extendedExamples/BottomNavigationBar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const MyTabs = createBottomTabNavigator({
<BottomNavigation.Bar
navigationState={state}
safeAreaInsets={insets}
itemLayout="auto"
onTabPress={({ route, preventDefault }) => {
const event = navigation.emit({
type: 'tabPress',
Expand Down Expand Up @@ -135,6 +136,7 @@ export default function App() {
<BottomNavigation.Bar
navigationState={state}
safeAreaInsets={insets}
itemLayout="auto"
onTabPress={({ route, preventDefault }) => {
const event = navigation.emit({
type: 'tabPress',
Expand Down
1 change: 1 addition & 0 deletions example/src/Examples/BottomNavigationBarExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const BottomNavigationBarExample = createBottomTabNavigator({
tabBar: ({ navigation, state, descriptors }) => (
<BottomNavigation.Bar
navigationState={state}
itemLayout="auto"
onTabPress={({ route, preventDefault }) => {
const event = navigation.emit({
type: 'tabPress',
Expand Down
85 changes: 65 additions & 20 deletions example/src/Examples/BottomNavigationExample.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import * as React from 'react';
import {
Dimensions,
Easing,
Image,
Platform,
StyleSheet,
View,
} from 'react-native';
import { Dimensions, Image, Platform, StyleSheet, View } from 'react-native';

import { useNavigation } from '@react-navigation/native';
import { Appbar, BottomNavigation, Menu } from 'react-native-paper';
import type { BottomNavigationRoute } from 'react-native-paper';
import type {
BottomNavigationItemLayout,
BottomNavigationRoute,
} from 'react-native-paper';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import ScreenWrapper from '../ScreenWrapper';

type Route = { route: { key: string } };
type SceneAnimation = React.ComponentProps<
typeof BottomNavigation
>['sceneAnimationType'];

const MORE_ICON = Platform.OS === 'ios' ? 'dots-horizontal' : 'dots-vertical';

Expand All @@ -40,16 +39,24 @@ const PhotoGallery = ({ route }: Route) => {
);
};

const renderScene = BottomNavigation.SceneMap({
album: PhotoGallery,
library: PhotoGallery,
favorites: PhotoGallery,
purchased: PhotoGallery,
});

const BottomNavigationExample = () => {
const navigation = useNavigation('BottomNavigation');

const insets = useSafeAreaInsets();
const [index, setIndex] = React.useState(0);
const [menuVisible, setMenuVisible] = React.useState(false);
const [sceneAnimation, setSceneAnimation] =
React.useState<
React.ComponentProps<typeof BottomNavigation>['sceneAnimationType']
>();
const [sceneAnimation, setSceneAnimation] = React.useState<SceneAnimation>();
const [labeled, setLabeled] = React.useState(true);
const [shifting, setShifting] = React.useState(false);
const [itemLayout, setItemLayout] =
React.useState<BottomNavigationItemLayout>('auto');

const [routes] = React.useState<BottomNavigationRoute[]>([
{
Expand All @@ -75,6 +82,7 @@ const BottomNavigationExample = () => {
title: 'Purchased',
focusedIcon: 'shopping',
unfocusedIcon: 'shopping-outline',
badge: 3,
},
]);

Expand Down Expand Up @@ -123,22 +131,59 @@ const BottomNavigationExample = () => {
}}
title="Scene animation: opacity"
/>
<Menu.Item
trailingIcon={labeled ? 'check' : undefined}
onPress={() => {
setLabeled((value) => !value);
setMenuVisible(false);
}}
title={labeled ? 'Labels: on' : 'Labels: off'}
/>
<Menu.Item
trailingIcon={shifting ? 'check' : undefined}
onPress={() => {
setShifting((value) => !value);
setMenuVisible(false);
}}
title={shifting ? 'Shifting labels: on' : 'Shifting labels: off'}
/>
<Menu.Item
trailingIcon={itemLayout === 'auto' ? 'check' : undefined}
onPress={() => {
setItemLayout('auto');
setMenuVisible(false);
}}
title="Layout: auto"
/>
<Menu.Item
trailingIcon={itemLayout === 'vertical' ? 'check' : undefined}
onPress={() => {
setItemLayout('vertical');
setMenuVisible(false);
}}
title="Layout: vertical"
/>
<Menu.Item
trailingIcon={itemLayout === 'horizontal' ? 'check' : undefined}
onPress={() => {
setItemLayout('horizontal');
setMenuVisible(false);
}}
title="Layout: horizontal"
/>
</Menu>
</Appbar.Header>
<BottomNavigation
safeAreaInsets={{ bottom: insets.bottom }}
navigationState={{ index, routes }}
onIndexChange={setIndex}
labelMaxFontSizeMultiplier={2}
renderScene={BottomNavigation.SceneMap({
album: PhotoGallery,
library: PhotoGallery,
favorites: PhotoGallery,
purchased: PhotoGallery,
})}
labeled={labeled}
shifting={shifting}
itemLayout={itemLayout}
renderScene={renderScene}
sceneAnimationEnabled={sceneAnimation !== undefined}
sceneAnimationType={sceneAnimation}
sceneAnimationEasing={Easing.ease}
getLazy={({ route }) => route.key !== 'album'}
/>
</View>
Expand Down
Loading
Loading