The Drawer component renders a plain <div> with no role attribute and no automatic accessible name. Callers who pass aria-label or aria-labelledby via props are labeling an anonymous div — screen readers ignore that label because there is no role to attach it to. Screen readers do not announce entry into a dialog region when the drawer opens, and virtual-cursor users have no landmark or structural cue that they are in a bounded modal area.
The component already does the right things for containment: AriaIsolate hides the rest of the page from AT while open, and useFocusTrap constrains keyboard focus. The gap is role announcement and accessible naming.
Acceptance criteria
- The
Drawer root <div> has role="dialog" when rendered.
- The
Drawer root <div> has aria-modal="true" when behavior === 'modal' (the default).
- The
Drawer automatically receives an accessible name via aria-labelledby, following the same pattern used by Dialog:
- An auto-generated heading ID (via
useId) is created by Drawer and passed through a DrawerContext.
- A
DrawerHeading sub-component reads the ID from context and applies it as its own id, so aria-labelledby on the root element always references the visible heading text.
- A dev-mode warning is raised (matching the existing
Dialog pattern) if the drawer is open but no DrawerHeading is present inside it.
- When the
Drawer opens, focus moves to the heading rather than the drawer container, matching Dialog. Focusing the container draws a focus ring around the entire drawer (which looks bad) and is a weaker announcement; focusing the heading fixes both. The DrawerHeading takes tabIndex={-1} and a ref carried through DrawerContext (like DialogContext.headingRef), which Drawer passes to useFocusTrap as its initialFocusElement.
References
The
Drawercomponent renders a plain<div>with noroleattribute and no automatic accessible name. Callers who passaria-labeloraria-labelledbyvia props are labeling an anonymous div — screen readers ignore that label because there is no role to attach it to. Screen readers do not announce entry into a dialog region when the drawer opens, and virtual-cursor users have no landmark or structural cue that they are in a bounded modal area.The component already does the right things for containment:
AriaIsolatehides the rest of the page from AT while open, anduseFocusTrapconstrains keyboard focus. The gap is role announcement and accessible naming.Acceptance criteria
Drawerroot<div>hasrole="dialog"when rendered.Drawerroot<div>hasaria-modal="true"whenbehavior === 'modal'(the default).Drawerautomatically receives an accessible name viaaria-labelledby, following the same pattern used byDialog:useId) is created byDrawerand passed through aDrawerContext.DrawerHeadingsub-component reads the ID from context and applies it as its ownid, soaria-labelledbyon the root element always references the visible heading text.Dialogpattern) if the drawer is open but noDrawerHeadingis present inside it.Draweropens, focus moves to the heading rather than the drawer container, matchingDialog. Focusing the container draws a focus ring around the entire drawer (which looks bad) and is a weaker announcement; focusing the heading fixes both. TheDrawerHeadingtakestabIndex={-1}and a ref carried throughDrawerContext(likeDialogContext.headingRef), whichDrawerpasses touseFocusTrapas itsinitialFocusElement.References
Dialogcomponent implementation of the same pattern:packages/react/src/components/Dialog/index.tsxDialogin Associate Dialog Header with Dialog Title via aria-labelledby #1259