diff --git a/docs/6.x/docs/guides/migration.md b/docs/6.x/docs/guides/migration.md
index a401ad8de8..372bac9e3d 100644
--- a/docs/6.x/docs/guides/migration.md
+++ b/docs/6.x/docs/guides/migration.md
@@ -135,6 +135,25 @@ The `style` props for `Appbar` and `Appbar.Header` no longer accept `Animated.Va
The `style.elevation` property is no longer supported. Use the `elevated` prop to control Appbar elevation.
+### Drawer
+
+`NavigationRail` is the Material Design 3 replacement for `Drawer.CollapsedItem` and the drawer-based side navigation. It supports a collapsed (96dp) and expanded (220–360dp) layout, a header slot for a menu button and `FAB`, and a modal variant.
+
+```diff
+-
+-
+-
+-
++
++
++
++
+```
+
+- **`focusedIcon` / `unfocusedIcon`** → **`activeIcon` / `icon`**
+- Expanded rows replace `Drawer.Item`; toggle them with the `expanded` prop instead of rendering a different component.
+- Use `NavigationRail.Modal` inside a `Portal` where a temporary drawer was used before.
+
### Surface
- The `elevation` prop no longer accepts a React Native `Animated.Value`. Any `elevation` changes are animated automatically.
diff --git a/docs/component-docs.config.ts b/docs/component-docs.config.ts
index 4196e8e7bd..8276ad46b6 100644
--- a/docs/component-docs.config.ts
+++ b/docs/component-docs.config.ts
@@ -115,6 +115,11 @@ const pages = {
MenuItem: 'Menu/MenuItem',
},
Modal: 'Modal',
+ NavigationRail: {
+ NavigationRail: 'NavigationRail/NavigationRail',
+ NavigationRailItem: 'NavigationRail/NavigationRailItem',
+ NavigationRailModal: 'NavigationRail/NavigationRailModal',
+ },
Portal: {
Portal: {
source: 'Portal/Portal',
diff --git a/docs/src/data/themeColors.ts b/docs/src/data/themeColors.ts
index 20f16962f7..14fd738d57 100644
--- a/docs/src/data/themeColors.ts
+++ b/docs/src/data/themeColors.ts
@@ -264,6 +264,31 @@ export const themeColors = {
backgroundColor: 'theme.colors.backdrop',
},
},
+ NavigationRail: {
+ collapsed: {
+ backgroundColor: 'theme.colors.surface',
+ },
+ expanded: {
+ backgroundColor: 'theme.colors.surfaceContainer',
+ },
+ },
+ 'NavigationRail.Item': {
+ active: {
+ indicatorColor: 'theme.colors.secondaryContainer',
+ iconColor: 'theme.colors.onSecondaryContainer',
+ 'textColor (collapsed)': 'theme.colors.secondary',
+ 'textColor (expanded)': 'theme.colors.onSecondaryContainer',
+ },
+ inactive: {
+ 'iconColor/textColor': 'theme.colors.onSurfaceVariant',
+ },
+ },
+ 'NavigationRail.Modal': {
+ '-': {
+ backgroundColor: 'theme.colors.surfaceContainer',
+ scrimColor: 'theme.colors.scrim',
+ },
+ },
ProgressBar: {
'-': {
tintColor: 'theme.colors.primary',
diff --git a/example/src/ExampleList.tsx b/example/src/ExampleList.tsx
index 8a2646d798..c28e18c479 100644
--- a/example/src/ExampleList.tsx
+++ b/example/src/ExampleList.tsx
@@ -27,6 +27,7 @@ import ListAccordionExampleGroup from './Examples/ListAccordionGroupExample';
import ListItemExample from './Examples/ListItemExample';
import ListSectionExample from './Examples/ListSectionExample';
import MenuExample from './Examples/MenuExample';
+import NavigationRailExample from './Examples/NavigationRailExample';
import ProgressBarExample from './Examples/ProgressBarExample';
import RadioButtonExample from './Examples/RadioButtonExample';
import RadioButtonGroupExample from './Examples/RadioButtonGroupExample';
@@ -72,6 +73,7 @@ export const mainExamples = {
ListSection: ListSectionExample,
ListItem: ListItemExample,
Menu: MenuExample,
+ NavigationRail: NavigationRailExample,
Progressbar: ProgressBarExample,
Radio: RadioButtonExample,
RadioGroup: RadioButtonGroupExample,
diff --git a/example/src/Examples/NavigationRailExample.tsx b/example/src/Examples/NavigationRailExample.tsx
new file mode 100644
index 0000000000..2e3dbc3be7
--- /dev/null
+++ b/example/src/Examples/NavigationRailExample.tsx
@@ -0,0 +1,201 @@
+import * as React from 'react';
+import { StyleSheet, View } from 'react-native';
+import type { ViewStyle } from 'react-native';
+
+import {
+ Button,
+ Chip,
+ FAB,
+ IconButton,
+ List,
+ NavigationRail,
+ Portal,
+ Switch,
+ Text,
+ useTheme,
+} from 'react-native-paper';
+import type { NavigationRailProps } from 'react-native-paper';
+import Animated, { cubicBezier } from 'react-native-reanimated';
+import type { AnimatedStyle } from 'react-native-reanimated';
+
+type Alignment = NonNullable;
+
+const destinations = [
+ { key: 'inbox', label: 'Inbox', icon: 'inbox-outline', activeIcon: 'inbox' },
+ {
+ key: 'starred',
+ label: 'Starred',
+ icon: 'star-outline',
+ activeIcon: 'star',
+ },
+ { key: 'sent', label: 'Sent', icon: 'send-outline', activeIcon: 'send' },
+ {
+ key: 'drafts',
+ label: 'Drafts',
+ icon: 'file-outline',
+ activeIcon: 'file',
+ },
+ {
+ key: 'trash',
+ label: 'Trash',
+ icon: 'delete-outline',
+ activeIcon: 'delete',
+ },
+];
+
+const alignments: Alignment[] = ['top', 'center', 'bottom'];
+
+const NavigationRailExample = () => {
+ const { colors, motion } = useTheme();
+ const [active, setActive] = React.useState('inbox');
+ const [expanded, setExpanded] = React.useState(false);
+ const [alignment, setAlignment] = React.useState('top');
+ const [labeled, setLabeled] = React.useState(true);
+ const [overlay, setOverlay] = React.useState(false);
+ const [animated, setAnimated] = React.useState(true);
+ const [modalVisible, setModalVisible] = React.useState(false);
+
+ const toggleStyle: AnimatedStyle = {
+ transform: [{ rotate: expanded ? '0deg' : '180deg' }],
+ transitionProperty: 'transform',
+ transitionDuration: animated ? motion.duration.medium2 : 0,
+ transitionTimingFunction: cubicBezier(...motion.easing.emphasized),
+ };
+
+ const renderItems = () =>
+ destinations.map(({ key, label, ...rest }) => (
+ setActive(key)}
+ />
+ ));
+
+ return (
+
+ setExpanded(false)}
+ header={
+ <>
+
+ setExpanded((value) => !value)}
+ />
+
+ {}}
+ />
+ >
+ }
+ >
+ {renderItems()}
+
+
+
+ {active}
+ (
+
+
+
+ )}
+ onPress={() => setExpanded((value) => !value)}
+ />
+ (
+
+
+
+ )}
+ onPress={() => setAnimated((value) => !value)}
+ />
+ (
+
+
+
+ )}
+ onPress={() => setOverlay((value) => !value)}
+ />
+ (
+
+
+
+ )}
+ onPress={() => setLabeled((value) => !value)}
+ />
+
+ {alignments.map((option) => (
+ setAlignment(option)}
+ >
+ {option}
+
+ ))}
+
+
+
+
+
+ setModalVisible(false)}
+ alignment={alignment}
+ animated={animated}
+ header={
+ setModalVisible(false)}
+ />
+ }
+ >
+ {renderItems()}
+
+
+
+ );
+};
+
+NavigationRailExample.title = 'Navigation Rail';
+
+const styles = StyleSheet.create({
+ screen: {
+ flex: 1,
+ flexDirection: 'row',
+ },
+ content: {
+ flex: 1,
+ padding: 16,
+ gap: 16,
+ },
+ chips: {
+ flexDirection: 'row',
+ gap: 8,
+ },
+});
+
+export default NavigationRailExample;
diff --git a/src/components/NavigationRail/NavigationRail.tsx b/src/components/NavigationRail/NavigationRail.tsx
new file mode 100644
index 0000000000..9117305c89
--- /dev/null
+++ b/src/components/NavigationRail/NavigationRail.tsx
@@ -0,0 +1,307 @@
+import * as React from 'react';
+import {
+ Pressable,
+ ScrollView,
+ StyleSheet,
+ useWindowDimensions,
+ View,
+} from 'react-native';
+import type { ColorValue, StyleProp, ViewStyle } from 'react-native';
+
+import Animated from 'react-native-reanimated';
+import type { AnimatedStyle } from 'react-native-reanimated';
+
+import { NavigationRailContext } from './context';
+import { NavigationRailTokens } from './tokens';
+import type { Alignment } from './tokens';
+import { clampExpandedWidth, getTransition } from './utils';
+import { useInternalTheme } from '../../core/theming';
+import { useReduceMotion } from '../../theme/accessibility/ReduceMotionContext';
+import { tokens } from '../../theme/tokens';
+import type { ThemeProp } from '../../theme/types';
+import { resolveCornerRadius } from '../../theme/utils/shape';
+
+export type Props = {
+ /**
+ * Navigation destinations, typically `NavigationRail.Item` elements.
+ */
+ children: React.ReactNode;
+ /**
+ * Whether the rail is expanded (icon + label rows, 220–360dp wide) or
+ * collapsed (stacked icon + label, 96dp wide). The width animates on change.
+ */
+ expanded?: boolean;
+ /**
+ * Width of the expanded rail. Clamped to the spec range of 220–360dp.
+ */
+ expandedWidth?: number;
+ /**
+ * Vertical placement of the destinations. Defaults to `top`.
+ */
+ alignment?: Alignment;
+ /**
+ * Content pinned above the destinations, e.g. a menu button and a `FAB`.
+ */
+ header?: React.ReactNode;
+ /**
+ * Whether the expanded rail floats above the content behind a scrim instead
+ * of pushing it. The rail keeps its collapsed footprint in the layout.
+ */
+ overlay?: boolean;
+ /**
+ * Called when the scrim is pressed. Only used with `overlay`.
+ */
+ onDismiss?: () => void;
+ /**
+ * Whether expanding and collapsing is animated. Defaults to `true`.
+ */
+ animated?: boolean;
+ /**
+ * Container color override. Defaults to `theme.colors.surface` when
+ * collapsed and `theme.colors.surfaceContainer` when expanded.
+ */
+ containerColor?: ColorValue;
+ style?: StyleProp>;
+ /**
+ * TestID used for testing purposes.
+ */
+ testID?: string;
+ /**
+ * @optional
+ */
+ theme?: ThemeProp;
+};
+
+const { rail, colors } = NavigationRailTokens;
+const scrimAlpha = tokens.md.sys.scrim.alpha;
+
+const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
+
+const justifyContent = {
+ top: 'flex-start',
+ center: 'center',
+ bottom: 'flex-end',
+} as const satisfies Record;
+
+/**
+ * Navigation rails let people switch between UI views on mid-sized devices.
+ * The rail is placed at the start edge of the screen and can be collapsed
+ * (icons with short labels) or expanded (icons with full labels).
+ *
+ * ## Usage
+ * ```js
+ * import * as React from 'react';
+ * import { StyleSheet, View } from 'react-native';
+ * import { FAB, IconButton, NavigationRail } from 'react-native-paper';
+ *
+ * const MyComponent = () => {
+ * const [expanded, setExpanded] = React.useState(false);
+ * const [active, setActive] = React.useState('inbox');
+ *
+ * return (
+ *
+ *
+ * setExpanded((e) => !e)}
+ * />
+ * {}}
+ * />
+ * >
+ * }
+ * >
+ * setActive('inbox')}
+ * />
+ * setActive('sent')}
+ * />
+ *
+ *
+ * );
+ * };
+ *
+ * const styles = StyleSheet.create({
+ * screen: { flex: 1, flexDirection: 'row' },
+ * });
+ *
+ * export default MyComponent;
+ * ```
+ *
+ * ## Theming
+ * Customize by overriding these `theme.colors` roles:
+ * - `surface`: collapsed container
+ * - `surfaceContainer`: expanded container
+ * - `secondaryContainer` / `onSecondaryContainer`: active indicator / active icon
+ * - `secondary`: active label (collapsed), focus indicator
+ * - `onSurfaceVariant`: inactive icon and label
+ * - `scrim`: backdrop behind the floating rail (`overlay`)
+ */
+const NavigationRail = ({
+ children,
+ expanded = false,
+ expandedWidth = rail.expandedMinWidth,
+ alignment = 'top',
+ header,
+ overlay = false,
+ onDismiss,
+ animated = true,
+ containerColor,
+ style,
+ testID = 'navigation-rail',
+ theme: themeOverrides,
+}: Props) => {
+ const theme = useInternalTheme(themeOverrides);
+ const reduceMotion = useReduceMotion();
+ const { width: windowWidth } = useWindowDimensions();
+
+ const targetWidth = clampExpandedWidth(expandedWidth);
+ const width = expanded ? targetWidth : rail.collapsedWidth;
+ const floating = overlay && expanded;
+ const endRadius = floating ? resolveCornerRadius(theme, rail.modalShape) : 0;
+ const backgroundColor =
+ containerColor ??
+ theme.colors[expanded ? colors.expandedContainer : colors.container];
+
+ const context = React.useMemo(
+ () => ({ expanded, expandedWidth: targetWidth, animated }),
+ [expanded, targetWidth, animated]
+ );
+
+ const panel = (
+
+ {header ? {header} : null}
+
+
+ {children}
+
+
+
+ );
+
+ if (!overlay) {
+ return panel;
+ }
+
+ return (
+
+
+ {panel}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ anchor: {
+ width: rail.collapsedWidth,
+ height: '100%',
+ zIndex: 1,
+ },
+ scrim: {
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ start: 0,
+ },
+ scrimShown: {
+ opacity: scrimAlpha,
+ pointerEvents: 'auto',
+ },
+ scrimHidden: {
+ opacity: 0,
+ pointerEvents: 'none',
+ },
+ panel: {
+ height: '100%',
+ paddingTop: rail.topSpace,
+ overflow: 'hidden',
+ },
+ floating: {
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ start: 0,
+ },
+ header: {
+ alignItems: 'flex-start',
+ gap: rail.itemSpace,
+ marginBottom: rail.headerSpace,
+ paddingHorizontal: rail.itemHorizontalPadding,
+ },
+ items: {
+ flex: 1,
+ },
+ itemsContent: {
+ flexGrow: 1,
+ gap: rail.itemSpace,
+ paddingHorizontal: rail.itemHorizontalPadding,
+ },
+});
+
+export default NavigationRail;
+
+// @component-docs ignore-next-line
+export { NavigationRail };
diff --git a/src/components/NavigationRail/NavigationRailItem.tsx b/src/components/NavigationRail/NavigationRailItem.tsx
new file mode 100644
index 0000000000..360da691d4
--- /dev/null
+++ b/src/components/NavigationRail/NavigationRailItem.tsx
@@ -0,0 +1,412 @@
+import * as React from 'react';
+import { Platform, StyleSheet, View } from 'react-native';
+import type {
+ GestureResponderEvent,
+ NativeSyntheticEvent,
+ StyleProp,
+ TargetedEvent,
+ ViewStyle,
+} from 'react-native';
+
+import Animated, {
+ useAnimatedStyle,
+ useSharedValue,
+ withSpring,
+} from 'react-native-reanimated';
+
+import { NavigationRailContext } from './context';
+import { NavigationRailTokens } from './tokens';
+import { getTransition, resolveItemColors } from './utils';
+import { useInternalTheme } from '../../core/theming';
+import { useReduceMotion } from '../../theme/accessibility/ReduceMotionContext';
+import { tokens } from '../../theme/tokens';
+import { toRawSpring } from '../../theme/tokens/sys/motion';
+import type { ThemeProp } from '../../theme/types';
+import { resolveCornerRadius } from '../../theme/utils/shape';
+import { isKeyboardFocusEvent } from '../../utils/isKeyboardFocusEvent';
+import Badge from '../Badge';
+import Icon from '../Icon';
+import type { IconSource } from '../Icon';
+import TouchableRipple from '../TouchableRipple/TouchableRipple';
+import Text from '../Typography/Text';
+
+export type Props = {
+ /**
+ * Icon of the destination.
+ */
+ icon: IconSource;
+ /**
+ * Icon shown while the destination is active. Falls back to `icon`.
+ */
+ activeIcon?: IconSource;
+ /**
+ * Label of the destination. Optional in the collapsed rail.
+ */
+ label?: string;
+ /**
+ * Whether the destination is the current one.
+ */
+ active?: boolean;
+ /**
+ * Whether the destination is disabled.
+ */
+ disabled?: boolean;
+ /**
+ * Badge shown on the icon: `true` for a dot, a `string` or `number` for text.
+ */
+ badge?: string | number | boolean;
+ /**
+ * Function to execute on press.
+ */
+ onPress?: (e: GestureResponderEvent) => void;
+ /**
+ * Function to execute on long press.
+ */
+ onLongPress?: (e: GestureResponderEvent) => void;
+ /**
+ * Accessibility label. Falls back to `label`.
+ */
+ 'aria-label'?: string;
+ /**
+ * Specifies the largest possible scale a label font can reach.
+ */
+ labelMaxFontSizeMultiplier?: number;
+ style?: StyleProp;
+ /**
+ * TestID used for testing purposes.
+ */
+ testID?: string;
+ /**
+ * @optional
+ */
+ theme?: ThemeProp;
+};
+
+const { rail, item } = NavigationRailTokens;
+
+// Badges hang off the icon's trailing edge, far enough out to keep
+// `badgeInset` clear of the collapsed indicator's edge.
+const badgeEnd = -(
+ item.collapsed.indicatorWidth -
+ item.expanded.leading -
+ item.iconSize -
+ item.badgeInset
+);
+const { opacity: stateOpacity, focusIndicator } = tokens.md.sys.state;
+
+// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
+const webNoOutline = { outline: 'none' } as unknown as ViewStyle;
+
+/**
+ * A destination inside a `NavigationRail`. Renders as a stacked icon and
+ * label in the collapsed rail and as a full-width row in the expanded rail,
+ * morphing between the two when the rail expands or collapses.
+ *
+ * ## Usage
+ * ```js
+ * import * as React from 'react';
+ * import { NavigationRail } from 'react-native-paper';
+ *
+ * const MyComponent = () => (
+ *
+ * );
+ *
+ * export default MyComponent;
+ * ```
+ */
+const NavigationRailItem = ({
+ icon,
+ activeIcon,
+ label,
+ active = false,
+ disabled = false,
+ badge = false,
+ onPress,
+ onLongPress,
+ 'aria-label': ariaLabel = label,
+ labelMaxFontSizeMultiplier,
+ style,
+ testID = 'navigation-rail-item',
+ theme: themeOverrides,
+}: Props) => {
+ const theme = useInternalTheme(themeOverrides);
+ const { expanded, expandedWidth, animated } = React.useContext(
+ NavigationRailContext
+ );
+ const reduceMotion = useReduceMotion();
+ const [focused, setFocused] = React.useState(false);
+
+ const colors = resolveItemColors({ theme, active });
+ const indicatorRadius = resolveCornerRadius(theme, item.indicatorShape);
+ const contentOpacity = disabled
+ ? stateOpacity.disabled
+ : stateOpacity.enabled;
+ const hasLabel = !!label;
+ const stacked = hasLabel && !expanded;
+
+ // Collapsed labeled items center the indicator + label block in the min height.
+ const height = stacked
+ ? item.collapsed.minHeight
+ : item.expanded.indicatorHeight;
+ const pillHeight = stacked
+ ? item.collapsed.indicatorHeight
+ : item.expanded.indicatorHeight;
+ const labelBlock =
+ theme.fonts[item.collapsed.labelTypescale].lineHeight +
+ item.collapsed.iconLabelGap;
+ const pillTop = stacked ? (height - pillHeight - labelBlock) / 2 : 0;
+ // Fixed label widths keep text measured once; the item clips the overflow.
+ const rowLabelWidth =
+ expandedWidth -
+ 2 * rail.itemHorizontalPadding -
+ item.expanded.leading -
+ item.iconSize -
+ item.expanded.iconLabelGap -
+ item.expanded.trailing;
+
+ const layoutTransition = getTransition(theme, ['height', 'paddingTop'], {
+ instant: !animated || reduceMotion,
+ });
+ const fadeTransition = getTransition(theme, ['opacity'], {
+ instant: !animated,
+ });
+ const fade = (shown: boolean) => [
+ shown ? styles.shown : styles.hidden,
+ fadeTransition,
+ ];
+
+ const selection = useSharedValue(active ? 1 : 0);
+ const pressed = useSharedValue(false);
+ const hovered = useSharedValue(false);
+
+ React.useEffect(() => {
+ const target = active ? 1 : 0;
+ selection.value =
+ !animated || reduceMotion
+ ? target
+ : withSpring(target, toRawSpring(theme.motion.spring.fast.spatial));
+ }, [active, animated, reduceMotion, theme, selection]);
+
+ const indicatorStyle = useAnimatedStyle(() => ({
+ opacity: selection.value,
+ transform: [{ scaleX: 0.5 + selection.value / 2 }],
+ }));
+
+ const stateLayerStyle = useAnimatedStyle(() => ({
+ opacity: pressed.value
+ ? stateOpacity.pressed
+ : hovered.value
+ ? stateOpacity.hovered
+ : 0,
+ }));
+
+ const onFocus = (e: NativeSyntheticEvent) => {
+ if (!disabled && isKeyboardFocusEvent(e)) setFocused(true);
+ };
+
+ const dot = typeof badge === 'boolean';
+ const badgeNode =
+ badge === false ? null : {dot ? undefined : badge};
+
+ const renderLabel = (row: boolean) => (
+
+ {label}
+
+ );
+
+ return (
+
+ {
+ pressed.value = true;
+ }}
+ onPressOut={() => {
+ pressed.value = false;
+ }}
+ onHoverIn={() => {
+ hovered.value = true;
+ }}
+ onHoverOut={() => {
+ hovered.value = false;
+ }}
+ onFocus={onFocus}
+ onBlur={() => setFocused(false)}
+ role="tab"
+ aria-selected={active}
+ aria-disabled={disabled}
+ aria-label={ariaLabel}
+ testID={testID}
+ style={[styles.touchable, Platform.OS === 'web' ? webNoOutline : null]}
+ theme={theme}
+ >
+
+
+
+ {focused ? (
+
+ ) : null}
+
+
+ {badgeNode ? (
+
+ {badgeNode}
+
+ ) : null}
+
+ {hasLabel ? (
+ <>
+
+ {renderLabel(false)}
+
+
+ {renderLabel(true)}
+
+ >
+ ) : null}
+ {badgeNode ? (
+
+ {badgeNode}
+
+ ) : null}
+
+
+
+ );
+};
+
+NavigationRailItem.displayName = 'NavigationRail.Item';
+
+const styles = StyleSheet.create({
+ touchable: {
+ flex: 1,
+ },
+ pill: {
+ justifyContent: 'center',
+ paddingStart: item.expanded.leading,
+ pointerEvents: 'none',
+ },
+ fill: {
+ ...StyleSheet.absoluteFill,
+ },
+ shown: {
+ opacity: 1,
+ },
+ hidden: {
+ opacity: 0,
+ },
+ focusRing: {
+ margin: -focusIndicator.outerOffset,
+ borderWidth: focusIndicator.thickness,
+ },
+ stackedLabel: {
+ position: 'absolute',
+ top: '100%',
+ start: 0,
+ width: item.collapsed.indicatorWidth,
+ marginTop: item.collapsed.iconLabelGap,
+ alignItems: 'center',
+ },
+ rowLabel: {
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ start: item.expanded.leading + item.iconSize + item.expanded.iconLabelGap,
+ justifyContent: 'center',
+ },
+ iconAnchor: {
+ alignSelf: 'flex-start',
+ },
+ iconBadge: {
+ position: 'absolute',
+ top: 0,
+ end: badgeEnd,
+ alignItems: 'flex-end',
+ },
+ rowBadge: {
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ end: item.expanded.trailing,
+ justifyContent: 'center',
+ alignItems: 'flex-end',
+ },
+});
+
+export default NavigationRailItem;
diff --git a/src/components/NavigationRail/NavigationRailModal.tsx b/src/components/NavigationRail/NavigationRailModal.tsx
new file mode 100644
index 0000000000..d07e9734fd
--- /dev/null
+++ b/src/components/NavigationRail/NavigationRailModal.tsx
@@ -0,0 +1,230 @@
+import * as React from 'react';
+import { Pressable, StyleSheet } from 'react-native';
+
+import Animated from 'react-native-reanimated';
+
+import NavigationRail from './NavigationRail';
+import type { Props as NavigationRailProps } from './NavigationRail';
+import { NavigationRailTokens } from './tokens';
+import { clampExpandedWidth, getTransition } from './utils';
+import { useLocale } from '../../core/locale';
+import { useInternalTheme } from '../../core/theming';
+import { useReduceMotion } from '../../theme/accessibility/ReduceMotionContext';
+import { tokens } from '../../theme/tokens';
+import { resolveCornerRadius } from '../../theme/utils/shape';
+import { addEventListener } from '../../utils/addEventListener';
+import { BackHandler } from '../../utils/BackHandler/BackHandler';
+import Surface from '../Surface';
+
+export type Props = Omit<
+ NavigationRailProps,
+ 'expanded' | 'overlay' | 'onDismiss' | 'containerColor' | 'style'
+> & {
+ /**
+ * Whether the modal rail is visible.
+ */
+ visible: boolean;
+ /**
+ * Callback that is called when the user dismisses the rail.
+ */
+ onDismiss?: () => void;
+ /**
+ * Determines whether tapping the scrim or pressing the hardware back
+ * button dismisses the rail.
+ */
+ dismissable?: boolean;
+ /**
+ * Accessibility label for the scrim.
+ */
+ overlayAccessibilityLabel?: string;
+ style?: NavigationRailProps['style'];
+};
+
+const { rail, colors } = NavigationRailTokens;
+const scrimAlpha = tokens.md.sys.scrim.alpha;
+
+const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
+
+/**
+ * An expanded navigation rail shown above the content with a scrim, for
+ * layouts where the rail is not permanently visible. Slides in from the
+ * start edge and out again on dismiss. Wrap it in a `Portal` to render above
+ * other components.
+ *
+ * ## Usage
+ * ```js
+ * import * as React from 'react';
+ * import { Button, NavigationRail, Portal } from 'react-native-paper';
+ *
+ * const MyComponent = () => {
+ * const [visible, setVisible] = React.useState(false);
+ *
+ * return (
+ * <>
+ *
+ * setVisible(false)}
+ * >
+ *
+ *
+ *
+ *
+ *
+ * >
+ * );
+ * };
+ *
+ * export default MyComponent;
+ * ```
+ */
+const NavigationRailModal = ({
+ visible,
+ onDismiss,
+ dismissable = true,
+ overlayAccessibilityLabel = 'Close navigation rail',
+ expandedWidth = rail.expandedMinWidth,
+ animated = true,
+ style,
+ testID = 'navigation-rail-modal',
+ theme: themeOverrides,
+ ...rest
+}: Props) => {
+ const theme = useInternalTheme(themeOverrides);
+ const { direction } = useLocale();
+ const reduceMotion = useReduceMotion();
+ const [mounted, setMounted] = React.useState(visible);
+ const [shown, setShown] = React.useState(false);
+
+ if (visible && !mounted) {
+ setMounted(true);
+ }
+
+ const { duration, easing } = theme.motion;
+ const enter = {
+ duration: duration.medium4,
+ easing: easing.emphasizedDecelerate,
+ };
+ const exit = {
+ duration: duration.short4,
+ easing: easing.emphasizedAccelerate,
+ };
+ const motion = { ...(shown ? enter : exit), instant: !animated };
+
+ React.useEffect(() => {
+ const timeout = setTimeout(() => setShown(visible), 0);
+ return () => clearTimeout(timeout);
+ }, [visible]);
+
+ React.useEffect(() => {
+ if (visible || !mounted) return undefined;
+ const timeout = setTimeout(
+ () => setMounted(false),
+ animated ? exit.duration : 0
+ );
+ return () => clearTimeout(timeout);
+ }, [visible, mounted, animated, exit.duration]);
+
+ React.useEffect(() => {
+ if (!visible || !dismissable) return undefined;
+ const subscription = addEventListener(
+ BackHandler,
+ 'hardwareBackPress',
+ () => {
+ onDismiss?.();
+ return true;
+ }
+ );
+ return () => subscription.remove();
+ }, [visible, dismissable, onDismiss]);
+
+ if (!mounted) {
+ return null;
+ }
+
+ const width = clampExpandedWidth(expandedWidth);
+ const offscreen = direction === 'rtl' ? width : -width;
+ const endRadius = resolveCornerRadius(theme, rail.modalShape);
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+NavigationRailModal.displayName = 'NavigationRail.Modal';
+
+const styles = StyleSheet.create({
+ panel: {
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ start: 0,
+ },
+ scrim: {
+ opacity: scrimAlpha,
+ },
+ shown: {
+ opacity: 1,
+ },
+ hidden: {
+ opacity: 0,
+ },
+});
+
+export default NavigationRailModal;
diff --git a/src/components/NavigationRail/context.ts b/src/components/NavigationRail/context.ts
new file mode 100644
index 0000000000..efeb6db5ba
--- /dev/null
+++ b/src/components/NavigationRail/context.ts
@@ -0,0 +1,18 @@
+import * as React from 'react';
+
+import { NavigationRailTokens } from './tokens';
+
+/**
+ * Expansion state of the enclosing rail. Items read `expanded` to switch
+ * layouts, `expandedWidth` to size the row label once (so text is not
+ * re-measured while the rail width animates) and `animated` to skip motion.
+ */
+export const NavigationRailContext = React.createContext<{
+ expanded: boolean;
+ expandedWidth: number;
+ animated: boolean;
+}>({
+ expanded: false,
+ expandedWidth: NavigationRailTokens.rail.expandedMinWidth,
+ animated: true,
+});
diff --git a/src/components/NavigationRail/index.ts b/src/components/NavigationRail/index.ts
new file mode 100644
index 0000000000..06dafb299a
--- /dev/null
+++ b/src/components/NavigationRail/index.ts
@@ -0,0 +1,16 @@
+import NavigationRailComponent from './NavigationRail';
+import NavigationRailItem from './NavigationRailItem';
+import NavigationRailModal from './NavigationRailModal';
+
+const NavigationRail = Object.assign(
+ // @component ./NavigationRail.tsx
+ NavigationRailComponent,
+ {
+ // @component ./NavigationRailItem.tsx
+ Item: NavigationRailItem,
+ // @component ./NavigationRailModal.tsx
+ Modal: NavigationRailModal,
+ }
+);
+
+export default NavigationRail;
diff --git a/src/components/NavigationRail/tokens.ts b/src/components/NavigationRail/tokens.ts
new file mode 100644
index 0000000000..ac2f3e0e0e
--- /dev/null
+++ b/src/components/NavigationRail/tokens.ts
@@ -0,0 +1,62 @@
+import type { ColorRole, Elevation, TypescaleKey } from '../../theme/types';
+import type { ShapeToken } from '../../theme/utils/shape';
+
+export type Alignment = 'top' | 'center' | 'bottom';
+
+/**
+ * MD3 Navigation rail spec tokens.
+ * @see https://m3.material.io/components/navigation-rail/specs
+ */
+const rail = {
+ collapsedWidth: 96,
+ expandedMinWidth: 220,
+ expandedMaxWidth: 360,
+ topSpace: 44,
+ headerSpace: 40,
+ itemSpace: 4,
+ itemHorizontalPadding: 20,
+ elevation: 0,
+ modalElevation: 2,
+ modalShape: 'large',
+} as const satisfies Record;
+
+const item = {
+ iconSize: 24,
+ indicatorShape: 'full',
+ badgeInset: 8,
+ collapsed: {
+ minHeight: 64,
+ indicatorWidth: 56,
+ indicatorHeight: 32,
+ iconLabelGap: 4,
+ labelTypescale: 'labelMedium',
+ },
+ expanded: {
+ indicatorHeight: 56,
+ leading: 16,
+ trailing: 16,
+ iconLabelGap: 8,
+ labelTypescale: 'labelLarge',
+ },
+} as const satisfies {
+ iconSize: number;
+ indicatorShape: ShapeToken;
+ badgeInset: number;
+ collapsed: Record;
+ expanded: Record;
+};
+
+const colors = {
+ container: 'surface',
+ expandedContainer: 'surfaceContainer',
+ activeIcon: 'onSecondaryContainer',
+ activeLabel: 'secondary',
+ activeExpandedLabel: 'onSecondaryContainer',
+ activeIndicator: 'secondaryContainer',
+ inactiveIcon: 'onSurfaceVariant',
+ inactiveLabel: 'onSurfaceVariant',
+ stateLayer: 'onSecondaryContainer',
+ focusIndicator: 'secondary',
+} as const satisfies Record;
+
+export const NavigationRailTokens = { rail, item, colors };
diff --git a/src/components/NavigationRail/utils.ts b/src/components/NavigationRail/utils.ts
new file mode 100644
index 0000000000..4b3c2835ff
--- /dev/null
+++ b/src/components/NavigationRail/utils.ts
@@ -0,0 +1,65 @@
+import type { ColorValue } from 'react-native';
+
+import { cubicBezier } from 'react-native-reanimated';
+import type { CSSTransitionProperties } from 'react-native-reanimated';
+
+import { NavigationRailTokens } from './tokens';
+import type { EasingConfig, InternalTheme } from '../../theme/types';
+
+const { rail, colors } = NavigationRailTokens;
+
+export type ItemColors = {
+ icon: ColorValue;
+ label: ColorValue;
+ expandedLabel: ColorValue;
+ indicator: ColorValue;
+ stateLayer: ColorValue;
+ focusIndicator: ColorValue;
+};
+
+/**
+ * Resolve item colors for the current selection state. The active label uses
+ * `secondary` in the collapsed rail and matches the icon in the expanded rail.
+ */
+export const resolveItemColors = ({
+ theme,
+ active = false,
+}: {
+ theme: InternalTheme;
+ active?: boolean;
+}): ItemColors => {
+ const c = theme.colors;
+ return {
+ icon: c[active ? colors.activeIcon : colors.inactiveIcon],
+ label: c[active ? colors.activeLabel : colors.inactiveLabel],
+ expandedLabel:
+ c[active ? colors.activeExpandedLabel : colors.inactiveLabel],
+ indicator: c[colors.activeIndicator],
+ stateLayer: c[colors.stateLayer],
+ focusIndicator: c[colors.focusIndicator],
+ };
+};
+
+/**
+ * Clamp a requested expanded width to the spec range (220–360dp).
+ */
+export const clampExpandedWidth = (width: number): number =>
+ Math.min(Math.max(width, rail.expandedMinWidth), rail.expandedMaxWidth);
+
+/**
+ * Rail motion as a CSS transition. Defaults to the 300ms emphasized curve used
+ * for expanding; the modal rail passes MD3 enter/exit durations and easings.
+ */
+export const getTransition = (
+ theme: InternalTheme,
+ properties: CSSTransitionProperties['transitionProperty'],
+ {
+ duration = theme.motion.duration.medium2,
+ easing = theme.motion.easing.emphasized,
+ instant = false,
+ }: { duration?: number; easing?: EasingConfig; instant?: boolean } = {}
+): CSSTransitionProperties => ({
+ transitionProperty: properties,
+ transitionDuration: instant ? 0 : duration,
+ transitionTimingFunction: cubicBezier(...easing),
+});
diff --git a/src/components/__tests__/NavigationRail/NavigationRail.test.tsx b/src/components/__tests__/NavigationRail/NavigationRail.test.tsx
new file mode 100644
index 0000000000..355ae01b27
--- /dev/null
+++ b/src/components/__tests__/NavigationRail/NavigationRail.test.tsx
@@ -0,0 +1,214 @@
+import { Text } from 'react-native';
+
+import { describe, expect, it, jest } from '@jest/globals';
+
+import { render, screen, userEvent } from '../../../test-utils';
+import NavigationRail from '../../NavigationRail';
+
+const items = (
+ <>
+
+
+
+ >
+);
+
+describe('NavigationRail render', () => {
+ it('renders collapsed', async () => {
+ expect(
+ (await render({items})).toJSON()
+ ).toMatchSnapshot();
+ });
+
+ it('renders expanded', async () => {
+ expect(
+ (await render({items})).toJSON()
+ ).toMatchSnapshot();
+ });
+
+ it('renders header and alignment', async () => {
+ expect(
+ (
+ await render(
+ Header}>
+ {items}
+
+ )
+ ).toJSON()
+ ).toMatchSnapshot();
+ });
+
+ it('renders icon-only items', async () => {
+ expect(
+ (
+ await render(
+
+
+
+ )
+ ).toJSON()
+ ).toMatchSnapshot();
+ });
+});
+
+describe('NavigationRail layout', () => {
+ it('uses collapsed width by default', async () => {
+ await render({items});
+
+ expect(screen.getByTestId('navigation-rail')).toHaveStyle({
+ width: 96,
+ });
+ });
+
+ it('keeps the collapsed footprint and fades the scrim in overlay mode', async () => {
+ const onDismiss = jest.fn();
+ const { rerender } = await render(
+
+ {items}
+
+ );
+
+ expect(screen.getByTestId('navigation-rail-scrim')).toHaveStyle({
+ opacity: 0,
+ });
+
+ await rerender(
+
+ {items}
+
+ );
+
+ expect(screen.getByTestId('navigation-rail')).toHaveStyle({ width: 220 });
+ expect(screen.getByTestId('navigation-rail').parent).toHaveStyle({
+ width: 96,
+ });
+ expect(screen.getByTestId('navigation-rail-scrim')).toHaveStyle({
+ opacity: 0.32,
+ });
+
+ await userEvent.setup().press(screen.getByTestId('navigation-rail-scrim'));
+
+ expect(onDismiss).toHaveBeenCalledTimes(1);
+ });
+
+ it('clamps the expanded width to the spec range', async () => {
+ await render(
+
+ {items}
+
+ );
+
+ expect(screen.getByTestId('navigation-rail')).toHaveStyle({
+ width: 360,
+ });
+ });
+});
+
+describe('NavigationRail.Item', () => {
+ it('shows the active icon only when active', async () => {
+ await render(
+
+
+
+
+ );
+
+ expect(screen.getByTestId('active')).toBeSelected();
+ expect(screen.getByTestId('inactive')).not.toBeSelected();
+ expect(screen.getByTestId('active-indicator')).toHaveStyle({
+ opacity: 1,
+ });
+ expect(screen.getByTestId('inactive-indicator')).toHaveStyle({
+ opacity: 0,
+ });
+ });
+
+ it('shows the stacked label collapsed and the row label expanded', async () => {
+ const { rerender } = await render(
+
+
+
+ );
+
+ expect(screen.getByTestId('navigation-rail-item-label')).toBeOnTheScreen();
+ expect(
+ screen.queryByTestId('navigation-rail-item-label-expanded')
+ ).toBeNull();
+
+ await rerender(
+
+
+
+ );
+
+ expect(screen.queryByTestId('navigation-rail-item-label')).toBeNull();
+ expect(
+ screen.getByTestId('navigation-rail-item-label-expanded')
+ ).toBeOnTheScreen();
+ });
+
+ it('uses the label as accessibility label', async () => {
+ await render(
+
+
+
+ );
+
+ expect(screen.getByRole('tab', { name: 'Inbox' })).toBeOnTheScreen();
+ });
+
+ it('calls onPress', async () => {
+ const user = userEvent.setup();
+ const onPress = jest.fn();
+ await render(
+
+
+
+ );
+
+ await user.press(screen.getByRole('tab'));
+
+ expect(onPress).toHaveBeenCalledTimes(1);
+ });
+
+ it('does not call onPress when disabled', async () => {
+ const user = userEvent.setup();
+ const onPress = jest.fn();
+ await render(
+
+
+
+ );
+
+ await user.press(screen.getByRole('tab'));
+
+ expect(onPress).not.toHaveBeenCalled();
+ expect(screen.getByRole('tab')).toBeDisabled();
+ });
+
+ it('renders badge text', async () => {
+ await render(
+
+
+
+ );
+
+ expect(screen.getByText('12')).toBeOnTheScreen();
+ });
+});
diff --git a/src/components/__tests__/NavigationRail/NavigationRailModal.test.tsx b/src/components/__tests__/NavigationRail/NavigationRailModal.test.tsx
new file mode 100644
index 0000000000..c341253112
--- /dev/null
+++ b/src/components/__tests__/NavigationRail/NavigationRailModal.test.tsx
@@ -0,0 +1,100 @@
+import { afterEach, describe, expect, it, jest } from '@jest/globals';
+
+import { act, render, screen, userEvent } from '../../../test-utils';
+import NavigationRail from '../../NavigationRail';
+import Portal from '../../Portal/Portal';
+
+const renderModal = (visible: boolean, onDismiss = jest.fn()) =>
+ render(
+
+
+
+
+
+
+ );
+
+describe('NavigationRail.Modal', () => {
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ it('renders when visible', async () => {
+ expect((await renderModal(true)).toJSON()).toMatchSnapshot();
+ });
+
+ it('renders nothing when hidden', async () => {
+ await renderModal(false);
+
+ expect(screen.queryByTestId('navigation-rail-modal')).toBeNull();
+ });
+
+ it('slides out before unmounting on dismiss', async () => {
+ jest.useFakeTimers();
+ const { rerender } = await renderModal(true);
+
+ await rerender(
+
+
+
+
+
+ );
+
+ expect(screen.getByTestId('navigation-rail-modal-surface')).toHaveStyle({
+ transform: [{ translateX: -220 }],
+ });
+ expect(screen.getByTestId('navigation-rail-modal-backdrop')).toHaveStyle({
+ opacity: 0,
+ });
+
+ await act(() => {
+ jest.advanceTimersByTime(200);
+ });
+
+ expect(screen.queryByTestId('navigation-rail-modal')).toBeNull();
+ });
+
+ it('unmounts immediately on dismiss when not animated', async () => {
+ jest.useFakeTimers();
+ const { rerender } = await render(
+
+
+
+
+
+ );
+
+ await rerender(
+
+
+
+
+
+ );
+ await act(() => {
+ jest.advanceTimersByTime(0);
+ });
+
+ expect(screen.queryByTestId('navigation-rail-modal')).toBeNull();
+ });
+
+ it('renders items in the expanded layout', async () => {
+ await renderModal(true);
+
+ expect(screen.getByTestId('navigation-rail-modal-rail')).toHaveStyle({
+ width: 220,
+ });
+ expect(screen.getAllByRole('tab')).toHaveLength(2);
+ });
+
+ it('dismisses on scrim press', async () => {
+ const user = userEvent.setup();
+ const onDismiss = jest.fn();
+ await renderModal(true, onDismiss);
+
+ await user.press(screen.getByTestId('navigation-rail-modal-backdrop'));
+
+ expect(onDismiss).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/components/__tests__/NavigationRail/__snapshots__/NavigationRail.test.tsx.snap b/src/components/__tests__/NavigationRail/__snapshots__/NavigationRail.test.tsx.snap
new file mode 100644
index 0000000000..a4fd6fd70a
--- /dev/null
+++ b/src/components/__tests__/NavigationRail/__snapshots__/NavigationRail.test.tsx.snap
@@ -0,0 +1,3460 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`NavigationRail render renders collapsed 1`] = `
+
+
+
+
+
+
+
+
+
+
+ inbox
+
+
+
+ 3
+
+
+
+
+
+ Inbox
+
+
+
+
+ Inbox
+
+
+
+
+ 3
+
+
+
+
+
+
+
+
+
+
+
+
+ send
+
+
+
+
+
+
+
+ Sent
+
+
+
+
+ Sent
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ delete
+
+
+
+
+ Trash
+
+
+
+
+ Trash
+
+
+
+
+
+
+
+
+`;
+
+exports[`NavigationRail render renders expanded 1`] = `
+
+
+
+
+
+
+
+
+
+
+ inbox
+
+
+
+ 3
+
+
+
+
+
+ Inbox
+
+
+
+
+ Inbox
+
+
+
+
+ 3
+
+
+
+
+
+
+
+
+
+
+
+
+ send
+
+
+
+
+
+
+
+ Sent
+
+
+
+
+ Sent
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ delete
+
+
+
+
+ Trash
+
+
+
+
+ Trash
+
+
+
+
+
+
+
+
+`;
+
+exports[`NavigationRail render renders header and alignment 1`] = `
+
+
+
+ Header
+
+
+
+
+
+
+
+
+
+
+
+ inbox
+
+
+
+ 3
+
+
+
+
+
+ Inbox
+
+
+
+
+ Inbox
+
+
+
+
+ 3
+
+
+
+
+
+
+
+
+
+
+
+
+ send
+
+
+
+
+
+
+
+ Sent
+
+
+
+
+ Sent
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ delete
+
+
+
+
+ Trash
+
+
+
+
+ Trash
+
+
+
+
+
+
+
+
+`;
+
+exports[`NavigationRail render renders icon-only items 1`] = `
+
+
+
+
+
+
+
+
+
+
+ inbox
+
+
+
+
+
+
+
+
+`;
diff --git a/src/components/__tests__/NavigationRail/__snapshots__/NavigationRailModal.test.tsx.snap b/src/components/__tests__/NavigationRail/__snapshots__/NavigationRailModal.test.tsx.snap
new file mode 100644
index 0000000000..87aa2cb1e7
--- /dev/null
+++ b/src/components/__tests__/NavigationRail/__snapshots__/NavigationRailModal.test.tsx.snap
@@ -0,0 +1,779 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`NavigationRail.Modal renders when visible 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ inbox
+
+
+
+
+ Inbox
+
+
+
+
+ Inbox
+
+
+
+
+
+
+
+
+
+
+
+
+ send
+
+
+
+
+ Sent
+
+
+
+
+ Sent
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/components/__tests__/NavigationRail/utils.test.tsx b/src/components/__tests__/NavigationRail/utils.test.tsx
new file mode 100644
index 0000000000..14fd1cf5fa
--- /dev/null
+++ b/src/components/__tests__/NavigationRail/utils.test.tsx
@@ -0,0 +1,35 @@
+import { describe, expect, it } from '@jest/globals';
+
+import { getTheme } from '../../../core/theming';
+import {
+ clampExpandedWidth,
+ resolveItemColors,
+} from '../../NavigationRail/utils';
+
+describe('resolveItemColors', () => {
+ const theme = getTheme();
+
+ it('returns inactive colors by default', () => {
+ expect(resolveItemColors({ theme })).toMatchObject({
+ icon: theme.colors.onSurfaceVariant,
+ label: theme.colors.onSurfaceVariant,
+ indicator: theme.colors.secondaryContainer,
+ });
+ });
+
+ it('uses secondary for the active collapsed label and the icon color when expanded', () => {
+ expect(resolveItemColors({ theme, active: true })).toMatchObject({
+ icon: theme.colors.onSecondaryContainer,
+ label: theme.colors.secondary,
+ expandedLabel: theme.colors.onSecondaryContainer,
+ });
+ });
+});
+
+describe('clampExpandedWidth', () => {
+ it('keeps widths inside the spec range', () => {
+ expect(clampExpandedWidth(100)).toBe(220);
+ expect(clampExpandedWidth(300)).toBe(300);
+ expect(clampExpandedWidth(1000)).toBe(360);
+ });
+});
diff --git a/src/index.tsx b/src/index.tsx
index f46d8e22d8..048bef5a02 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -37,6 +37,7 @@ export { default as Icon } from './components/Icon';
export { default as IconButton } from './components/IconButton/IconButton';
export { default as Menu } from './components/Menu/Menu';
export { default as Modal } from './components/Modal';
+export { default as NavigationRail } from './components/NavigationRail';
export { default as Portal } from './components/Portal/Portal';
export { default as ProgressBar } from './components/ProgressBar';
export { default as RadioButton } from './components/RadioButton';
@@ -115,6 +116,9 @@ export type { Props as ListSubheaderProps } from './components/List/ListSubheade
export type { Props as MenuProps } from './components/Menu/Menu';
export type { Props as MenuItemProps } from './components/Menu/MenuItem';
export type { Props as ModalProps } from './components/Modal';
+export type { Props as NavigationRailProps } from './components/NavigationRail/NavigationRail';
+export type { Props as NavigationRailItemProps } from './components/NavigationRail/NavigationRailItem';
+export type { Props as NavigationRailModalProps } from './components/NavigationRail/NavigationRailModal';
export type { Props as PortalProps } from './components/Portal/Portal';
export type { Props as PortalHostProps } from './components/Portal/PortalHost';
export type { Props as ProgressBarProps } from './components/ProgressBar';