Accessibility
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>
<script setup>
import { VyCheckbox } from '@vyui/kit'
import { ref } from 'vue'
const checked = ref(false)
</script>
<template>
<VyCheckbox v-model="checked" />
</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>
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
| Field | What it does | Example |
|---|---|---|
role | The kind of control | 'button', 'checkbox', 'switch', 'tab', 'slider', 'heading', 'dialog' |
label | Spoken name (for controls without text) | 'Add to favorites' |
state | Spoken state | 'checked', 'expanded', 'on', 'pressed' |
selected | For tabs/options — announces "selected" when true, nothing when false | isActive |
value | A range value | { now: 30, max: 100 } → "30 of 100" |
disabled | Marks the control unavailable | true |
exclusiveFocus | Traps screen-reader focus inside (modals) | true |
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.