Prepared Docs

Prepared Docs

⌘K

    Loading navigation…

User

  1. Components
  2. Drawer
  3. Accessibility

Drawer

Accessibility

Drawer from @prepared911/ui-drawer is a sliding panel rendered in a portal. It is not a modal dialog: there is no backdrop, no default focus trap, and the rest of the page remains in the tab order unless you add behavior yourself. Treat accessibility as a shared contract between the component (what it guarantees) and your app (labels, focus, and announcements).

What the component handles

  • Escape: While the drawer is open, pressing Escape closes it by default (a document-level listener via useEscapeClose). Pass closeOnEscape={false} and/or onEscapeKeyDown when a parent owns layered Escape (for example a popover above the drawer). You do not need to wire onKeyDown for Escape on the drawer root.
  • Click outside: When clickOutsideToClose is true, a click outside the drawer panel closes it. Default is false; enable it only when that dismissal model matches your UX.
  • Open / close callbacks: onOpen and onClose run when the drawer transitions between closed and open so you can coordinate focus or analytics.
  • Slide animation: Width animates via Motion (AnimatePresence) with a 300 ms ease-in-out transition; the package does not currently read prefers-reduced-motion for that animation.
  • Resize handle: when resizable, the drag handle exposes aria-label="Drag to resize" and a tooltip; resize remains pointer-driven.
  • DrawerHeader close control: When passing onClose to DrawerHeader, the dismiss control is an IconButton with a localized accessible name (via closeTitle or the default close string).
  • Portal: the drawer mounts into DrawerPortal's target or document.body, which affects page landmarks unless you wrap content with explicit roles and labels (see below).

What you must provide

  • Accessible name for the panel: The drawer shell does not set aria-label, aria-labelledby, or role on the outer container. Wrap primary content in a landmark or region and associate a visible title, or set aria-label on a wrapper Box when there is no visible heading.
  • Focus on open: Move focus to the first logical control inside the drawer when it opens (often the first button or the panel wrapper with tabIndex={-1} for programmatic focus), then manage order inside your content.
  • Focus on close: Return focus to the element that opened the drawer (trigger button or link) when it closes, so keyboard and screen reader users do not lose context.
  • Modal-like flows: If the drawer must behave like a dialog (only the drawer is operable while open), you need a focus trap and often aria-modal="true"; the package does not supply those; use a dedicated dialog pattern or add a trap library and test thoroughly.
  • Live regions: If open/close is easy to miss audibly, announce state with a polite aria-live region or an equivalent pattern; the drawer does not announce by itself.
  • Heading order: DrawerHeader renders a prominent title with CoreText; heading level is not a prop today. Ensure the surrounding page's heading outline still makes sense (there is an internal note in source about semantic order for drawer titles vs page h2/h3).

Keyboard behavior

  • Escape (esc) closes an open drawer while closeOnEscape is true (the default). Call event.preventDefault() from onEscapeKeyDown to keep the drawer open.
  • Tab moves through focusable content in document order; because there is no built-in trap, focus can leave the drawer and move to the page behind it unless you constrain it.
  • Enter / Space activate buttons and other controls inside the drawer, including the header dismiss control when onClose is provided.
  • Resize is intended for pointer users; keyboard-only resizing is not provided by the component.

Focus management

Opening should send focus into the drawer; closing should restore focus to the trigger. Use the onOpen and onClose callbacks provided by Drawer to coordinate these moves. Defer focus calls that depend on animation until after the panel is present in the DOM (for example requestAnimationFrame or a short delay aligned with the 300 ms motion duration).

If the first focusable control is not the right starting point, focus a wrapper with tabIndex={-1} and an aria-label that matches the drawer purpose, then move to inner controls as needed.

Focus on open

Use onOpen to move focus into the drawer once the animation completes:

Focus restoration on close

Use a ref to capture the trigger element and restore focus when the drawer closes:

Semantics and roles

Choose a role that matches how the drawer is used:

  • role="complementary" when the panel supplements the main content (side detail, filters, related info).
  • role="region" with a required accessible name when the panel is a distinct section but not strictly "complementary."

Pair with aria-labelledby pointing at a stable id for the visible title, or aria-label when the title is not visible. DrawerHeader does not set an id on the title element, so you need to provide one yourself when using aria-labelledby.

Using aria-label

The simplest approach when the label matches a known string:

Using aria-labelledby with a visible title

When the drawer title is dynamic or you prefer to reference it directly, add your own id to a visible element:

Avoid relying on aria-hidden on the whole panel to mean "closed"—the drawer unmounts its open branch when closed; drive visibility from open state and focus, not by hiding an inert duplicate tree.

Screen reader announcements

Announce meaningful transitions when users cannot infer them from focus alone. Use a visually hidden live region alongside the drawer:

Keep announcements short and non-interrupting (aria-live="polite") unless there is a safety-critical reason for assertive output.

Reduced motion

The drawer's slide animation does not currently consult prefers-reduced-motion. If you must respect system settings before the library adds support, you can override the transition duration in your own SCSS using the drawer class:

Prefer reducing duration rather than removing visibility of open state. Avoid relying on motion for essential feedback.

Touch targets and responsive behavior

  • DrawerHeader uses a small IconButton for dismiss; touch targets should meet your minimum size expectations; the stylesheet enlarges the chevron icon for visibility.
  • resizable adds a wide hit target along the inner edge; still primarily pointer-oriented.
  • Use responsive width / minWidth / maxWidth so narrow viewports do not clip content or trap horizontal overflow; pair with DrawerContent (scrollable body) for long content.

DataTable and drawers

When a right-anchored drawer sits beside a DataTable, pass horizontalScrollSpacing with the open drawer's width in pixels so the table's horizontal scroll region gains enough padding that columns are not permanently hidden behind the drawer. That preserves keyboard and pointer access to columns that would otherwise sit under the panel.

Use useDrawerStore to read the current width:

Known caveats

  • No default focus trap: The page behind the drawer stays interactive unless you add trapping.
  • No aria-modal: Not set by the component; add it yourself if your UX is dialog-like.
  • Portal: Rendering into body can separate the drawer from main landmark structure; explicit role + labeling on your wrapper mitigates confusion in rotor/landmark navigation.
  • Heading level: Not configurable on DrawerHeader; validate against each page's outline.
  • Motion: No built-in reduced-motion branch for the slide animation today.

Previous

Drawer / API and Development

Next

Empty State / Usage

On this page

What the component handles
What you must provide
Keyboard behavior
Focus management
Focus on open
Focus restoration on close
Semantics and roles
Using aria-label
Using aria-labelledby with a visible title
Screen reader announcements
Reduced motion
Touch targets and responsive behavior
DataTable and drawers
Known caveats