Every component pattern this library covers, cross-referenced against the standardization efforts and the component libraries a reader may already know — Open UI, native HTML, Material Web, Radix, Bootstrap, and the WAI-ARIA Authoring Practices — with the accessibility requirements for each pattern. The This library column says what to reach for here; the per-component pages linked from Components carry the runnable demos.
| Column | Content |
|---|---|
| Component | Canonical pattern name |
| Open UI | Open UI standardization status + link |
| HTML | Native HTML element(s) |
| Material Web | Material Web component |
| Radix | Radix UI primitive |
| Bootstrap | Bootstrap 5.3 component |
| APG | WAI-ARIA Authoring Practices pattern |
| This library | What to reach for in public/ui/, and how far it is built |
| Marker | Meaning |
|---|---|
| done | Implemented — the link points to its demo page |
| todo | Documented but not yet implemented in public/ui/ |
| plain | Standard HTML/CSS, no custom component needed |
| Label | Meaning |
|---|---|
| Graduated | Shipped in browsers or spec-ready |
| Active | Active proposal with explainer |
| Research | Component research / cross-library comparison |
| Component | Open UI | HTML | Material Web | Radix | Bootstrap | APG | This library |
|---|---|---|---|---|---|---|---|
| Button | Research | <button> | md-button | — | Buttons | Button | plain .btn classes (ui-button) |
| Toggle | Press Button | <button aria-pressed> | md-icon-button toggle | Toggle | — | Button (toggle) | plain [aria-pressed] CSS |
| Toggle Group | — | Group of <button aria-pressed> | — | Toggle Group | Button group | — | todo — single-select is covered by Radio group |
| Segmented button | Not proposed | <fieldset role="radiogroup"> + <input type="radio"> | Segmented button (deprecated → connected button group) | Toggle Group (type="single") | Button group + .btn-check | Radio Group — no segmented pattern of its own | done data-look="segmented" (demo) |
| Link | Research | <a> | — | — | — | Link | plain |
role="button". Activate with Enter or Space. For toggle buttons, add aria-pressed. Screen readers announce "button" automatically for <button> elements; avoid <div> with role="button" when possible.aria-pressed="true|false" to convey state. Screen readers announce "pressed" / "not pressed". For icon-only toggles, provide aria-label.<button aria-pressed> elements inside a container with role="group" and aria-label, and implement keyboard navigation for the group. Do not put buttons with aria-pressed inside role="radiogroup". Radio groups require options with role="radio" and aria-checked; using the wrong roles prevents screen readers from announcing the selected state.role="link" on <a href>. Activate with Enter (not Space). Link text must be descriptive — screen readers list links out of context. Avoid "click here".
A segmented button (Apple: segmented control; IBM Carbon:
content switcher) is a linear row of two-to-five mutually exclusive options that reads as
one control. Every system builds it out of the same parts and differs mainly in
how the selected segment is painted — this is the evidence behind
data-look="segmented" in Radio group, drawn from
each system's own spec.
| System | Container | Selected segment | Sizing |
|---|---|---|---|
| Material 3 | "A toggle button has a shared stroked container" — the frame and the
dividers between segments both take the Outline role; "each segment is clearly
divided"; fully rounded corners by default |
Secondary container fill + On secondary container label — a
tonal fill, never the primary role — plus a checkmark:
"the icon label is replaced by the checkmark icon when the segment is selected" |
Height 40dp, outline 1dp, label padding min 12dp, target 48dp; segment width = container width / total segments; 2–5 segments; don't let segments wrap to a new line; don't span the full width of a large pane |
| Material 3 Expressive | Segmented buttons are deprecated in favour of the connected button group, which "overrides the individual button's shape to make them visually more belong to a group with 2dp spacing, 8dp inner corners, and fully rounded outer corners" | Filled button among outlined ones | Also 2–5 segments; use different rounding for the group's outer corners and the inner corners |
| Apple HIG | "A linear set of two or more segments, each of which functions as a button"; "segmented controls preserve their grouping regardless of the view size… This grouping can also help people understand at a glance which controls are currently selected" | A neutral raised thumb inside a recessed track (not a tinted or accent fill) | "All segments are usually equal in width"; "no more than about five to seven segments in a wide interface"; "prefer using either text or images — not a mix of both"; nouns or noun phrases, title case |
| IBM Carbon | One bar of equal-width tabs; "only one content tab can be selected at a time and there should always be one selected" | Inverse layer fill | Small 32px / medium 40px / large 48px; labels two-to-three words |
| Bootstrap 5.3 | A button group of .btn-styled <label>s |
The .btn-* variant's own filled state |
Hides the real control with .btn-check on
<input type="radio"> — i.e. the same
native-radio-plus-CSS construction as ours |
| Open UI | No proposal and no research page. "Segmented Control" appears in the component-name matrix only as one library's name for the pattern, so there is no standardization status to track — unlike Switch or Popover. | ||
What every system agrees on, and what this library therefore does:
<legend> cuts a hole in a border set on the <fieldset>).1fr columns here, which sizes
every segment to the widest one's content and cannot wrap.--brand tint, the token-set equivalent of Material's Secondary container.| Component | Open UI | HTML | Material Web | Radix | Bootstrap | APG | This library |
|---|---|---|---|---|---|---|---|
| Checkbox | Research | <input type="checkbox"> | md-checkbox | Checkbox | Checks | Checkbox | plain |
| Radio Group | Research | <fieldset> + <input type="radio"> | md-radio | Radio Group | Radios | Radio Group | done <fieldset role="radiogroup"> (demo) — CSS-only; three looks: unstyled, data-look="segmented", .option-card |
| Switch | Active | <input type="checkbox" role="switch"> | md-switch | Switch | Switches | Switch | done [role="switch"] (demo) |
| Select | Graduated | <select> | md-select | Select | Select | Listbox | done <x-select> (demo) |
| Combobox | Active | <input> + <datalist> | — | — | — | Combobox | done <ui-combobox> (demo) — incl. tag-input modes: chips (single token), multiple |
| Text Input | Research | <input> | md-text-field | — | Form control | — | plain |
| Input Group | — | <div role="group"> / <label> / <fieldset> + .prefix/.suffix | md-text-field prefix/suffix | — | Input group | — | done .input-group (demo) |
| Textarea | — | <textarea> | md-text-field | — | Form control | — | plain |
| Number Input | Research | <input type="number"> | — | — | — | Spinbutton | todo |
| Slider | Research | <input type="range"> | md-slider | Slider | Range | Slider | plain |
| File Input | Research | <input type="file"> | — | — | File input | — | plain |
| Calendar | Research | <input type="date"> | — | — | — | Spinbutton / Grid | todo (low priority) |
<input type="checkbox">. Space toggles. For mixed/indeterminate state, set aria-checked="mixed". Always pair with <label>. Screen readers announce "checkbox, checked/not checked".<fieldset> and declare
role="radiogroup" (a bare fieldset is role="group", and radios inside
do not upgrade it); name it with a <legend> or aria-label. Arrow
keys move between options and select — selection follows focus, which is what separates
a radio group from a listbox. Only the selected radio is in the tab order (roving tabindex), so
the group is one Tab stop. Home/End do nothing here, unlike in a listbox
or toolbar. Screen readers announce group label + "radio button, X of Y". Hiding the radio to
paint a custom look is fine with opacity, never with display: none —
see Radio group § Accessibility.role="switch" on a checkbox. Screen readers announce "switch, on/off" (JAWS) or "toggle, checked" (NVDA). Visually distinguish from checkbox to avoid confusion.role="listbox" on the popup, role="option" on items. Arrow keys navigate, Enter/Space select, Escape closes. Type-ahead search expected. aria-activedescendant or roving tabindex for focus.role="combobox" on the input, aria-expanded, aria-controls pointing to listbox, aria-activedescendant for virtual focus. Arrow keys navigate suggestions, Escape clears/closes.role="slider" implicit on <input type="range">. aria-valuemin, aria-valuemax, aria-valuenow, optional aria-valuetext for human-readable value. Arrow keys adjust value. For multi-thumb, see Slider (Multi-Thumb).<input type="date"> provides built-in a11y. Custom implementations must follow the grid pattern with proper role="grid" and announce date changes.| Component | Open UI | HTML | Material Web | Radix | Bootstrap | APG | This library |
|---|---|---|---|---|---|---|---|
| Tabs | Research | — | md-tabs | Tabs | Navs & tabs | Tabs | done <ui-tab> |
| Breadcrumb | Research | <nav> + <ol> | — | — | Breadcrumb | Breadcrumb | todo |
| Navigation | Active | <nav> | — | Navigation Menu | Navbar | Landmarks | todo |
| Menubar | — | — | — | Menubar | — | Menu and Menubar | todo |
| Toolbar | Active | — | — | Toolbar | — | Toolbar | done <ui-toolbar> |
| Pagination | — | <nav> + buttons | — | — | Pagination | — | todo |
role="tablist" on container, role="tab" on each tab, role="tabpanel" on panels. Arrow keys move between tabs. aria-selected="true" on active tab. Only active tab in tab order (tabindex="0"), others tabindex="-1". JAWS/NVDA announce "tab, 1 of N, selected".<nav aria-label="Breadcrumb">. Use <ol> for ordered list semantics. Current page gets aria-current="page". Screen readers announce the navigation landmark and list structure.<nav> with a descriptive aria-label (e.g., "Main navigation"). Multiple <nav> elements on a page must have distinct labels. Screen readers list navigation landmarks.role="menubar" with role="menuitem" children. Arrow Left/Right moves between top-level items, Up/Down opens submenus. Complex pattern — only use for app-style menubars, not site navigation.role="toolbar" on container with aria-label. Arrow Left/Right moves between tools (roving tabindex). Tab moves in/out of the toolbar as a single tab stop. Group related controls with aria-label on the toolbar.<nav aria-label="Pagination">. Current page gets aria-current="page". Use <a> or <button> for page links, not <span>.| Component | Open UI | HTML | Material Web | Radix | Bootstrap | APG | This library |
|---|---|---|---|---|---|---|---|
| Dialog (Modal) | Research | <dialog> | md-dialog | Dialog | Modal | Dialog | done <ui-dialog> (demo) |
| Alert Dialog | — | <dialog> | — | Alert Dialog | Modal | Alert Dialog | done <ui-dialog> (confirm variant) |
| Response Dialog | — | <dialog> | — | — | — | Dialog | done response_dialog() |
| Popover | Graduated | [popover] | — | Popover | Popovers | — | done <ui-popover> (demo) |
| Tooltip | Research | [popover=hint] | — | Tooltip | Tooltips | Tooltip | done tooltip() (demo) |
| Context Menu | Active | — | md-menu | Context Menu | — | Menu | done <ui-menu trigger="contextmenu"> (demo) |
| Dropdown Menu | Active | — | md-menu | Dropdown Menu | Dropdowns | Menu Button | done <ui-menu> (demo) |
| Toast | Research | <output> | — | Toast | Toasts | Alert | done toast() (demo) |
| Hover Card | — | [popover=hint] | — | Hover Card | — | — | todo |
| Drawer / Sheet | — | <dialog> | — | — | Offcanvas | Dialog | done <ui-dialog> (slide variant) |
role="dialog" implicit on <dialog>. Must have aria-labelledby (heading) or aria-label. Focus moves into dialog on open, trapped inside. Escape closes. On close, focus returns to the trigger element. JAWS announces "dialog, [title]" on open.role="alertdialog". Same as dialog but announces urgently. Focus must move to the least destructive action button (e.g., "Cancel"). Cannot be dismissed by clicking backdrop — requires explicit user action.window.prompt(). Uses standard role="dialog" by default — a form input is not an urgent interruption — and takes alertdialog: true for a destructive confirmation, which uses role="alertdialog" and autofocus on Cancel (Dialog § 11). The caller supplies the body (typically a <form>); a first focusable element with autofocus lets the user type immediately. Form fields must have associated <label> elements (or aria-label). Escape, backdrop, [x], and Cancel all dismiss (return null). Returns a promise resolving to FormData from the first <form> in the body on confirm, or null on cancel. See response_dialog() in lib/dialog.mjs.role="dialog" if interactive, or no special role for simple info panels. Close on Escape and light-dismiss (click outside). Manage focus: move to popover on open, return to trigger on close.role="tooltip" on the popup element. Connect via aria-describedby on the trigger. Show on mouseenter/focusin, hide on mouseleave/focusout/Escape. Must not contain interactive content. JAWS reads tooltip text as part of the element's description.role="menu" on container, role="menuitem" on items. Arrow Up/Down navigates items, Enter activates, Escape closes. Type-ahead jumps to matching items. Context menus have two openers: right-click (at the pointer) and Shift+F10 / the Menu key (anchored to the focused element). A pointer-only context menu is unreachable for a keyboard or screen-reader user.<output> with role="status" (polite) for info, role="alert" (assertive) for errors. Do not move focus to toasts. Auto-dismiss after ~4–6s. Must be pausable on hover for WCAG 2.2.2 (Pause, Stop, Hide). Stack multiple toasts visually.prefers-reduced-motion. JAWS/NVDA treat it as a modal dialog.| Component | Open UI | HTML | Material Web | Radix | Bootstrap | APG | This library |
|---|---|---|---|---|---|---|---|
| Callout (inline alert) | — | — | — | — (Radix has no inline alert; its Alert Dialog is an interruption) | Alerts | — (not a live region; see Alert for the announced case) | plain .callout (demo) |
| Notification List | — | — | — | Alert (persistent variant) | Alerts | Alert | done <ui-notifications> (demo) |
| Status Bar | — | <output> / [role="status"] | — | — | — | Status role (ARIA) | done <ui-statusbar> (demo) |
role="alert" would announce it on every re-render of the surrounding view, and the message is already visible next to its subject. Colour reinforces the message and never carries it alone. A message about the page belongs in the Status Bar or a toast instead.aria-haspopup="dialog" + aria-expanded and its aria-label includes the current item count so JAWS/NVDA announce "Warnings, 2" on focus. The visual count badge is aria-hidden. The popover panel has role="dialog" + aria-label; the inner list has explicit role="list" (preserves semantics when list-style: none is applied); each item has role="listitem" and decorative icons are aria-hidden. Use toast() for transient announcements — a notification list is for persistent state.role="status" (implicit aria-live="polite" + aria-atomic="true"), so changes are announced politely without stealing focus — MDN names "status bars" as the canonical use case for the role. The role lives on the inner region, not the host, so the count badge wrapper isn't pulled into the announcement via aria-atomic. The expand button (visible only when there's more than one active item) has an aria-label from the host's label attribute; the count badge and decorative icons are aria-hidden. Roles set at construction — never flipped at runtime — so SR live-region behavior stays reliable. Errors that need an assertive announcement are routed through toast(), not via a runtime role swap. References: WCAG 2.2 §4.1.3 Status Messages, Technique ARIA22.| Component | Open UI | HTML | Material Web | Radix | Bootstrap | APG | This library |
|---|---|---|---|---|---|---|---|
| Accordion | Graduated | <details name="..."> | — | Accordion | Accordion | Accordion | done — accordion() decorator (public/ui/lib/accordion.mjs, demo) |
| Collapsible | — | <details> / <summary> | — | Collapsible | Collapse | Disclosure | done — styled .ui-disclosure (zero-JS) + accordion() (demo) |
| Tree View | — | — | — | — | — | Tree View | done — tree() decorator (public/ui/lib/tree.mjs, demo) |
<details name="group"> provides exclusive accordion behavior (Baseline 2024). <summary> is the trigger — implicit role="button", activates with Enter/Space. Screen readers announce "expanded/collapsed", so don't add aria-expanded to a native <summary> (it fights the native state). The accordion() decorator (public/ui/lib/accordion.mjs) wraps each panel in a role="region" labelled by its summary (aria-labelledby) and wires aria-controls; per APG, regions are dropped past ~6 panels ({ region: false }).<details> / <summary> provides built-in disclosure semantics; the styled .ui-disclosure works with zero JS. Screen readers announce "expanded/collapsed". accordion() adds the panel region/labelledby wiring, exclusive name, and lazy load() (polite live region + aria-busy, error reverts the open). The chevron is a decorative CSS ::before; the open animation (::details-content + interpolate-size) is a progressive enhancement disabled under prefers-reduced-motion.tree() decorator (public/ui/lib/tree.mjs), which progressively enhances a nested <ul>/<li>: role="tree"/treeitem/group, aria-level/aria-setsize/aria-posinset, roving tabindex, Up/Down between visible rows, Right expands / enters children, Left collapses / moves to parent, aria-expanded on parent nodes, single-select via aria-selected, plus optional lazy children via a load() promise (announced through a live region). No other system in the row above offers a tree primitive, so the APG pattern is the only reference for it.| Component | Open UI | HTML | Material Web | Radix | Bootstrap | APG | This library | |
|---|---|---|---|---|---|---|---|---|
| Card | Research | <article> / <section> | — | — | Card | — | plain <article> | |
| Table | Research | <table> | — | — | Tables | Table / Grid | plain | |
| Badge | Research | <span> | — | — | Badge | — | plain .badge classes (ui-badge) | |
| Icon | Research | — | md-icon | — | — | — | done <ui-icon> (demo) — loading spinner: <ui-icon name="spinner" class="spin"> (Loading & spinners) | |
| Progress | — | <progress> | md-progress | — | Progress | — | plain | |
| Meter | — | <meter> | — | — | — | Meter | todo | |
| Avatar | Research | <img> | — | Avatar | — | — | todo | |
| Skeleton | Research | — | — | — | Placeholders | — | todo | |
| Carousel | Research | CSS scroll-snap | — | — | Carousel | Carousel | todo | |
| Tag / Chip | Research | <span> | md-chip | — | — | — | plain static pill = .badge; interactive removable chips = done `<ui-combobox chips\ | multiple>` (demo) |
| Separator | — | <hr> | md-divider | — | — | — | todo | |
| Image | Research | <img> / <picture> | — | — | — | — | plain | |
| List | Research | <ul> / <ol> | md-list | — | List group | Feed | plain |
<article> for self-contained content (screen readers list articles). If the entire card is clickable, the click target must be the inner <a> or <button>, not the card wrapper — otherwise screen readers announce the entire card text as the link label.role="table" (implicit on <table>) for static data; use role="grid" for interactive tables with cell-level focus. <caption> or aria-label for table name. <th scope="col|row"> for headers. Screen readers announce row/column position when navigating.aria-label on the parent or include visually-hidden text.aria-hidden="true". Meaningful icons: provide aria-label or visually-hidden text. Icon buttons must always have an accessible name.<progress> has implicit role="progressbar". Add aria-label for context (e.g., "Upload progress"). For indeterminate progress, omit the value attribute. JAWS/NVDA announce percentage.<meter> has implicit role. Use aria-label for context. low, high, optimum attributes convey thresholds. Screen reader support varies — consider adding visible text alongside.role="region" with aria-label and aria-roledescription="carousel". Slides: role="group" with aria-roledescription="slide" and aria-label="N of M". Auto-rotation must have a pause button (WCAG 2.2.2). Prev/Next buttons must be keyboard accessible.aria-busy="true" on the container while loading. Use aria-hidden="true" on skeleton placeholders. When content loads, remove aria-busy so screen readers re-announce the region.| Component | Open UI | HTML | Material Web | Radix | Bootstrap | APG | This library |
|---|---|---|---|---|---|---|---|
| Scroll Area | — | overflow: auto | — | Scroll Area | — | — | plain CSS |
| Grid | — | CSS Grid | — | — | Grid | Grid | todo |
| Landmarks | — | <header>, <nav>, <main>, <footer>, <aside> | — | — | — | Landmarks | plain |
| Sidebar | — | CSS layout | — | — | Offcanvas | — | done layout-sidebar.html — hamburger + modal dialog |
| Side panel (collapsible) | — | <aside> + CSS | — | Collapsible (vertical only) | Offcanvas (modal) | Disclosure | done side_panel() (demo) — non-modal, with a handle attached to its edge |
| Navigation rail (icon strip a panel collapses to) | — | <aside> + CSS | Navigation rail | — | — | Toolbar of Disclosure buttons | done side_panel() with --sp-rail + <ui-toolbar panel-rail> (demo) — several views in one collapsible panel; shadcn's collapsible="icon" sidebar is the same shape |
| Resizable | — | CSS resize | — | — | — | Window Splitter | todo |
| View (swap) | — | — | — | — | — | — | done <ui-outlet> (demo) |
tabindex="0" if the area is not focusable by default. Screen readers handle scroll containers natively.<header> = role="banner", <nav> = role="navigation", <main> = role="main", <footer> = role="contentinfo", <aside> = role="complementary". Multiple instances of <nav> or <aside> need distinct aria-label values.role="separator" with aria-valuenow (panel size as percentage). Arrow keys resize. aria-label describes what is being resized (e.g., "Resize sidebar").aria-expanded + aria-controls, a constant visually hidden name, and the rail is one Tab stop with Up/Down and Home/End. Opening leaves focus on the button; the rail is never inert. Buttons are 48 × 48 CSS px (WCAG 2.5.8 / 2.5.5), the icon and open marker clear 3:1 (1.4.11), and the open state survives forced colors as an outline.<button> with aria-expanded + aria-controls (Disclosure), and its accessible name stays constant across states — aria-expanded is what conveys the state. A collapsed panel's contents get inert so Tab skips them, marked on the children rather than the panel: inert is inherited with no way to opt back in, so applying it to the panel would also disable the handle and prevent reopening. Non-modal — focus is never trapped and Escape dismisses nothing.Consulted where a pattern's look carries meaning and the APG says nothing about it — the segmented-button section above is built from these.