Accessibility

Vy UI components ship with native Lynx accessibility built in — what you get for free and the one thing you add.

Vy UI components are accessible by default. Roles, state (checked, expanded, selected…), and focusability are wired into every component for Lynx's native screen readers — VoiceOver on iOS and TalkBack on Android. The one thing a component can't infer is a name for a control with no visible text, so that's the main thing you add.

What you get for free

Drop a component in and the native accessibility data is already on the element. State stays in sync automatically — a checkbox announces "checked" / "unchecked" as it toggles, an accordion "expanded" / "collapsed", a slider its value. This is true whether you use the headless @vyui/core primitives or the styled @vyui/kit components:

<script setup>
import { CheckboxRoot, CheckboxIndicator } from '@vyui/core'
import { ref } from 'vue'

const checked = ref(false)
</script>

<template>
  <CheckboxRoot v-model="checked">
    <CheckboxIndicator></CheckboxIndicator>
  </CheckboxRoot>
</template>

Add a label to controls without visible text

A component can't guess a name for a control that has no text — an icon-only button, an avatar, a close "×". Give those an accessibility-label (Lynx's equivalent of aria-label) so screen readers announce something meaningful:

<template>
  <!-- icon-only controls need a label -->
  <VyButton accessibility-label="Add to favorites">
    <Icon name="heart" />
  </VyButton>

  <!-- a button with text is announced from that text — no label needed -->
  <VyButton>Save changes</VyButton>
</template>
Built-in icon controls already set a sensible default — close buttons announce "Close", pagination announces "Next Page", and so on. Pass accessibility-label only when you want a more specific name.

Why Lynx is different from the web

On the web, <button> and <input type="checkbox"> get their accessibility from the browser, and you only reach for ARIA on custom widgets. Lynx renders to native <view> and <text> — generic boxes with no built-in roles — so accessibility has to be attached explicitly. Vy UI does that for you. Note that Lynx ignores web aria-* attributes; native accessibility uses accessibility-* props instead, which the components handle.

Building your own component

If you compose your own primitive on top of @vyui/core, use the useA11y composable to attach native accessibility the same way the built-ins do. Pass it a descriptor and spread the result onto your root element:

<script setup>
import { useA11y } from '@vyui/core'
import { ref } from 'vue'

const checked = ref(false)

const a11y = useA11y(() => ({
  role: 'checkbox',
  state: checked.value ? 'checked' : 'unchecked',
}))
</script>

<template>
  <view v-bind="a11y" @tap="checked = !checked">
    <text>Wifi</text>
  </view>
</template>

It's reactive: when checked changes, the announced state updates automatically.

Descriptor reference

FieldWhat it doesExample
roleThe kind of control'button', 'checkbox', 'switch', 'tab', 'slider', 'heading', 'dialog'
labelSpoken name (for controls without text)'Add to favorites'
stateSpoken state'checked', 'expanded', 'on', 'pressed'
selectedFor tabs/options — announces "selected" when true, nothing when falseisActive
valueA range value{ now: 30, max: 100 } → "30 of 100"
disabledMarks the control unavailabletrue
exclusiveFocusTraps screen-reader focus inside (modals)true
Lynx allows one role per element, so a disabled control announces "disabled" rather than its usual role. For tabs and options, pass selected (not a state string) — selected items announce "selected" and unselected ones stay quiet, which is how a screen reader expects a list to behave.

On the roadmap

Spoken announcements for transient UI like toasts, moving focus into a dialog when it opens, and an ARIA layer for the Lynx web target are in progress — see the roadmap.