Labeling
DrawerContent does not name itself. Naming the drawer is your job, and it is required: a drawer without a name is inaccessible, because screen readers announce it only as "dialog".
- Reference a visible heading with
aria-labelledbyonDrawerContent, or setaria-labelwhen there is no visible heading. - If the drawer owns a focused task, the heading should describe that task directly.
side-inspector.tsx · 60 lines
import { useId, type CSSProperties } from 'react';
import {
Badge,
Button,
Drawer,
DrawerClose,
DrawerContent,
DrawerTrigger,
Heading,
Icon,
IconButton,
Text,Keyboard Behavior
AnnouncementFocus the demo and press a key from the table.
| Key | Action |
|---|---|
| EnterSpace | On the trigger: opens the drawer. The drawer itself takes the focus, or the child with autoFocus. |
| TabShift + Tab | Moves the focus to the next or previous control inside the drawer. The page behind is inert, so the focus does not reach it. |
| Escape | Closes the drawer and returns the focus to the element that opened it. With closedBy='none' it does nothing. |
In nested drawers Escape closes only the top one.
Focus Behavior
- Focus should move into the drawer on open.
- Focus should return sensibly to the original trigger or equivalent action on close.
- Nested drawer stacks need real integration testing instead of isolated assumptions.
Focus handling comes from the native <dialog>: the rest of the page is inert while the drawer is open, Tab stays inside the drawer (it can still reach the browser UI), and closing restores focus to the element that was focused before opening.
On open the drawer 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 drawer is exposed as
role=dialogwitharia-modalby the browser and is named by youraria-labelledbyoraria-label. - The trigger exposes
aria-haspopup='dialog'andaria-expanded. DrawerClosehas no default label, so an icon-only button inside it needs anaria-label; the library ships no built-in strings.- The drawer should expose a clear panel purpose.
- Long panels should still read as one coherent overlay task rather than a disconnected page fragment.
Form Semantics
- Drawers often contain forms, but the form semantics belong to the controls inside the panel.
- Error states and required fields should remain visible even when the panel scrolls.
- Buttons in a
<form method="dialog">close the drawer throughonChangeOpen(false), and the submit button'svaluebecomes the dialog'sreturnValue. This also works withclosedBy='none'.
Accessibility Risks
- A drawer without
aria-labelledbyoraria-label. - Long content with poor scroll affordance.
- Nested overlays without retesting focus restoration.