Skip to content

Accessibility

Read this to see which accessibility work the components do for you, which parts stay your app’s job, and how the test suite verifies both.

View source View as Markdown

Roles, keyboard maps and focus behaviour are the same in every product, so Mirror ships them and there is no prop to switch them off.

What Mirror guarantees

Roles and ARIA state

Every part renders the role its pattern requires and keeps the matching state attributes in sync. Checkbox is role="checkbox" with aria-checked as true, false or mixed; Tabs is a tablist of tabs controlling panels; and Select is a button with aria-haspopup="listbox" driving a listbox.

Relationships

Field wires the label, the description and the error to the control. Field.Label gets for, the control gets aria-describedby listing descriptions then errors, and Field.Error renders as role="alert".

<template>
  <Field.Root :options="{ name: 'email' }">
    <Field.Label>Email</Field.Label>
    <Input v-model="email" type="email" />
    <Field.Error>Enter an email address.</Field.Error>
  </Field.Root>
</template>

Keyboard maps

Each interactive component implements the keyboard map from its ARIA pattern, including the parts that are easy to miss: roving focus and one tab stop for Radio and Tabs, PageUp and PageDown on Slider, typeahead on Select, and Enter blocked on Checkbox so the form owns it. The map is tabulated on every component page.

Focus management

Popups trap focus while modal and return it to the trigger on close. A disabled non-native control keeps its tabindex and reports aria-disabled rather than leaving the tab order, so a screen-reader user can still reach it and hear that it is disabled.

The focus ring

Both the colour and the width of focus-ring come from tokens rather than from a utility, so an app cannot style the ring down to nothing while still using the class.

What stays your job

Accessible names

Mirror never invents one, so a Toggle.Root whose content is a glyph, a Progress.Root, and any control outside a Field all need an aria-label or an aria-labelledby from you.

<template>
  <Toggle.Root aria-label="Bold" v-model="bold">
    <icon-bold />
  </Toggle.Root>
</template>

Colour contrast

The on-* colours are paired with their fills in the token tree, so text on a fill stays legible. Any other pairing is yours to check, and so is every colour your app defines outside the tree.

Visible focus

The parts report focus state but draw nothing, so apply focus-visible:focus-ring to anything a user can reach.

Motion preferences

Timing lives in your CSS, so honouring prefers-reduced-motion is a media query you write.

@media (prefers-reduced-motion: reduce) {
  .mirror-select-popup-enter-active,
  .mirror-select-popup-leave-active {
    transition: none;
  }
}

Reading order

asChild and as let you render anything, so keep the DOM order the reading order and keep a control’s label adjacent to it.

How it is verified

Every component in packages/vue has a dedicated ARIA test file and, where it takes keyboard input, a dedicated keyboard test file. They assert the roles, the state attributes and the key handling described on that component’s page, so the docs and the suite move together.

packages/vue/src/components/Checkbox/tests/
  Checkbox.aria.test.ts
  Checkbox.keyboard.test.ts

Errors are branded and thrown rather than swallowed, so a part assembled in a way that would break its ARIA relationships fails loudly at render.

Further reading

  • Form integration: Field, validation and native constraint validation.
  • Animation: where timing lives, and how to switch it off.