Navigation
Navigate to other screens from within the bottom sheet.
TrueSheet integrates with React Navigation out of the box. It just works!
How?
You can use the Sheet Navigator to present screens as sheets (also works with Expo Router), or simply navigate from within sheets using your existing navigation setup.
Sheet Navigator
TrueSheet provides a custom navigator built on standard-navigation, so the same implementation works with both React Navigation and Expo Router. The first screen (or initialRouteName) is the base content, while other screens are presented as sheets.
Install @react-navigation/native (version 7.3.0 or higher) and standard-navigation. Both are declared as optional peer dependencies.
npm install @react-navigation/native standard-navigationBasic Usage
import { NavigationContainer } from '@react-navigation/native';
import {
createTrueSheetNavigator,
useTrueSheetNavigation,
} from '@lodev09/react-native-true-sheet/navigation';
const Sheet = createTrueSheetNavigator();
function App() {
return (
<NavigationContainer>
<Sheet.Navigator>
{/* Base screen (first screen is the default) */}
<Sheet.Screen name="Main" component={MainScreen} />
{/* Sheet screens */}
<Sheet.Screen
name="Details"
component={DetailsSheet}
options={{ detents: ['auto', 1], cornerRadius: 16 }}
/>
</Sheet.Navigator>
</NavigationContainer>
);
}Static API
The navigator also supports React Navigation's static API via createTrueSheetScreen:
import {
createTrueSheetNavigator,
createTrueSheetScreen,
} from '@lodev09/react-native-true-sheet/navigation';
const Sheet = createTrueSheetNavigator({
screens: {
Main: MainScreen,
Details: createTrueSheetScreen({
screen: DetailsSheet,
options: { detents: ['auto', 1], cornerRadius: 16 },
}),
},
});Wrapping Existing Navigation
Wrap your root navigator to present sheets from anywhere:
const Stack = createNativeStackNavigator();
const Sheet = createTrueSheetNavigator();
function RootStack() {
return (
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Navigator>
);
}
function App() {
return (
<NavigationContainer>
<Sheet.Navigator>
<Sheet.Screen name="Root" component={RootStack} />
<Sheet.Screen
name="Details"
component={DetailsSheet}
options={{ detents: ['auto', 1], cornerRadius: 16 }}
/>
</Sheet.Navigator>
</NavigationContainer>
);
}Nesting Navigators in Sheets
Native stacks are not supported inside a sheet screen. On Android, react-native-screens looks for the app's root view above its containers, and the sheet renders outside of it. Presenting a sheet screen whose component is a createNativeStackNavigator crashes with:
IllegalStateException: ScreenContainer is not attached under ReactRootViewOnly the base screen (the first screen, or initialRouteName) can host a native stack, as shown in Wrapping Existing Navigation.
For multi-step flows inside a sheet, use one of these instead:
-
Stack sheets. Make each step its own sheet screen and navigate between them. The presenting sheet stays visible underneath, and
navigation.pop(),popTo(), andpopToTop()walk back through the stack. This is the recommended approach.function SettingsSheet() { const navigation = useTrueSheetNavigation(); return ( <View> <Button title="Profile" onPress={() => navigation.navigate('Profile')} /> <Button title="Back" onPress={() => navigation.pop()} /> </View> ); }With Expo Router, keep each step as a sibling route under the
Sheetlayout instead of giving the flow its own_layout.tsxwith a<Stack>:app/ ├── _layout.tsx # Sheet layout ├── index.tsx # Base content ├── settings.tsx # Sheet screen └── profile.tsx # Sheet screen, presented from settings// app/_layout.tsx export default function SheetLayout() { return ( <Sheet> <Sheet.Screen name="index" /> <Sheet.Screen name="settings" options={{ detents: ['auto', 1] }} /> <Sheet.Screen name="profile" options={{ detents: ['auto', 1] }} /> </Sheet> ); } // app/settings.tsx export default function SettingsSheet() { const router = useRouter(); return ( <View> <Button title="Profile" onPress={() => router.push('/profile')} /> <Button title="Back" onPress={() => router.back()} /> </View> ); } -
Local state. Swap the sheet's content with plain React state when you don't need navigation history or deep links.
Navigation & Resizing
function DetailsSheet() {
const navigation = useTrueSheetNavigation();
return (
<View>
<Button title="Expand" onPress={() => navigation.resize(1)} />
<Button title="Close" onPress={() => navigation.goBack()} />
</View>
);
}Screen Options
All TrueSheet props are available as screen options, plus the following navigation-specific options:
| Option | Type | Description |
|---|---|---|
detentIndex | number | The detent index to present at. Defaults to 0. |
reanimated | boolean | Enable worklet-based position events for this screen. |
positionChangeHandler | function | A callback that receives position change events. When reanimated is enabled, this must be a worklet function. |
Reanimated Integration
Enable worklet-based position events for smooth UI thread animations:
// In your navigator
<Sheet.Screen
name="Details"
component={DetailsSheet}
options={{
reanimated: true,
positionChangeHandler: (payload) => {
'worklet';
// Access payload.position, payload.detentIndex, etc.
console.log(payload.position);
},
}}
/>When reanimated: true is set, react-native-reanimated must be installed and positionChangeHandler must be a worklet function. The integration is lazy-loaded, so screens without reanimated: true don't require reanimated.
Dynamic Header & Footer
Use navigation.setOptions() to set or update header and footer from within a sheet screen. This is useful when you need access to navigation state or sheet events.
function DetailsSheet() {
const navigation = useTrueSheetNavigation();
const [detentIndex, setDetentIndex] = useState(0);
useEffect(() => {
const unsubscribe = navigation.addListener('sheetDetentChange', (e) => {
setDetentIndex(e.data.index);
});
return unsubscribe;
}, [navigation]);
useEffect(() => {
navigation.setOptions({
footer: (
<View style={{ padding: 16 }}>
{detentIndex > 0 && <Button title="Collapse" onPress={() => navigation.resize(0)} />}
<Button title="Close" onPress={() => navigation.goBack()} />
</View>
),
});
}, [navigation, detentIndex]);
return <View>{/* ... */}</View>;
}All TrueSheet props like header, footer, grabber, dismissible, etc. can be dynamically updated via setOptions.
Scrollable Content
scrollableRef and scrollableOptions are screen options like any other prop. Since the ref is created inside the screen component, set it with setOptions:
function DetailsSheet() {
const navigation = useTrueSheetNavigation();
const scrollableRef = useRef<ScrollView>(null);
useEffect(() => {
navigation.setOptions({ scrollableRef });
}, [navigation]);
return (
<ScrollView ref={scrollableRef}>
{/* ... */}
</ScrollView>
);
}Bound the scroll view's height with flex: 1 via the screen's style option.
<Sheet.Screen
name="Details"
component={DetailsSheet}
options={{ detents: [0.5, 1], style: { flex: 1 } }}
/>See the Scrolling guide for more information.
Screen Listeners
Use screenListeners on the navigator or listeners on individual screens:
<Sheet.Navigator
screenListeners={{
sheetDidPresent: (e) => console.log('Presented:', e.data.index),
sheetDidDismiss: () => console.log('Dismissed'),
}}
>Or use addListener within a screen component:
function DetailsSheet() {
const navigation = useTrueSheetNavigation();
useEffect(() => {
const unsubscribe = navigation.addListener('sheetDidPresent', (e) => {
console.log('Presented:', e.data.index);
});
return unsubscribe;
}, [navigation]);
return <View>{/* ... */}</View>;
}| Event | Description |
|---|---|
sheetWillPresent | Sheet is about to present |
sheetDidPresent | Sheet finished presenting |
sheetWillDismiss | Sheet is about to dismiss |
sheetDidDismiss | Sheet finished dismissing |
sheetDismissAttempt | User tried to dismiss a non-dismissible sheet |
sheetDetentChange | Detent changed |
sheetDragBegin | User started dragging |
sheetDragChange | User is dragging |
sheetDragEnd | User stopped dragging |
sheetPositionChange | Position changed |
See Lifecycle Events for more details.
Expo Router
TrueSheet ships a ready-to-use Sheet layout for Expo Router via the /navigation/expo-router entry point. It integrates with Expo Router's built-in navigation directly — no @react-navigation/* install needed. All navigator features above (screen options, reanimated, dynamic header/footer, listeners) apply here too.
Requires Expo SDK 57+ (expo-router version 57.0.0 or higher). Install standard-navigation, declared as an optional peer dependency:
npm install standard-navigationapp/
├── _layout.tsx # TrueSheet navigator
├── index.tsx # Base content
└── details.tsx # Sheet screen// app/_layout.tsx
import { Sheet } from '@lodev09/react-native-true-sheet/navigation/expo-router';
export default function SheetLayout() {
return (
<Sheet>
<Sheet.Screen name="index" />
<Sheet.Screen
name="details"
options={{
detents: ['auto', 1],
cornerRadius: 16,
}}
/>
</Sheet>
);
}Inside sheet screens, import useTrueSheetNavigation from the same entry point:
import { useTrueSheetNavigation } from '@lodev09/react-native-true-sheet/navigation/expo-router';A sheet route must not have its own _layout.tsx with a <Stack>. Keep multi-step flows as sibling sheet screens under the Sheet layout instead. See Nesting Navigators in Sheets.
See Expo Router docs for more information.
Navigating from Sheets
Navigate directly from sheets - they remain visible when presenting modals on top.
// Navigate directly - no need to dismiss first!
navigation.navigate('SomeScreen')Requires a patch to react-native-screens. See PR #3415.
On Expo SDK 56+, this patch is silently dropped on EAS builds because react-native-screens ships precompiled. See Patched react-native-screens Not Applied on EAS to force it to build from source.
Web Limitation
On native platforms, TrueSheet automatically detects react-native-screens and handles sheet visibility when navigating. However, this detection is not supported on web.
As a workaround, use useFocusEffect to manually present/dismiss the sheet when the screen gains or loses focus:
import { useFocusEffect } from '@react-navigation/native';
function DetailsSheet() {
const sheet = useRef<TrueSheet>(null);
useFocusEffect(
useCallback(() => {
sheet.current?.present();
return () => {
sheet.current?.dismiss();
};
}, [])
);
return <TrueSheet ref={sheet}>{/* ... */}</TrueSheet>;
}