Skip to main content

Introducing Sheet Navigator

ยท 4 min read
Jovanni Lo
Lead Mobile Developer

I'm excited to introduce Sheet Navigator โ€” a custom React Navigation navigator that makes presenting sheets as natural as navigating between screens.

Sheet Navigator

The Problemโ€‹

Bottom sheets are a staple of modern mobile apps. But integrating them with React Navigation has always been... awkward. You'd typically have to:

  1. Manage sheet refs manually
  2. Call present() and dismiss() imperatively
  3. Sync sheet state with navigation state yourself
  4. Handle the back button separately
  5. Deal with focus/blur when modals appear on top

It works, but it's not elegant. I wanted sheets to feel like first-class citizens in React Navigation.

The Solution: Sheet Navigatorโ€‹

Sheet Navigator treats sheets as navigation destinations. The first screen is your base content, and every other screen becomes a sheet:

import { NavigationContainer } from '@react-navigation/native';
import { createTrueSheetNavigator } from '@lodev09/react-native-true-sheet/navigation';

const Sheet = createTrueSheetNavigator();

function App() {
return (
<NavigationContainer>
<Sheet.Navigator>
<Sheet.Screen name="Home" component={HomeScreen} />
<Sheet.Screen
name="Details"
component={DetailsSheet}
options={{ detents: ['auto', 1], cornerRadius: 16 }}
/>
</Sheet.Navigator>
</NavigationContainer>
);
}

Now you can navigate to sheets like any other screen:

navigation.navigate('Details', { itemId: 123 });

And dismiss them naturally:

navigation.goBack();

Featuresโ€‹

Full Screen Options Supportโ€‹

All TrueSheet props work as screen options. Configure each sheet declaratively:

<Sheet.Screen
name="Settings"
component={SettingsSheet}
options={{
detents: [0.5, 1],
cornerRadius: 20,
grabber: true,
dimmedDetentIndex: 1,
backgroundColor: '#1a1a1a',
}}
/>

Programmatic Resizingโ€‹

Use the useTrueSheetNavigation hook to resize sheets from within:

import { useTrueSheetNavigation } from '@lodev09/react-native-true-sheet/navigation';

function DetailsSheet() {
const navigation = useTrueSheetNavigation();

return (
<View>
<Button title="Expand" onPress={() => navigation.resize(1)} />
<Button title="Collapse" onPress={() => navigation.resize(0)} />
</View>
);
}

Rich Event Systemโ€‹

Listen to sheet lifecycle events at the navigator level:

<Sheet.Navigator
screenListeners={{
sheetWillPresent: (e) => console.log('Presenting at index:', e.data.index),
sheetDidDismiss: () => console.log('Sheet dismissed'),
sheetDetentChange: (e) => console.log('Detent changed to:', e.data.index),
sheetPositionChange: (e) => console.log('Position:', e.data.position),
}}
>

All 14 events from TrueSheet are available:

LifecycleDragFocus
sheetWillPresentsheetDragBeginsheetWillFocus
sheetDidPresentsheetDragChangesheetDidFocus
sheetWillDismisssheetDragEndsheetWillBlur
sheetDidDismisssheetPositionChangesheetDidBlur
sheetDetentChange

Wrap Your Existing Navigationโ€‹

Already have a complex navigation setup? Wrap it with Sheet 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="QuickActions" component={QuickActionsSheet} />
<Sheet.Screen name="Share" component={ShareSheet} />
</Sheet.Navigator>
</NavigationContainer>
);
}

Now any screen in your app can present sheets:

// From anywhere in RootStack
navigation.navigate('QuickActions');

Expo Router Supportโ€‹

Using Expo Router? Sheet Navigator works with withLayoutContext:

// app/_layout.tsx
import { withLayoutContext } from 'expo-router';
import { createTrueSheetNavigator } from '@lodev09/react-native-true-sheet/navigation';

const { Navigator } = createTrueSheetNavigator();
export default withLayoutContext(Navigator);
// app/details.tsx
export const unstable_settings = {
options: { detents: ['auto', 1], cornerRadius: 16 },
};

export default function DetailsSheet() {
// Sheet content
}

Under the Hoodโ€‹

Sheet Navigator is built on a custom router that extends React Navigation's StackRouter. Here's what makes it work:

Smart Dismiss Handling โ€” When you call goBack(), the sheet animates out smoothly before the route is removed. No janky state transitions.

Resize State Management โ€” The router tracks resize operations with resizeIndex and resizeKey, ensuring smooth transitions between detents.

Focus/Blur Tracking โ€” When modals are presented on top of sheets, focus events are emitted so you can pause/resume operations accordingly.

Getting Startedโ€‹

Sheet Navigator is included in @lodev09/react-native-true-sheet v3.1.0+. Just import from the navigation subpath:

import {
createTrueSheetNavigator,
useTrueSheetNavigation,
} from '@lodev09/react-native-true-sheet/navigation';

Check out the full documentation for more examples and API details.

Running into issues? See the Troubleshooting guide for common fixes.

That's Allโ€‹

Have feedback or feature requests? Open an issue โ€” I'd love to hear from you!