Skip to content

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

  • displayportal
  • widthfill
  • heightcontent
  • intrinsicSizepinned to bottom edge, max-h ~80vh, scrolls inside
  • stackablefalse
  • fullBleedtrue

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

NameTypeDefaultDescription
titleReactNodeTop centered title.
descriptionReactNodeSub-title under the title.
actionsActionSheetAction[]Required. List of actions; each has id, label, optional description, icon, destructive, disabled, and onAction(close).
cancelLabelReactNodeCancelLabel for the bottom cancel button. Set null to hide.
isOpenbooleanControlled open state.
defaultOpenbooleanfalseUncontrolled initial open.
onOpenChange(open: boolean) => voidOpen 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

KeyAction
EscapeClose.

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

ComponentRelationNote
SheetalternativeUse Sheet for general side drawers; ActionSheet is the iOS-style action menu only.
MenualternativeOn desktop prefer Menu anchored to a trigger.
ButtontriggerCommon opener.
Dialogsibling
FABsibling

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' },
]}
/>