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
PropTypeDefaultDescriptionPlatforms
open requiredboolean—

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.

web, lit, rn, swiftui
headingstring—

What the actions apply to ("Photo.jpg"), shown muted above the list. Also the accessible name; when omitted the name is `copy.defaultLabel`.

web, lit, rn, swiftui
actions requiredarray { 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.

web, lit, rn, swiftui
dismissiblebooleantrue

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.

web, lit, rn, swiftui
cancelLabelstring—

Label of the explicit cancel row on phones. Defaults to `copy.cancelLabel`.

web, lit, rn, swiftui

Events

2 rows
EventTypeDescriptionPlatforms
onActiononAction / action

An action was chosen; receives its `id`. The consumer performs it and closes.

web, lit, rn, swiftui
onCloseonClose / close

Dismissed without choosing: reason `escape`, `scrim`, `cancel`, or `drag`.

web, lit, rn, swiftui

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

scrimsurfacefocusScopehandleheaderheadinglistitemitemIcondividercancelButton

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.

ActionSheet keyboard support

8 rows
KeyAction
EscapeCloses without choosing.
ArrowDownMoves focus to the next action.
ArrowDownFrom the last action wraps to the first.
ArrowUpFrom the first action wraps to the last.
HomeFirst action.
EndLast action.
Enter or SpaceFires onAction for the focused action (nothing when it is disabled); the sheet does not close itself — the Keyboard story's consumer closes on onAction, which is the close `expect: closes` observes. Implement the key as activation only: no close handler of the sheet's own. (focus on an action)
TabCloses and moves focus on (a menu is not a tab stop container). No Tab handler is needed: the roving tabindex leaves one stop and the outside-close rule does the rest, which is why this rule is manual rather than asserted. (wide-screen menu presentation)

Contrast pairs

4 rows
ForegroundBackgroundLevel
color.foregroundcolor.overlay.surfaceAA
color.foregroundcolor.background.subtleAA
color.foreground.dangercolor.overlay.surfaceAA
color.foreground.mutedcolor.overlay.surfaceAA

Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.