How to make a bottom sheet in React Native
By the LumioUI team · Updated
A bottom sheet is a panel that slides up from the bottom of the screen over a dimmed backdrop. To build one in React Native, move it with a Reanimated shared value (translateY). A gesture-handler pan lets people drag it down, and a flick or a long drag closes it. The quickest route is the LumioUI Action Sheet, shown below, or the Pro Bottom Sheet when you need snap heights.
Live demo of the LumioUI Action Sheet component. It's interactive, so try it.
How a bottom sheet works
- Position. One shared value holds how far the sheet is pushed down: 0 when open, the screen height when closed. Animate it with a spring to open and a short timing to close, so leaving is quicker than arriving.
- Backdrop. Derive the backdrop's opacity from the same value, so it dims in step with the sheet and never gets out of sync.
- Drag to dismiss. A pan gesture adds the finger's movement to the value. On release, a fast flick (around 800 points per second) or a drag past a threshold closes it; anything else springs back.
- Accessibility. Give the backdrop a "Close" label, move focus into the sheet when it opens, and let screen-reader users close it without dragging.
Here is that idea in plain Reanimated and gesture-handler:
import * as React from "react";
import { Dimensions, Pressable, StyleSheet, View } from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, {
runOnJS, useAnimatedStyle, useSharedValue, withSpring, withTiming,
} from "react-native-reanimated";
const H = Dimensions.get("window").height;
export function Sheet({ open, onClose, children }) {
// 0 = fully open, H = off screen
const y = useSharedValue(H);
React.useEffect(() => {
y.value = open ? withSpring(0, { damping: 22, stiffness: 220 }) : withTiming(H, { duration: 200 });
}, [open]);
const drag = Gesture.Pan()
.onChange((e) => { y.value = Math.max(0, y.value + e.changeY); })
.onEnd((e) => {
// A fast flick down or a drag past a third closes it
if (e.velocityY > 800 || y.value > 200) runOnJS(onClose)();
else y.value = withSpring(0);
});
const sheet = useAnimatedStyle(() => ({ transform: [{ translateY: y.value }] }));
const backdrop = useAnimatedStyle(() => ({ opacity: 1 - y.value / H }));
return (
<View style={StyleSheet.absoluteFill} pointerEvents={open ? "auto" : "none"}>
<Animated.View style={[StyleSheet.absoluteFill, { backgroundColor: "rgba(0,0,0,.4)" }, backdrop]}>
<Pressable style={{ flex: 1 }} onPress={onClose} accessibilityLabel="Close" />
</Animated.View>
<GestureDetector gesture={drag}>
<Animated.View style={[{ position: "absolute", left: 0, right: 0, bottom: 0 }, sheet]}>
{children}
</Animated.View>
</GestureDetector>
</View>
);
} It works, but a production sheet needs more: safe-area padding, a keyboard that doesn't cover inputs, reduced-motion support, haptics, and a backdrop that blocks touches only while open.
The quick way: LumioUI Action Sheet
The Action Sheet handles all of that. It's a detached sheet of actions that rises over a fading backdrop. Your pick stays highlighted while the other rows dim, then it closes. Drag down or tap outside to dismiss. It's free and runs in Expo Go.
import * as React from "react";
import { ActionSheet } from "@/components/ui/action-sheet/action-sheet";
import { Button } from "@/components/ui/button/button";
export function PhotoMenu() {
const [open, setOpen] = React.useState(false);
return (
<>
<Button onPress={() => setOpen(true)}>Edit photo</Button>
<ActionSheet
open={open}
onOpenChange={setOpen}
title="Edit photo"
message="Changes apply to this photo only."
actions={[
{ label: "Crop", onPress: () => {} },
{ label: "Replace", onPress: () => {} },
{ label: "Delete photo", destructive: true, onPress: () => {} },
]}
/>
</>
);
} openandonOpenChangemake it controlled. UsedefaultOpento let it manage itself.- Actions take
destructiveanddisabled.cancelLabelrenames the Cancel row. material="glass"switches to the glass surface over photos or video.
Need a sheet that settles at different heights, holds forms and keeps clear of the keyboard? Use the Pro Bottom Sheet.
Add it to your app
LumioUI's CLI copies the component's source into your project, so it's yours to read and change. Run init once per project:
npx @lumioui/cli init
npx @lumioui/cli add action-sheet It works in Expo (SDK 54+) and bare React Native, with Reanimated 4 and NativeWind. See the installation guide and the full Action Sheet API.
Related components
More React Native guides
- Animated tabs with a sliding indicator in React Native
- How to show toast notifications in React Native
- Skeleton loading screens in React Native
- How to add dark mode and a theme toggle in React Native
- How to build an accordion in React Native
- How to make a side drawer menu in React Native
- How to make an animated heart like button in React Native
- How to make a modal dialog in React Native
- How to build an animated carousel in React Native
- How to make an animated splash screen in React Native and Expo
- How to build a month calendar in React Native