Accessibility
The accessibility props every component takes, and how runtimes present them.
Every component takes React Native's accessibility props, so a custom-drawn control can tell screen readers what it is. Soundor owns these semantics: each runtime presents the same ones to its platform (ARIA in the browser, the platform's accessibility natively).
<Pressable
accessibilityRole="adjustable"
accessibilityLabel="Gain"
accessibilityValue={{ min: -60, max: 12, now: gain, text: `${gain} dB` }}
accessibilityActions={[{ name: 'increment' }, { name: 'decrement' }]}
onAccessibilityAction={(event) => {
if (event.actionName === 'increment') setGain(gain + 0.5);
if (event.actionName === 'decrement') setGain(gain - 0.5);
}}
/>The props
| Prop | What it says |
|---|---|
accessible | true: one element, its descendants' text its label. false: the element itself is not perceived; its descendants still are. |
accessibilityLabel | What is read. By default, a text's text, or the text inside a button-like element. |
accessibilityHint | What acting on it does. |
accessibilityRole | none, text, image, button, link, adjustable, checkbox, switch, togglebutton, radio, radiogroup, progressbar, search, combobox, menu, menubar, menuitem, scrollbar, spinbutton, tab, tablist, header, summary, keyboardkey, timer, toolbar, alert, dialog. |
accessibilityState | disabled, selected, checked (true, false, 'mixed'), busy, expanded. |
accessibilityValue | min, max, now (fractional values are fine) and text ('-3.5 dB', read in place of the number). |
accessibilityActions | The actions it responds to: activate, increment, decrement, longpress, expand, collapse, escape, setValue, or custom names with a label. |
onAccessibilityAction | An action asked of this element: event.actionName, and event.value for setValue. event.preventDefault() keeps the runtime's own response (a Pressable's press, focusing) from following. |
Defaults
ViewandScrollVieware not elements themselves; what they contain is.Textis read as its text (nested<Text>included, once). Accessibility props of a nested<Text>have no effect.Imageis decorative unless given a label, a role oraccessible.TextInputis a text field with its current text and placeholder.Pressableis one element, abuttonunless given another role, labelled by its text; it offersactivate(itsonPress) and, withonLongPress,longpress;disabledmakes it disabled to assistive technology too (unlessaccessibilityState.disabledsays otherwise).TextInputoffersactivate(start editing: keyboard focus),focusandsetValue(replace its text, firinginputandchange).Modalis a modaldialog(accessibilityLabelnames it): while it shows, only it is perceived, with the portals opened from inside it. WithonRequestCloseit offersescape(VoiceOver's scrub), which asks it to close like Escape does.
Moving a screen reader's cursor does not move keyboard focus; only an
explicit focus request does (or activating a text input), and only to a
focusable element.
Examples
// A button: its text is its label; assistive technology's "activate" is onPress.
<Pressable onPress={save}>
<Text>Save preset</Text>
</Pressable>
// A toggle: a switch with its state.
<Pressable
accessibilityRole="switch"
accessibilityLabel="Bypass"
accessibilityState={{ checked: bypassed }}
onPress={() => setBypassed(!bypassed)}
/>
// An adjustable gain control. Every input changes the same state through
// one function; the accessibility value is that state, not a copy.
<View
focusable
accessibilityRole="adjustable"
accessibilityLabel="Gain"
accessibilityValue={{ min: -60, max: 12, now: gain, text: `${gain.toFixed(1)} dB` }}
accessibilityActions={[{ name: 'increment' }, { name: 'decrement' }, { name: 'setValue' }]}
onAccessibilityAction={(event) => {
if (event.actionName === 'increment') change(gain + 0.5);
if (event.actionName === 'decrement') change(gain - 0.5);
if (event.actionName === 'setValue') change(Number(event.value));
}}
onKeyDown={(event) => {
if (event.key === 'ArrowUp') change(gain + 0.5);
if (event.key === 'ArrowDown') change(gain - 0.5);
}}
onPointerMove={drag}
/>
// A text input: labelled; its text is read as its value.
<TextInput accessibilityLabel="Preset name" value={name} onChangeText={setName} />
// A modal dialog: only it is read while open; escape asks it to close.
<Modal visible={open} onRequestClose={close} accessibilityLabel="Settings">
{/* ... */}
</Modal>The primitives example has a complete knob (GainKnob).
How it works
Soundor owns a platform-neutral semantic tree. It is built from the view's
nodes and their accessibility, by the same rules in every runtime. The
runtime's platform backend then presents it:
| Platform | Backend |
|---|---|
| Web | ARIA on the DOM elements that draw nodes |
| Windows | AccessKit (UI Automation) |
| Linux | AccessKit (AT-SPI), x64 |
| Android | AccessKit, once a runtime has an Android view |
| macOS | Soundor's VoiceOver bridge |
| iOS | Soundor's VoiceOver bridge (no iOS runtime yet) |
-
Not tied to a framework: the semantics do not come from JUCE, iPlug2 or Skia. A plugin framework only tells the backend which native view the surface is in, and where. A plugin's accessibility code is the same whatever framework hosts it.
-
Three trees: the render tree (nodes, and where they draw: a portal's content is under
overlayRoot), the keyboard focus structure (FocusScopes, which follow React), and the semantic tree (what a screen reader perceives) often look alike but differ:- a button's text is not an element of its own;
- a portal opened inside a modal is read inside it, wherever it draws
(its overlay entry's
accessibilityParent); - a modal hides the rest from the semantic tree while keyboard focus is
trapped by its own
FocusScope.
Closing a modal restores keyboard focus (the
FocusScope) and the accessibility context (the semantic tree) separately. -
Coordinates: an element's bounds are the view's logical pixels, like
getBoundingClientRect(). Each backend converts them to its platform's screen coordinates itself; there is no screen or window coordinate API. -
Cost: nothing is computed until assistive technology is listening. After that, each frame sends only the elements that changed.
See the native accessibility reference for the rules in detail and the backends, and its Apple bridge notes for how the macOS and iOS bridges stay safe in a plugin host.