DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

Action Sheet

A mobile-first bottom sheet presenting a set of contextual actions. Built on the Base UI Drawer primitive with swipe-to-dismiss. Compound component pattern with Header, Body, Group, Item, Footer and Cancel. List, grid and rail layouts. Five heights, five roundings, three scrim modes, single and multi selection, LTR and RTL support.

Playground

Installation

pnpm add @tessinaui/ui

Usage

import {
  ActionSheet,
  ActionSheetTrigger,
  ActionSheetContent,
  ActionSheetHeader,
  ActionSheetTitle,
  ActionSheetDescription,
  ActionSheetBody,
  ActionSheetGroup,
  ActionSheetSeparator,
  ActionSheetItem,
  ActionSheetFooter,
  ActionSheetCancel,
} from "@tessinaui/ui";
<ActionSheet>
  <ActionSheetTrigger className="...">Open</ActionSheetTrigger>

  <ActionSheetContent>
    <ActionSheetHeader>
      <ActionSheetTitle>Actions</ActionSheetTitle>
      <ActionSheetDescription>Choose an action below.</ActionSheetDescription>
    </ActionSheetHeader>

    <ActionSheetGroup>
      <ActionSheetItem icon={<Share2 />}>Share</ActionSheetItem>
      <ActionSheetItem icon={<Copy />}>Copy link</ActionSheetItem>
      <ActionSheetSeparator />
      <ActionSheetItem intent="error" icon={<Trash2 />}>Delete</ActionSheetItem>
    </ActionSheetGroup>

    <ActionSheetCancel />
  </ActionSheetContent>
</ActionSheet>

When to use

  • Use an action sheet for a short list of actions that follow from something the person just did — a "more" button, a long-press, a share affordance — on phones and narrow viewports.
  • Use a Dropdown Menu where there is a pointer and room to anchor a panel to its trigger. Keyboard shortcuts, cascading submenus and hover-open belong there, not here. See Responsive for the swap.
  • Use a Modal when the person must read something or fill something in before continuing, and a Drawer for a panel with its own workflow (filters, forms, inspectors).
  • Keep it short. Carbon caps context menus at 12 items, Atlassian at 15, and Apple asks you to avoid scrolling an action sheet at all. If your list scrolls far, it is a Drawer or a page.
  • Labels are verbs. "Delete photo", not "Photo deletion". Atlassian's 24-character ceiling is a good target; the row truncates rather than wrapping.

Examples

Default

A trigger button that opens a bottom sheet with a group of actions and a cancel row.

With Header

Add an ActionSheetHeader with a title and description above the actions. Pass close for the trailing dismiss button.

Destructive Actions

Mark dangerous actions with intent="error". In a list sheet they go last, separated by a rule — the convention Carbon, Fluent and Primer share. (In a confirmation sheet the destructive button goes on top instead; see Confirmation.)

Post options

The full row vocabulary: leading icons, a trailing chevron, a trailing badge, a link row that keeps its link semantics through render, and destructive actions below a rule. onAction on the root reports each row's value in one handler.

Sizes

Five height budgets — sm (40dvh), md (60dvh), lg (75dvh), xl (90dvh) and full (the whole viewport) — the same ramp as Drawer's bottom side. A full sheet always needs a close affordance, since there is no scrim strip left to tap.

Rounded

Five corner-rounding options via the rounded prop. The value cascades to the handle, tile plates and skeleton.

Curving your own content

Content you put inside — an avatar or preview tile in a row — should curve like the chrome around it, or it reads as pasted on. The sheet publishes its inner radius as a CSS variable:

<img className="rounded-[var(--action-sheet-item-radius)]" />

It tracks whatever the component is actually doing: the explicit step when rounded is set (6px at md), and 0 at rounded="none" — so your content squares off exactly when the container does.

Inset and edge-to-edge

inset (the default) floats the sheet off the viewport edges, the iOS-classic card. inset={false} attaches it edge-to-edge with only the top corners rounded — the majority form in the wild and what Material 3 specifies for compact widths.

Scrim

blur frosts and dims the page, dim is a plain dark overlay, and shadow keeps the page fully visible so the panel separates through elevation alone. Clicks outside still dismiss in every mode.

Non-modal

modal={false} renders no scrim, locks nothing and leaves the page interactive. modal="trap-focus" keeps focus inside the sheet while the page stays scrollable.

Header actions

The header takes a leading back slot, a trailing action slot and a close button. With any slot present it becomes a three-column title row; with none it stays a centered iOS-style block.

Groups

detached gives a group its own card below the sheet (the iOS grouped shape), dividers draws a hairline between every row, variant="inset" recesses the group into a well, and label gives it a heading that also names it for assistive tech.

Share

layout="rail" scrolls targets horizontally and layout="grid" wraps them into icon tiles — the two share-sheet shapes. ActionSheetBody holds free content such as a link field.

Attach

