Accessibility

Screen readers read your app aloud and let people move through it by touch: VoiceOver on iPhone and TalkBack on Android. Lynx elements take attributes that tell the screen reader what each element is and what to say.

id, a11y-id and name

Three attributes name an element, and they're often confused:

AttributeWhat it's for
idThe element's unique name on the page. accessibility-elements and requestFocus('#id') use it
a11y-idA second name that only accessibility uses. accessibility-elements-a11y lists these, and several elements can share one
nameLets native code in the host app find the view. Accessibility doesn't use it

Use id unless you have a reason not to.

Label an element

<view
  [accessibility-element]="true"
  accessibility-traits="button"
  accessibility-label="Add to favorites"
  (bindtap)="favorite()"
>
  <image src="assets/heart.png" class="h-6 w-6" />
</view>
  • <text> and <image> are read by default. A <text> reads its own text, so most text needs nothing extra.
  • [accessibility-element]="true" makes an element one stop for the screen reader. Use it on a <view> that acts as a button or groups related content.
  • Bind true/false attributes with brackets. A plain accessibility-element="true" passes the string "true", which Angular's strict template checks reject.
  • accessibility-label is what gets read. Give one to anything without readable text, such as an icon button.
  • accessibility-traits says what kind of thing it is, such as button, image, link or header. Note the plural: Lynx ignores accessibility-trait.

Set the reading order

<view accessibility-elements="title,price,buy">
  <text id="price" [accessibility-element]="true">$12</text>
  <text id="title" [accessibility-element]="true">Blue mug</text>
  <view
    id="buy"
    [accessibility-element]="true"
    accessibility-traits="button"
    accessibility-label="Buy"
    (bindtap)="buy()"
  ></view>
</view>
  • Screen readers go top to bottom, then left to right, by default.
  • accessibility-elements lists child ids, separated by commas, in the order to read them. The example reads the title before the price.
  • Children not in the list can't be focused once a parent sets it.
  • accessibility-elements-a11y does the same with a11y-ids.

Hide decoration

<view [accessibility-elements-hidden]="true">
  <image src="assets/confetti.png" class="h-24 w-full" />
</view>

[accessibility-elements-hidden]="true" hides an element and everything inside it from the screen reader.

Keep focus inside a dialog

<overlay [attr.visible]="open()">
  <view class="flex h-full w-full items-center justify-center bg-black/50">
    <view
      class="w-4/5 rounded-lg bg-background p-6"
      [accessibility-exclusive-focus]="true"
    >
      <text class="text-lg text-foreground">Delete this item?</text>
      <view
        [accessibility-element]="true"
        accessibility-traits="button"
        accessibility-label="Cancel"
        (bindtap)="open.set(false)"
      >
        <text>Cancel</text>
      </view>
    </view>
  </view>
</overlay>

With [accessibility-exclusive-focus]="true", the screen reader moves only between the elements inside it. People can't swipe out of the dialog to the page behind. See Dialogs and overlays.

Announce changes and move focus from code

Some changes don't move the screen reader, such as a cart count going up. Tell people about them with LynxAccessibility:

src/app/cart.ts
import { Component, inject, signal } from '@angular/core';
import { LYNX_ELEMENTS, LynxAccessibility } from '@blotch/angular-lynx';

@Component({
  selector: 'app-cart',
  imports: [LYNX_ELEMENTS],
  template: `
    <view
      [accessibility-element]="true"
      accessibility-traits="button"
      accessibility-label="Add to cart"
      (bindtap)="add()"
    >
      <text>Add to cart</text>
    </view>
    <text id="total">{{ count() }} items</text>
  `,
})
export class Cart {
  readonly #a11y = inject(LynxAccessibility);
  readonly count = signal(0);

  add(): void {
    this.count.update((n) => n + 1);
    this.#a11y.announce(`${this.count()} items in cart`).catch(() => {});
  }
}
  • announce(text) makes the screen reader speak, without moving focus.
  • requestFocus('#id') moves the screen reader to the element with that id, such as requestFocus('#total').
  • Both return a Promise that rejects where Lynx can't do it, such as in the web preview. The catch keeps that quiet.

The signatures are in the API reference.

Test with a screen reader

On an iPhone:

  1. Open Settings → Accessibility → VoiceOver and turn it on.
  2. Swipe right or left to move between elements. Double-tap anywhere to tap the selected one.
  3. To toggle it quickly, set Settings → Accessibility → Accessibility Shortcut to VoiceOver, then triple-click the side button.

VoiceOver doesn't run in the iOS Simulator, the virtual iPhone on your Mac. Instead, open Accessibility Inspector from Xcode's Xcode → Open Developer Tool menu and point it at the Simulator. It shows the label and traits of each element you hover.

On Android:

  1. Open Settings → Accessibility → TalkBack and turn it on. On some phones, it's under Installed apps or Downloaded apps in that menu.
  2. Swipe right or left to move between elements. Double-tap to tap the selected one.

TalkBack also runs in the Android emulator, the virtual Android phone on your computer. Drag with the mouse to swipe. If its Settings has no TalkBack, install Android Accessibility Suite from the Play Store, which needs an emulator image with Google Play.

You can test either way in Lynx Explorer or in your own app. AngularLynx turns on Lynx's accessibility support in every build, so there's nothing to enable.

Common attributes

AttributeValueEffect
accessibility-elementtrue / falseMakes the element one stop for the screen reader
accessibility-labeltextWhat the screen reader reads
accessibility-traitsbutton, image, link, …What kind of element it is
accessibility-valuetextA current value read after the label, such as a slider's position
accessibility-elementscomma-separated idsReading order of the children
accessibility-elements-a11ycomma-separated a11y-idsThe same, by a11y-id
accessibility-elements-hiddentrue / falseHides the element and its children
accessibility-exclusive-focustrue / falseKeeps focus inside the element

The full list, with platform support, is in the accessibility API reference.