Action sheet

Free · MITReact Native · Expo

An 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.

View as Markdown
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.

Installation

$ npx shadcn@latest add https://goodui.dev/native/r/action-sheet.json

Then 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

Use it for
  • 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.
Reach for something else
  • 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

PropTypeDefault
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.