Tiles above a list, two-line rows, and a switch in the trailing slot. A row whose trailing control is the affordance sets closeOnSelect={false} so the sheet stays open.

Account switcher

selection="single" marks the current row with aria-current and a trailing check. The leading slot sizes icons but never media, so an Avatar keeps its own box.

Sort

A single-select group with an ActionSheetFooter. Rows set closeOnSelect={false} so the choice is applied by the footer button rather than on every tap.

Collections

selection="multiple" marks rows with aria-pressed and keeps the sheet open by default. A Search field in ActionSheetBody filters the list.

Confirmation

A title, a description and an ActionSheetFooter of buttons. Stacked, the affirmative action goes first in the DOM and on top — the order Material 3, Apple and Astryx all specify. Side by side, dismiss goes left and the affirmative right.

Photo source

align="center" centers row labels for the stock-iOS look, the preferred action goes first, and detached={false} turns Cancel into an ordinary last row instead of its own card.

Nested navigation

Drill-down panes inside one sheet: rows with closeOnSelect={false} swap the content and ActionSheetBack returns.

Responsive

One action list, two containers — a bottom sheet on phones and an anchored dropdown menu from md up. Both are rendered and swapped by CSS, so there is no hydration mismatch.

Loading

Use ActionSheetSkeleton as a placeholder while sheet content loads. It takes the same size, rounded and inset props as the sheet and reads the same row geometry.

API Reference

ActionSheet

Root component. Provides context and wraps the Base UI Drawer.Root.

PropTypeDefaultDescription
size"sm" | "md" | "lg" | "xl" | "full""md"Height budget (40dvh / 60dvh / 75dvh / 90dvh / full viewport)
rounded"none" | "sm" | "md" | "lg" | "full""md"Corner radius; cascades to the handle, tile plates and skeleton
insetbooleantrueFloat the sheet off the viewport edges, or attach it edge-to-edge
dir"ltr" | "rtl""ltr"Text direction applied to the panel and all children
openboolean—Controlled open state
defaultOpenboolean—Uncontrolled initial open state
onOpenChange(open: boolean, event: Event) => void—Called when the open state changes
onOpenChangeComplete(open: boolean) => void—Called after the open/close animation settles
onAction(value: string, event: React.MouseEvent) => void—Called with an item's value when it is activated, before the sheet closes
dismissiblebooleantrueWhether scrim tap, Escape and the swipe close the sheet
modalboolean | "trap-focus"truetrue traps focus, locks scroll and shows the scrim; "trap-focus" traps focus only; false is fully non-modal
scrim"blur" | "dim" | "shadow""blur"Background treatment while open (modal only)

ActionSheetContent

Renders the panel anchored to the bottom of the viewport inside a portal, with the backdrop when the sheet is modal. ActionSheetCancel and any ActionSheetGroup detached child are hoisted out of the main card automatically, fragments included.

Base UI's initialFocus and finalFocus pass through: use initialFocus to name the element that takes focus on open, and finalFocus to redirect focus on close.

PropTypeDefaultDescription
sizeActionSheetSize—Overrides the height budget from the root
roundedActionSheetRounded—Overrides the corner radius from the root
insetboolean—Overrides the inset presentation from the root
showHandlebooleantrueWhether to show the drag handle pill
aria-labelstring—Required when the sheet has no ActionSheetHeader

ActionSheetHeader

Optional header with title and description. With no slots it is a centered block; give it back, action or close and it becomes a three-column title row.

PropTypeDefaultDescription
closeboolean | ReactNode—true renders the house close button; a node supplies your own
actionReactNode—Trailing text action ("Edit", "Done", "Select all")
backReactNode—Leading slot, for ActionSheetBack
dividerbooleantrueHairline under the header

ActionSheetGroup

Container for action rows or tiles.

PropTypeDefaultDescription
layout"list" | "grid" | "rail""list"Rows, a wrapping tile grid, or a horizontal tile rail
columnsnumber4Columns when layout="grid"
variant"plain" | "inset" | "cards""plain"On the card, in a recessed well, or one cell per row
labelReactNode—Heading above the group; also names it for assistive tech
dividersbooleanfalseHairline between every row
detachedbooleanfalseRender as its own card below the sheet
selection"none" | "single" | "multiple""none"Row selection semantics
align"start" | "center""start"Label alignment inside rows

ActionSheetItem

Individual action row, or a tile inside a grid or rail group.

PropTypeDefaultDescription
intent"none" | "error""none"error is the destructive row
variant"default" | "strong""default"Deprecated — no longer renders anything. strong used to bold the preferred action's label; control text is now one weight library-wide, so the row paints identically either way. Still accepted so callers do not break — mark a preferred action with intent or ordering instead
size"sm" | "md" | "lg" | "xl"—Overrides the row density from the sheet
iconReactNode—Leading icon, avatar or thumbnail (decorative)
trailingReactNode—Trailing chevron, value text, badge or shortcut (decorative, never interactive)
descriptionReactNode—Second line under the label
selectedbooleanfalseMarks the current choice inside a selection group
loadingbooleanfalseShows a spinner and marks the row busy: it stays focusable but refuses activation, so the sheet stays open
closeOnSelectbooleantrueClose the sheet when the row activates (false in selection="multiple" groups)
valuestring—Reported to the root's onAction
renderReactElement—Swap in a custom element — an <a> for navigation rows
disabledboolean—Disables the row. A render link, which ignores disabled, takes aria-disabled and leaves the tab order

