ActionSheet
A short list of actions for the thing the user just touched — a sheet from the bottom on phones, a Menu anchored to the trigger on wide screens.
Basic usage
Install the package:
pnpm add @design-schema/react @design-schema/tokens Import the stylesheet once for the app, then the component:
import '@design-schema/tokens/calm-precise/css';
import '@design-schema/react/index.css';
import { ActionSheet } from '@design-schema/react'; Examples
import { useEffect, useState, type ComponentProps, type ReactElement } from 'react';
import { ActionSheet, type ActionSheetAction, type ActionSheetCloseReason } from '@design-schema/react';
const PHOTO_ACTIONS: ActionSheetAction[] = [
{ id: 'share', label: 'Share', icon: 'external' },
{ id: 'rename', label: 'Rename' },
{ id: 'duplicate', label: 'Duplicate' },
{ id: 'delete', label: 'Delete photo', icon: 'danger', tone: 'danger' },
];
function Consumer(args: ComponentProps<typeof ActionSheet>): ReactElement {
const [open, setOpen] = useState(args.open);
useEffect(() => setOpen(args.open), [args.open]);
return (
<ActionSheet
{...args}
open={open}
onAction={(id: string) => {
args.onAction?.(id);
setOpen(false);
}}
onClose={(reason: ActionSheetCloseReason) => {
args.onClose?.(reason);
setOpen(false);
}}
/>
);
}
<Consumer open heading="Photo.jpg" actions={PHOTO_ACTIONS} />Props
5 rows| Prop | Type | Default | Description |
|---|---|---|---|
| open required | boolean | — | Controlled only — there is no uncontrolled mode; the consumer owns `open`, the sheet requests a dismissal through `onClose` and reports a choice through `onAction`, and the consumer sets `open` to false for both. |
| heading | string | — | What the actions apply to ("Photo.jpg"), shown muted above the list. Also the accessible name; when omitted the name is `copy.defaultLabel`. |
| actions required | array { id: string; label: string; icon?: IconName; tone?: "default" | "danger"; disabled?: boolean }[] | — | Two to about eight actions. `danger` actions are visually distinct and grouped last: the component does the grouping, so the consumer may pass them in any order — the default actions render in the order given, then the danger ones in the order given. The count is guidance, not enforced: no dev warning outside that range. Every optional field also accepts an explicit `undefined` (`icon?: IconName | undefined`, and so on) wherever the type is written out, since rn Menu builds these objects with explicit undefined values under exactOptionalPropertyTypes. |
| dismissible | boolean | true | Escape, the scrim, the cancel row and the drag all request close. When false, as in Dialog: the Cancel row and the divider above it are not rendered, the drag handle is not rendered, the scrim and the drag do nothing, and Escape still reports through onClose; with no `heading` either, the header has nothing to show and is not rendered at all (no empty padded strip). It gates the sheet presentation only — the wide Menu presentation has no scrim, drag or cancel row, and clicking outside always closes it. |
| cancelLabel | string | — | Label of the explicit cancel row on phones. Defaults to `copy.cancelLabel`. |
Events
2 rows| Event | Type | Description |
|---|---|---|
| onAction | onAction / action | An action was chosen; receives its `id`. The consumer performs it and closes. |
| onClose | onClose / close | Dismissed without choosing: reason `escape`, `scrim`, `cancel`, or `drag`. |
Accessibility
Decisions this component leaves to you
Supply an accessible name. This component has no text of its own to fall back on, so without one a screen reader announces its role and nothing else.
If you wire up the gesture events, provide the pointer and keyboard path to the same outcome.
Implicit ARIA role: menu.
Anatomy
What generation guarantees
Accessible name — Every instance carries a programmatic name. Where the component has no text of its own to use, the name is a prop the consumer supplies.
Focus trap — While it is open, Tab cycles inside it; focus cannot reach the page behind.
Focus restore — On close, focus returns to whatever opened it.
Escape dismisses — Escape closes it from anywhere inside.
Inert background — While it is open, the rest of the page is inert to the pointer and to assistive technology.
Arrow navigation — Arrow keys move between items; Home and End jump to the ends.
Roving tabindex — The group is a single tab stop; the arrow keys move within it.
Keyboard operable — Everything the pointer can do here, the keyboard can do — see the keyboard section.
Visible focus — Keyboard focus is always drawn, from the theme’s focus tokens, and is never removed.
Contrast — Text and meaningful non-text contrast meets WCAG AA; the pairs are listed below.
Reduced motion — Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.
Target size — Interactive targets are at least 44×44 CSS pixels — the touch-first size, not the minimum.
Gesture alternative — Every gesture — swipe, drag, long-press — has a single-pointer and a keyboard equivalent.
Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.