Skip to main content
Version: Unreleased

React Navigation

TrueSheet integrates with React Navigation out of the box. It just works!

navigation

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-navigation

Basic 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>
);
}
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:

OptionTypeDescription
detentIndexnumberThe detent index to present at. Defaults to 0.
reanimatedbooleanEnable worklet-based position events for this screen.
positionChangeHandlerfunctionA 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);
},
}}
/>
note

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.

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>;
}
tip

All TrueSheet props like header, footer, grabber, dismissible, etc. can be dynamically updated via setOptions.

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>;
}
EventDescription
sheetWillPresentSheet is about to present
sheetDidPresentSheet finished presenting
sheetWillDismissSheet is about to dismiss
sheetDidDismissSheet finished dismissing
sheetDetentChangeDetent changed
sheetDragBeginUser started dragging
sheetDragChangeUser is dragging
sheetDragEndUser stopped dragging
sheetPositionChangePosition 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-navigation
app/
├── _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';

See Expo Router docs for more information.

Navigate directly from sheets - they remain visible when presenting modals on top.

// Navigate directly - no need to dismiss first!
navigation.navigate('SomeScreen')
note

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