Creating a bottom sheet in React Native: three practical ways in Expo
Bottom sheets look simple until you need one that actually feels polished.
Open from the bottom, snap nicely, support gestures, work with scroll views, look right on iOS and Android, and maybe match a heavily branded design. That is where the easy version stops being enough.
If you are using React Native with Expo today, I see three realistic ways to build them:
- Use a route in Expo Router and present it as a sheet-like screen.
- Use a dedicated library like
@gorhom/bottom-sheet. - Use native sheets through Expo UI with SwiftUI and Jetpack Compose.
Each one solves a different level of the problem.
Why this topic matters #
Bottom sheets are one of those components that can start as “just a modal” and very quickly become a real UI architecture decision.
The moment you need:
- multiple snap points
- proper gesture handling
- keyboard behavior
- custom headers and backgrounds
- branded visual details
- consistent behavior across iOS and Android
you are no longer choosing only a component. You are choosing how much control you want over presentation, animation, and native behavior.
Option 1: Expo Router route presented as a sheet #
This is the easiest place to start.
Expo Router lets you present a screen using modal styles, including
presentation: "formSheet". In practice, this is the most natural solution if
your bottom sheet is really a screen in your app flow.
Simple example:
import { Stack } from "expo-router";
export default function Layout() {
return (
<Stack>
<Stack.Screen name="index" />
<Stack.Screen
name="payment-sheet"
options={{
presentation: "formSheet",
sheetAllowedDetents: [0.4, 0.9],
sheetCornerRadius: 24,
}}
/>
</Stack>
);
}
And then the route is just a normal screen:
import { View, Text } from "react-native";
export default function PaymentSheetScreen() {
return (
<View style={{ flex: 1, padding: 24 }}>
<Text>Payment details</Text>
</View>
);
}
Why this approach is attractive #
- very little setup
- native navigation integration
- easier mental model because the sheet is just another route
- good fit when the sheet is actually part of navigation
For many apps, this is enough.
If all you need is a clean sheet presentation with a couple of detents, this is probably the fastest way to ship.
Where it starts to hurt #
The problem appears when the sheet stops being “a screen from the bottom” and starts being “a custom interactive component”.
That is where you hit the ceiling of the presentation system.
For example, imagine you want a sheet like some of the ones in Revolut, with very specific top-border decoration, custom layered backgrounds, or a highly designed chrome around the sheet.
At that point, the route approach can become tricky.
On iOS, some parts may feel easier because the native sheet presentation is more aligned with that mental model. On Android, getting the same visual result can take more work, and sometimes you end up fighting the modal container instead of building the UI itself.
Also, once you want the sheet to behave like a reusable component instead of a navigation destination, using routes starts to feel less natural.
Option 2: A library like @gorhom/bottom-sheet #
This is the most straightforward option when you want a “real” bottom sheet component.
@gorhom/bottom-sheet has been the default choice for a lot of React Native
apps for a reason. It gives you the behavior people usually expect from a modern
bottom sheet:
- gesture-driven interactions
- snapping between multiple heights
- modal and non-modal patterns
- keyboard handling
- scrollable content support
- more control over background, handles, and composition
Simple example:
import { useMemo, useRef } from "react";
import { Text, View } from "react-native";
import BottomSheet from "@gorhom/bottom-sheet";
export default function AccountSheet() {
const ref = useRef<BottomSheet>(null);
const snapPoints = useMemo(() => ["25%", "50%", "90%"], []);
return (
<BottomSheet ref={ref} index={1} snapPoints={snapPoints}>
<View style={{ flex: 1, padding: 24 }}>
<Text>Account actions</Text>
</View>
</BottomSheet>
);
}
Why many teams still pick this one #
This option gives you the best balance between control and practicality.
It is easier to shape into a custom product component than a route-based sheet, and it is still much simpler than going fully native with separate SwiftUI and Jetpack Compose thinking.
If you need a reusable sheet system across your app, this tends to fit much better than the route approach.
It is also the most flexible path if your design team wants the sheet to become its own interaction surface rather than just a navigation presentation style.
What you pay for #
You take on more implementation responsibility.
This means owning:
- Reanimated integration
- gesture Handler integration
- library version compatibility
- edge cases around nested scrolls and gestures
That is not necessarily bad. It is just the price of having more control.
Still, in practice, this is the option many teams end up using because it solves the real product needs without forcing them into a fully native UI layer.
Option 3: Native sheets with Expo UI #
The newest route is using Expo UI, which lets you render native UI pieces from React using SwiftUI on iOS and Jetpack Compose on Android.
This is interesting because bottom sheets are one of those components where “native” can really matter.
On iOS, Expo UI exposes a SwiftUI BottomSheet.
On Android, Expo UI exposes a Jetpack Compose ModalBottomSheet.
Very small example on iOS:
import { useState } from "react";
import { Host, BottomSheet, Button, Text, VStack } from "@expo/ui/swift-ui";
export default function NativeSheetIOS() {
const [open, setOpen] = useState(false);
return (
<Host style={{ flex: 1 }}>
<VStack>
<Button label="Open" onPress={() => setOpen(true)} />
<BottomSheet isPresented={open} onIsPresentedChange={setOpen}>
<Text>Native iOS sheet</Text>
</BottomSheet>
</VStack>
</Host>
);
}
And on Android:
import { useState } from "react";
import {
Host,
ModalBottomSheet,
Button,
Column,
Text,
} from "@expo/ui/jetpack-compose";
export default function NativeSheetAndroid() {
const [visible, setVisible] = useState(false);
return (
<Host style={{ flex: 1 }}>
<Button onPress={() => setVisible(true)}>Open</Button>
{visible && (
<ModalBottomSheet onDismissRequest={() => setVisible(false)}>
<Column>
<Text>Native Android sheet</Text>
</Column>
</ModalBottomSheet>
)}
</Host>
);
}
Why this is interesting #
- truly native sheet APIs
- closer to platform behavior by default
- strong option if you already want to mix React Native with native UI surfaces
- useful when platform fidelity matters more than having a single abstraction
This is the approach that feels the most “native-first”.
But it does not remove all the hard parts #
This is important.
Using native sheets through Expo UI does not automatically mean unlimited visual freedom.
You are still depending on how SwiftUI and Jetpack Compose present modal sheets. That means if your design goes far beyond normal platform expectations, you can run into some of the same issues as the route-based approach, just at a different layer.
If the goal is a very custom branded sheet effect, like a fancy bordered top edge, a patterned frame, or a very specific container treatment, native sheet presentation can still feel restrictive.
So this is not automatically “the most customizable”. It is often “the most native”.
That is a different advantage.
A practical way to think about the trade-offs #
Here is the mental model I would use:
Route-based sheet #
Best when the sheet is basically a screen.
Good for:
- simple flows
- quick setup
- navigation-driven sheets
- teams that want the least moving parts
Watch out for:
- visual customization ceilings
- reusable component patterns
- highly branded sheet surfaces
Library-based sheet #
Best when the sheet is a product component.
Good for:
- reusable sheet systems
- richer gesture interactions
- custom design requirements
- apps that want one flexible solution across many screens
Watch out for:
- dependency maintenance
- gesture and animation integration
- implementation complexity
Native sheet with Expo UI #
Best when platform-native behavior is the priority.
Good for:
- apps leaning into native UI fidelity
- teams already exploring Expo UI
- platform-specific composition using SwiftUI and Compose
Watch out for:
- framework maturity and evolution
- platform-specific thinking
- the fact that native modal containers still define some limits
So which one should you pick? #
If you just need a bottom sheet and want to move fast, the Expo Router route approach is the easiest starting point.
If you want the most straightforward balance of flexibility and real-world
control, a library like @gorhom/bottom-sheet is usually the safest option.
If you are already leaning into Expo UI and want platform-native sheet behavior, the SwiftUI and Jetpack Compose route is the most interesting one to watch.
The key is understanding what problem you are actually solving.
Sometimes you need a screen presented from the bottom. Sometimes you need a fully interactive reusable sheet system. Sometimes you need the most native feel possible.
Those are not the same requirement, even if all three get called “bottom sheet”.
Links #
- Expo Router modals and form sheets: https://docs.expo.dev/router/advanced/modals/
- Expo Router web modals: https://docs.expo.dev/router/advanced/web-modals/
@gorhom/bottom-sheetdocs: https://gorhom.dev/react-native-bottom-sheet/@gorhom/bottom-sheetmodal docs: https://gorhom.dev/react-native-bottom-sheet/modal- Expo UI: https://docs.expo.dev/versions/latest/sdk/ui/
- Expo UI SwiftUI BottomSheet: https://docs.expo.dev/versions/latest/sdk/ui/swift-ui/bottomsheet/
- Expo UI Jetpack Compose ModalBottomSheet: https://docs.expo.dev/versions/latest/sdk/ui/jetpack-compose/bottomsheet/
Conclusions #
There is no single “correct” way to build a bottom sheet in React Native today.
There is the simple route-based option, the very practical library option, and the more native-first Expo UI option.
The right choice depends on what your sheet really is.
If it is basically navigation, use navigation. If it is a reusable interaction surface, use a bottom-sheet library. If native presentation fidelity is the goal, Expo UI is becoming a serious option.