Action sheet
Free · MITReact Native · ExpoAn iOS-style list of choices that springs up from the bottom edge in grouped cards; it follows the finger to be dragged away, and pulling it up stretches the gap between the cards before they spring back together.
Installation
$ npx shadcn@latest add https://goodui.dev/native/r/action-sheet.jsonThen run npx expo install --fix so react-native-gesture-handler, react-native-reanimated match your Expo SDK. Files land in components/goodui-native/action-sheet.
- Wrap the app in GestureHandlerRootView and ThemeProvider (the sheet renders in its overlay layer).
Usage
import { ActionSheet } from "@/components/goodui-native/action-sheet/action-sheet";
export function ConversationOptions({ open, onOpenChange, chat }: Props) {
return (
<ActionSheet
open={open}
onOpenChange={onOpenChange}
title={chat.name}
groups={[
[
{ label: "Mute for 8 hours", onPress: () => mute(chat.id) },
{ label: "Pin to top", onPress: () => pin(chat.id) },
],
[{ label: "Delete conversation", destructive: true, onPress: () => remove(chat.id) }],
]}
/>
);
}When to use
- Two to eight choices about one thing the person just touched: a conversation, a photo, a card.
- Choices that include a destructive one, set apart in its own group.
- Forms, filters or long content: use bottom-sheet.
- Choices about an item in a list where the item should stay visible and in place: use context-menu.
- A single yes or no question: use alert-dialog.
Examples
Icons, attached to the edge
<ActionSheet
open={open}
onOpenChange={setOpen}
variant="attached"
title="Profile photo"
actions={[
{ label: "Take photo", icon: (p) => <CameraIcon {...p} />, onPress: openCamera },
{ label: "Choose from library", icon: (p) => <ImageIcon {...p} />, onPress: openLibrary },
{ label: "Remove photo", icon: (p) => <TrashIcon {...p} />, destructive: true, onPress: removePhoto },
]}
/>A choice that must be made
<ActionSheet
open={open}
onOpenChange={setOpen}
dismissible={false}
title="You have unsaved changes"
cancelLabel="Keep editing"
actions={[
{ label: "Save draft", onPress: saveDraft },
{ label: "Discard changes", destructive: true, onPress: discard },
]}
/>API reference
| Prop | Type | Default |
|---|---|---|
| open* Whether the sheet is shown. Closing animates out before it unmounts. | boolean | — |
| onOpenChange* Called with false when an action is chosen, on cancel, on a drag down, a backdrop tap or back. | (open: boolean) => void | — |
| title Short context above the actions, such as the item's name. | string | — |
| message One line explaining the choice. | string | — |
| actions One group of actions. Use `groups` for several. | ActionSheetAction[] | — |
| groups Several groups, each shown as its own card (inset) or separated by a band (attached). | ActionSheetAction[][] | — |
| cancelLabel Label of the cancel row. Pass `null` to leave it out. Same as `labels.cancel`. | string | null | — |
| variant `inset` floats above the bottom edge as cards, like iOS; `attached` is one sheet with a grabber on the edge. | "inset" | "attached" | — |
| dismissible Allow closing by dragging down, tapping outside, back or Escape. Default `true`. | boolean | — |
| haptic Haptic when an action is chosen. Default `true`. | boolean | — |
| labels Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide. | Partial<ActionSheetLabels> | — |
| testID Identifier for end-to-end tests. | string | — |
Motion and haptics
- Enter: the cards rise from below the edge on the smooth spring while the backdrop fades in.
- Drag: the sheet follows the finger 1:1 downward and the backdrop lightens with it; release projects the velocity and either dismisses or springs back.
- Pull up: the sheet resists more the further it goes, and the action cards travel further than the cancel card so the gap between them stretches; on release they spring back together on the bouncy spring.
- Rows fill on touch down; a drag that moves the sheet cancels the press.
- Reduced motion: no slide and no stretch; the sheet fades in and out.
- Haptic: tap when an action is chosen (not for cancel)
- Drag, backdrop and the stretch run on the UI thread; a closed sheet mounts nothing.
Accessibility
- The sheet is modal for screen readers (accessibilityViewIsModal) and announces its title when it opens.
- Each row is a button with a label and a disabled state; double tap activates it.
- Every row is at least 56 pt tall; on the web rows are focusable and Enter or Space chooses, Escape closes.
- Android back closes the sheet when it is dismissible.
Notes for AI
- onPress runs after the sheet has finished closing, so it is safe to open an alert, another sheet or navigate from it.
- Use groups to separate the destructive action from the others; cancel is added automatically (cancelLabel={null} removes it).
- Rows align to the leading edge when any action has an icon; otherwise they are centered like iOS.
- For an irreversible destructive choice, follow up with an Undo toast rather than a second confirmation.
Related
A sheet that rests at detents, follows the finger 1:1, settles with velocity and hands drags off to its scroll view.
Long-press an element and it lifts out of the page above a dimmed backdrop while a menu springs out beside it; keep the finger down, slide onto an item and release to choose. Right-click opens it on the web.
A centered alert that springs in from slightly larger; touch an action and slide across the others with a tick per action, tap outside and it nudges, and async actions keep it open with a spinner until they settle.
Background confirmations that stack at the top, fan out on tap, swipe away up or sideways, and pause their timers while touched.