Accessibility
shadcn/ui gets its behaviour from Radix, which only runs in React. partialkit reaches the same place from two directions: browser primitives where the platform already implements the pattern, and an explicit ARIA layer where it does not.
Attributes you set yourself are never overwritten — every ARIA attribute is applied only when absent.
Where each piece comes from
Section titled “Where each piece comes from”| Concern | Source |
|---|---|
Modal semantics, focus trap, Esc, background inert, focus restore | <dialog>.showModal() |
| Menu top layer, light dismiss | popover API |
aria-labelledby / aria-describedby on dialogs | wired from .dialog-title / .dialog-description |
role="menu", role="menuitem", role="separator" | applied on mount and on every open |
aria-haspopup, aria-controls, aria-expanded on triggers | kept in sync on toggle |
data-state="open" | "closed" on overlays and triggers | the runtime |
data-disabled mirrored to aria-disabled, skipped by keyboard nav | dropdown menu runtime |
role="tab", role="tabpanel", aria-selected, roving tabindex | tabs runtime |
role="menubar" and a roving tab order across its triggers | menubar runtime |
aria-roledescription="carousel", and “n of m” on every slide | carousel runtime |
aria-describedby from a trigger to its tooltip | tooltip runtime |
Disclosure semantics on <details> / <summary> | the browser — accordion and collapsible add none |
Focus rings, :disabled, aria-invalid error state | component CSS, on native elements |
prefers-reduced-motion | transitions collapse to 1 ms |
Cross-engine gaps partialkit fills
Section titled “Cross-engine gaps partialkit fills”The platform is not uniform, and the test suite runs on Chromium, Firefox and WebKit to catch it.
- WebKit does not focus a button when you click it (a macOS convention), so
document.activeElementis not a reliable record of what opened a dialog. partialkit tracks the opener explicitly. - WebKit clears focus after
closeinstead of returning it. partialkit restores it, checking both during and after the event. Tabinside a menu would let the default action move focus on before the trigger is restored, so the runtime handles it directly.
Keyboard support
Section titled “Keyboard support”Dialog
Section titled “Dialog”| Key | Action |
|---|---|
| Esc | Closes and returns focus to whatever opened it |
| Tab | Cycles within the dialog; the rest of the page is inert |
Dropdown Menu
Section titled “Dropdown Menu”| Key | Action |
|---|---|
| ↓ / ↑ | Move between enabled items, wrapping at the ends |
| Home / End | First / last enabled item |
| a–z, 0–9 | Type-ahead to the first item starting with what you type |
| Esc | Closes and returns focus to the trigger |
| Tab | Closes and returns focus to the trigger |
| Enter / Space | Activates the item — it is a real <button> |
Disabled items carry aria-disabled="true" rather than disabled, so a screen reader still
announces them while arrow navigation skips them.
The same table applies to the context menu — it is the same panel, opened by right click instead of by a trigger.
| Key | Action |
|---|---|
| Tab | Reaches the selected tab, then the panel — one stop for the whole list |
| ← / → | Move between tabs, wrapping and skipping disabled ones |
| ↑ / ↓ | The same, when the list is vertical |
| Home / End | First / last tab |
| Enter / Space | Selects, under data-activation="manual" |
Popover and Tooltip
Section titled “Popover and Tooltip”| Key | Action |
|---|---|
| Esc | Closes either one; a popover returns focus to its trigger |
| Tab | Moves through a popover and out of it — it is not modal |
A tooltip never takes focus: it opens when its trigger is focused and describes it through
aria-describedby.
Accordion and Collapsible
Section titled “Accordion and Collapsible”| Key | Action |
|---|---|
| Enter / Space | Toggles the section |
| Tab | Reaches each trigger, then the revealed content |
Both are <details>/<summary>, so this comes from the browser rather than from partialkit.
What is still yours
Section titled “What is still yours”- Accessible names for icon-only controls. Use
aria-label, or.sr-onlytext inside the button. - A heading in every dialog.
.dialog-titlebecomes the accessible name. langon<html>.