intent="destructive" and intent="default" still work as deprecated aliases of error and none, and will be removed in a future minor.


ActionSheetFooter

Action area at the bottom of the sheet. Put house Buttons inside.

PropTypeDefaultDescription
stackedbooleanstacked below sm, row aboveFull-width stacked actions, or a row
dividerbooleanfalseHairline above the footer

ActionSheetCancel

PropTypeDefaultDescription
size"sm" | "md" | "lg" | "xl"—Overrides the size from context
showbooleantruefalse renders nothing
detachedbooleantrueIts own card below the sheet, or an ordinary last row
childrenReactNode"Cancel"Button label

ActionSheetBack

Leading back button for drill-down sheets. Renders an icon only, so aria-label is required. The arrow flips automatically in RTL.

ActionSheetBody

Padded, scrollable region for free content — fields, chips, media, a paragraph. Use ActionSheetGroup for action rows.

ActionSheetSeparator

Horizontal rule between rows, inset to the label start.

ActionSheetTrigger · ActionSheetClose

Buttons that open and close the sheet. Style via className or swap the element with render.

ActionSheetSkeleton

PropTypeDefaultDescription
size · rounded · inset—"md" · "md" · trueMirror the live sheet
showHandlebooleantrueRender the handle
showHeaderbooleanfalseRender the header bars
itemsnumber4Number of row placeholders
showCancelbooleantrueRender the cancel card
labelstring | null"Loading"Announced while shown; null stays silent

Behaviour

  • Slides up from the bottom edge on a 450ms deceleration curve. Releasing a swipe scales the exit duration by the flick velocity, so a hard flick lands fast.
  • Swipe down to dismiss. Touch swipes work anywhere on the panel and respect scrollable content (the gesture engages at the scroll edge); mouse drags work from the handle and panel chrome, so text inside the sheet stays selectable.
  • Closes on scrim tap, Escape, the swipe, ActionSheetCancel, and any ActionSheetItem with closeOnSelect (the default). dismissible={false} blocks the first three and keeps the rest.
  • Height is constrained by size; the group scrolls internally when content overflows.
  • The panel is capped at 480px and centered on viewports wider than that.

Accessibility

  • The panel is a Base UI drawer dialog: role="dialog", labelled by ActionSheetTitle and described by ActionSheetDescription. A sheet with no header must pass aria-label on ActionSheetContent — development builds warn when neither is present.
  • Rows follow the APG modal-dialog pattern, not the menu pattern: Tab and Shift+Tab move between rows and wrap, Enter and Space activate, Escape closes. Rows are buttons, not menuitems, so no arrow-key roving is claimed. Mixing the two would be a violation of both patterns.
  • Focus moves into the panel on open and returns to the trigger on close. Nothing auto-focuses a destructive row; pass initialFocus on ActionSheetContent to name a safer target explicitly.
  • selection="single" marks the current row with aria-current="true"; selection="multiple" uses aria-pressed. A group with a label is a named group.
  • The leading icon and trailing cluster are decorative (aria-hidden) — the row's text is its accessible name. Never put an interactive control in trailing; give the row closeOnSelect={false} and let the whole row be the target instead.
  • Rows meet the 44×44px touch floor on phones at every size (WCAG 2.5.5). The drag handle is decorative and not focusable — every gesture has a button equivalent.
  • Every part exposes a stable data-slot (action-sheet-content, action-sheet-header, action-sheet-group, action-sheet-item, action-sheet-footer, action-sheet-cancel, …) for testing and agent tooling.
  • All motion is disabled under prefers-reduced-motion: reduce. The swipe is direct manipulation and stays available.
  • AT matrix: VoiceOver + Safari verified locally. NVDA and JAWS untested — the component stays beta until both are on record.
AccordionAlert

On this page

PlaygroundInstallationUsageWhen to useExamplesDefaultWith HeaderDestructive ActionsPost optionsSizesRoundedCurving your own contentInset and edge-to-edgeScrimNon-modalHeader actionsGroupsShareAttachAccount switcherSortCollectionsConfirmationPhoto sourceNested navigationResponsiveLoadingAPI ReferenceActionSheetActionSheetContentActionSheetHeaderActionSheetGroupActionSheetItemActionSheetFooterActionSheetCancelActionSheetBackActionSheetBodyActionSheetSeparatorActionSheetTrigger · ActionSheetCloseActionSheetSkeletonBehaviourAccessibility