Accessibility
How duck-primitives implement WAI-ARIA patterns and what you need to know.
Every primitive implements the correct WAI-ARIA pattern for its component type. This guide explains what you get for free and what you still need to provide.
What primitives handle automatically
Dialog / Alert Dialog
role="dialog"(orrole="alertdialog")aria-labelledbyconnected to Titlearia-describedbyconnected to Descriptionaria-modal="true"for modal dialogs- Focus trap: Tab cycles within the dialog
- Escape closes the dialog
aria-hiddenon content behind the dialog- Focus restoration to trigger on close
- Scroll lock on body
Popover / Tooltip / Hover Card
aria-expandedon triggeraria-controlslinking trigger to contentaria-haspopupon trigger- Focus management (auto-focus on open, restoration on close)
Menu / Context Menu / Menubar
role="menu",role="menuitem",role="menuitemcheckbox",role="menuitemradio"aria-checkedon checkbox and radio items- Arrow key navigation between items
- Type-ahead character search
- Submenu opening on ArrowRight
aria-disabledon disabled items
Progress
role="progressbar"aria-valuemin,aria-valuemax,aria-valuenowaria-valuetextwith human-readable label
What you need to provide
Primitives handle the hard parts, but these items are your responsibility. Missing them will break the accessible experience.
Always include Title and Description
Dialog and Alert Dialog warn in development if no Title is found. Always include them:
<Dialog.Content>
<Dialog.Title>Required for screen readers</Dialog.Title>
<Dialog.Description>Optional but recommended</Dialog.Description>
</Dialog.Content><Dialog.Content>
<Dialog.Title>Required for screen readers</Dialog.Title>
<Dialog.Description>Optional but recommended</Dialog.Description>
</Dialog.Content>If you want a visually hidden title, use CSS:
<Dialog.Title className="sr-only">Settings dialog</Dialog.Title><Dialog.Title className="sr-only">Settings dialog</Dialog.Title>Provide meaningful labels
Triggers should have descriptive text content or an aria-label:
// Good: text content
<Dialog.Trigger>Open settings</Dialog.Trigger>
// Good: aria-label for icon buttons
<Dialog.Trigger aria-label="Open settings">
<GearIcon />
</Dialog.Trigger>// Good: text content
<Dialog.Trigger>Open settings</Dialog.Trigger>
// Good: aria-label for icon buttons
<Dialog.Trigger aria-label="Open settings">
<GearIcon />
</Dialog.Trigger>Respect reduced motion
Wrap animations in prefers-reduced-motion queries so users who are sensitive to motion have a comfortable experience.
@media (prefers-reduced-motion: reduce) {
.overlay, .content {
animation: none !important;
}
}@media (prefers-reduced-motion: reduce) {
.overlay, .content {
animation: none !important;
}
}Keyboard navigation summary
| Component | Keys |
|---|---|
| Dialog | Escape close, Tab / Shift+Tab cycle focus |
| Popover | Escape close, Tab move focus |
| Tooltip | Escape close, focus-triggered |
| Menu | Arrow keys navigate, Enter / Space activate, Escape close |
| Menubar | ArrowLeft / ArrowRight switch menus, ArrowUp / ArrowDown navigate items |
| Progress | None (display-only) |