Labeling
SheetContent does not name itself. Naming the sheet is your job, and it is required: a sheet without a name is inaccessible, because screen readers announce it only as "dialog". Reference a visible heading with aria-labelledby on SheetContent, or set aria-label when there is no visible heading. Non-closable sheets should explain why the user is in the flow and how to complete it.
import { useId, useState, type CSSProperties } from 'react';
import {
Button,
Heading,
Icon,
IconButton,
Sheet,
SheetClose,
SheetContent,
SheetHandle,
SheetTrigger,
Text,Keyboard Behavior
| Key | Action |
|---|---|
| EnterSpace | On the trigger: opens the sheet. The sheet itself takes the focus, or the child with autoFocus. |
| TabShift + Tab | Moves the focus to the next or previous control inside the sheet. The page behind is inert, so the focus does not reach it. A SheetHandle without several snap points is disabled and skipped. |
| EnterSpace | On SheetHandle with several snap points: moves the sheet to the next higher snap point, and from the highest one back to the lowest. |
| Escape | Closes the sheet and returns the focus to the element that opened it. With closedBy='none' it does nothing. |
SheetHandle is always a <button> and has no name of its own: give it one with aria-label, for example Resize sheet. Without several snap points it is disabled and aria-hidden, so it is not announced, and the sheet can still be dragged by it.
Focus Behavior
- Focus should move into the sheet on open.
- Focus should restore sensibly on close.
Focus handling comes from the native <dialog>: the rest of the page is inert while the sheet is open, Tab stays inside the sheet (it can still reach the browser UI), and closing restores focus to the element that was focused before opening.
On open the sheet itself takes focus, so nothing inside is highlighted and screen readers announce its title; Tab moves to the first control. Put autoFocus on a child to focus it instead, for example the first field of a form.
Screen Reader Semantics
- The sheet is exposed as
role=dialogwitharia-modalby the browser and is named by youraria-labelledbyoraria-label. - The trigger exposes
aria-haspopup='dialog'andaria-expanded. SheetClosehas no default label, so an icon-only button inside it needs anaria-label; the library ships no built-in strings.- The sheet should read as one coherent overlay surface.
- Non-closable sheets need especially clear purpose.
Form Semantics
- If the sheet contains a form, required fields and error messaging should remain visible at every snap point.
- Gesture-friendly layout should not hide the real submit and dismissal semantics.
- Buttons in a
<form method="dialog">close the sheet throughonChangeOpen(false), and the submit button'svaluebecomes the dialog'sreturnValue. This also works withclosedBy='none', so a non-closable sheet still has a completion path.
Accessibility Risks
- A sheet without
aria-labelledbyoraria-label. - Relying only on gesture understanding.
- Snap points that create unusable partially visible content.
- Non-closable flows without clear completion path.