Skip to content
Alpha

Hololink UI is under active development. Components, APIs and these docs may change without notice.

Accessibility

Hololink UI targets WCAG 2.2 level AA. The components do the work that belongs in a component library. You do the work that depends on your content and your page.

This page explains the commitment. For key-by-key behaviour, see the Keyboard reference. Each component page also has its own Accessibility section.

  • Native semantics first. Components render native elements where one exists: <button>, <a>, <input>, <select>, <textarea>, <details>, <dialog>, <table>, <hr>, <nav>. Where there is no native element (tabs, menu, radio group, switch), they set the matching ARIA role and state, and manage keyboard behaviour.
  • Visible focus. Interactive elements show a focus ring when focused with the keyboard. The ring uses --s-color-border-interactive and meets 3:1 against the surface and page background.
  • Visible control boundaries. Form controls, switch tracks and slider tracks use --s-color-border-control, which meets 3:1 against the surface and background. Decorative borders (border-subtle, border-strong) are never the only boundary of a control.
  • Reduced motion. Transitions and animations (overlay fades, spinner rotation, skeleton shimmer, carousel sliding, switch thumb) only run under prefers-reduced-motion: no-preference.
  • Contrast-tested token pairs. Semantic text and status colours meet 4.5:1 on the surfaces they are meant for, in both the light and dark themes. Control and focus colours meet 3:1.
  • No colour-only meaning. The design rules forbid it. Alerts and toasts add an icon to the status colour, a switch shows state by thumb position as well as colour, and badges and tags rely on the text you write.
  • Overlays behave like overlays. Dialog and Drawer use showModal(), which traps focus, makes the page inert and closes on Esc. They return focus to the element that opened them. Popover and Menu use the Popover API for light dismiss.
  • Form participation. Form controls that are custom elements (Checkbox, Radio, Select, Slider, Switch, Text Field, Textarea) are form-associated through ElementInternals, so they submit with the form and report validity.

Components cannot know your content or your page. You provide:

  • Accessible names. Give every form control a label. Give every Icon Button a meaningful label. Give landmarks and groups a name where there is more than one (carousels, dialogs, tab lists, radio groups).
  • Headings and page structure. Use one <h1>, keep heading levels in order, and use landmarks (<main>, <nav>, <header>). Note that some components render fixed heading levels (for example Accordion uses h3 and Dialog uses h2).
  • Content. Write link text that makes sense out of context, error messages that explain how to fix the problem, and alternative text for images.
  • Focus management in your app. For example, moving focus after removing a Tag, dismissing an Alert, or changing page in Pagination.
  • Announcements. Components do not announce changes you make elsewhere. Use a live region when something changes that users would otherwise miss.
  • Overrides. If you override --s-* tokens or --holo-* hooks, you are responsible for keeping contrast and focus visibility.
  • Testing in your own context. Real content, real themes and real assistive technology can behave differently from a demo.

These are current behaviours worth knowing about. They are also noted on the relevant component pages.

  • Accordion headers are ordinary tab stops, with no arrow-key navigation between them.
  • Carousel autoplay pauses on hover and focus but has no pause button.
  • Form Field names its control with a native <label for>, which only labels native controls and form-associated custom elements. A custom element that renders its input in its own shadow root, such as Text Field, needs its own label.
  • Tooltip triggers must be in the same document or shadow root as the tooltip, because the aria-describedby id reference cannot cross a shadow-root boundary.
  • Popover sets aria-expanded on its trigger but not aria-controls, because the panel is inside its shadow root.
  • Button does not forward ARIA attributes such as aria-expanded or aria-describedby to its inner <button>. Use a native <button> as the trigger for Popover and Tooltip.
  • Toast and the close buttons of Dialog and Drawer use fixed English labels (“Dismiss notification”, “Close dialog”, “Close drawer”). Alert, Tag, Pagination and Breadcrumb have label props.
  • Storybook with @storybook/addon-a11y. The addon is registered globally and runs axe on every story using the WCAG rule tags wcag2a, wcag2aa, wcag21a, wcag21aa and wcag22aa. Stories cover variants and states, and are checked in both themes.
  • Token contrast unit tests. packages/ui/src/styles/tokens.unit.test.ts parses tokens.css, resolves var() and light-dark(), and asserts 4.5:1 for text pairs and 3:1 for control boundaries and focus colours, in light and dark. It runs in CI with pnpm test.
  • Keyboard passes. Each component gets a manual keyboard pass before merge: tab order, activation keys, arrow-key behaviour, Esc and focus return.
  • Component tests. Component tests check the roles, states and attributes that components set.

Automated tools find only part of the problems that matter. They do not replace testing with a keyboard and a screen reader.