ActionSheet
ActionSheet
const [open, setOpen] = useState(false);
<> <FAB onClick={() => setOpen(true)} aria-label="New" icon={<PlusIcon />} /> <ActionSheet isOpen={open} onOpenChange={setOpen} title="Add to your library" description="Choose what you want to add." actions={[ { id: 'photo', label: 'Take photo', icon: <CameraIcon />, onAction: (close) => { /* … */ close(); } }, { id: 'upload', label: 'Upload from device', icon: <UploadIcon />, onAction: (close) => { /* … */ close(); } }, { id: 'paste', label: 'Paste from clipboard', icon: <ClipboardIcon />, onAction: (close) => { /* … */ close(); } }, { id: 'delete', label: 'Delete library', destructive: true, onAction: (close) => { /* … */ close(); } }, ]} /></>Install: @helixui/core
import { ActionSheet, type ActionSheetAction } from '@helixui/core'status: stable · since: 0.5.0
Tags: mobile, modal, menu, bottom-sheet, actions, ios
Anatomy
[overlay (dimmed)] ┌── action sheet panel (slides up from bottom) ──┐ │ Title (optional) │ │ ───────────────────────────────────────────── │ n │ • Action 1 │ │ • Action 2 │ │ • Destructive action (red) │ │ ───────────────────────────────────────────── │ │ Cancel │ └────────────────────────────────────────────────┘Layout
- display —
portal - width —
fill - height —
content - intrinsicSize —
pinned to bottom edge, max-h ~80vh, scrolls inside - stackable —
false - fullBleed —
true
Visual
A bottom-anchored panel with rounded top corners (radius.lg) that slides up from the bottom edge over a dimmed overlay. Each action is a full-width row separated by hairlines; destructive rows render in danger tone. A trailing Cancel row is separated by a small gap. Mounts with a translateY animation that respects prefers-reduced-motion.
Props
| Name | Type | Default | Description |
|---|---|---|---|
title | ReactNode | — | Top centered title. |
description | ReactNode | — | Sub-title under the title. |
actions | ActionSheetAction[] | — | Required. List of actions; each has id, label, optional description, icon, destructive, disabled, and onAction(close). |
cancelLabel | ReactNode | Cancel | Label for the bottom cancel button. Set null to hide. |
isOpen | boolean | — | Controlled open state. |
defaultOpen | boolean | false | Uncontrolled initial open. |
onOpenChange | (open: boolean) => void | — | Open change handler. |
Tokens used
color.bg.surface.default, color.bg.action.neutral.default, color.text.primary, color.text.secondary, color.text.muted, color.text.action.brand, color.text.action.danger, color.border.default, color.border.focus, radius.lg, space.1, space.2, space.3, space.4, font.family.sans, font.size.xs, font.size.sm, font.size.md, font.weight.medium, font.weight.semibold
Accessibility
Role: dialog
Keyboard
| Key | Action |
|---|---|
Escape | Close. |
Notes
- Built on react-aria-components Modal — focus trap, restoration, dismiss on overlay click, all handled.
- The cancel button is a button (not an action item) so it visually separates from the list — matches iOS pattern.
Composes with
| Component | Relation | Note |
|---|---|---|
Sheet | alternative | Use Sheet for general side drawers; ActionSheet is the iOS-style action menu only. |
Menu | alternative | On desktop prefer Menu anchored to a trigger. |
Button | trigger | Common opener. |
Dialog | sibling | |
FAB | sibling |
Prompt examples
These are the AI prompt → JSX mappings used by the helixui prompt DSL and integrations like Cursor / Claude Code.
mobile destructive choice
“show a delete / cancel sheet on tap”
<ActionSheet isOpen={open} onOpenChange={setOpen} actions={[ { label: 'Delete', tone: 'danger', onPress: handleDelete }, { label: 'Cancel', tone: 'neutral' }, ]}/>photo source picker
“Take photo / Choose from library / Cancel sheet”
<ActionSheet title="Add photo" isOpen={open} onOpenChange={setOpen} actions={[ { label: 'Take Photo', onPress: takePhoto }, { label: 'Choose from Library', onPress: pick }, { label: 'Cancel' }, ]}/>