Repository navigation
Accessibility
Every element has a place in a semantics tree: a role, an accessible name,
a description, states and a value, computed the way browsers do (HTML-AAM and
AccName 1.2, checked against Playwright). The tree is what tests query with
ByRole, what tela inspect prints, and what an agent reads to understand a
screen. An operating-system accessibility backend (AT-SPI, UI Automation,
NSAccessibility) is not there yet; the tree is built on demand, so it costs
nothing while you do not ask for it.
| Element | Role |
|---|---|
Button |
button |
H1 to H6
|
heading, with [level=N]
|
P |
paragraph |
Img |
img (named by Alt) |
A with Href
|
link |
Input, Textarea
|
textbox (named by its Label) |
Div, Span
|
none |
Elements without a role are left out of the snapshot, and their text is merged into their parent's.
For components built from divs, give the role yourself with Role
(WAI-ARIA 1.2). An unknown role panics in dev mode with the closest valid one.
The props follow React's aria-* attributes:
| Prop | Sets |
|---|---|
Role |
The role: "switch", "dialog", "tab", "progressbar"... |
AriaLabel |
The accessible name, for elements whose content does not name them (an icon button) |
AriaLabelledBy |
Names an element with the text of one or more others (joined by a space), attached with Ref
|
AriaDescription |
A longer hint read after the name |
AriaHidden |
Hides the subtree from the semantics tree, not from the screen |
AriaChecked |
Checked state of a checkbox, radio or switch |
AriaPressed |
Makes a button a toggle button and sets whether it is pressed |
AriaSelected |
Selected state of a tab, option, row or cell |
AriaExpanded |
Whether what a button controls (a menu, a disclosure) is expanded |
AriaDisabled |
Disabled for assistive technologies without making it inert (Disabled does both) |
AriaRequired |
A field the user must fill in |
AriaReadOnly |
A widget the user can focus but not edit |
AriaBusy |
An element that is being updated (a list that loads) |
AriaInvalid |
A field whose value failed validation; it wins over an Input's own validity (an email field with a malformed address is already invalid) |
AriaSort |
How a table is sorted by a column header: "ascending", "descending", "other" or "none"
|
AriaCurrent |
The current item of a set: "page" (the link of the page being shown), "step", "location", "date", "time" or "true"; "false" and "" mean none |
AriaLevel |
Level of a custom heading, list item, row or tree item |
AriaValueNow, AriaValueMin, AriaValueMax, AriaValueText
|
The value of a range widget (slider, progressbar, spinbutton, meter) |
These props also drive the aria-* class variants: aria-checked:, aria-expanded:,
aria-pressed:, aria-selected:, aria-disabled:, aria-hidden:, aria-busy:,
aria-required:, aria-readonly: and aria-invalid: (and their group- and peer- forms) match
when the element's prop is true, so the look follows the semantics:
Button(AriaExpanded(open), Class("aria-expanded:bg-slate-100"));
aria-[sort=ascending]: and aria-[sort=descending]: match the AriaSort of a
header (group-aria-[sort=descending]:rotate-180 turns its arrow), and
aria-[current=page]: (also step, location, date, time and true)
match the AriaCurrent of a link or step (Tailwind has no bare aria-current:).
The aria snapshot prints [invalid] (from AriaInvalid(true) only, on the
roles that support it) but not required, readonly, busy, the sort or the
current item, as Playwright's does not.
func WifiSwitch() tela.Node {
on, setOn := tela.UseState(true)
return Button(Role("switch"), AriaChecked(on), AriaLabel("Wi-Fi"),
Class("h-6 w-11 rounded-full"), IfElse(on, Class("bg-indigo-600"), Class("bg-slate-300")),
OnClick(func(tela.MouseEvent) { setOn.Set(!on) }),
Div(Class("size-5 rounded-full bg-white")),
)
}There are no checkbox, radio, select or slider elements: kit has the
components (kit.Checkbox, kit.RadioGroup, kit.Select, kit.Slider) and
aria the hooks behind them. For a control of your own, a Button or
Div with a Role, the state props, TabIndex and key handlers is how you
build it.
func ConfirmDialog(confirm func()) tela.Node {
title := tela.UseElementRef()
return Div(Role("dialog"), AriaLabelledBy(title), Class("rounded-xl bg-white p-6 shadow-xl"),
H2(Ref(title), Class("text-lg font-semibold"), Text("Delete file?")),
P(Class("text-slate-500"), Text("This cannot be undone.")),
Button(AutoFocus(true), OnClick(func(tela.MouseEvent) { confirm() }), Text("Delete")),
)
}func Progress(percent float64) tela.Node {
return Div(Role("progressbar"), AriaLabel("Upload"),
AriaValueNow(percent), AriaValueMin(0), AriaValueMax(100),
Class("h-2 w-64 rounded-full bg-slate-200"),
Div(Class("h-2 rounded-full bg-indigo-500"), Style(tela.Width(float32(percent)*2.56))),
)
}Label names a text field and focuses it when clicked (see
Forms and Text Input). For anything else, use
AriaLabel or AriaLabelledBy. Mark decorative images with Alt(""): the
semantics tree leaves them out; Alt panics on elements that are not an
Img.
tela inspect prints the tree as a Playwright aria snapshot, one line per
role, with the accessible name and the states:
- heading "Settings" [level=1]
- switch "Email notifications" [checked]
- button "Save changes" [disabled]From a test, App.AriaSnapshot returns the same text,
App.ExpectAria compares it and App.AriaGolden stores long ones
in a file. The headless telatest.AriaSnapshot works without a test. See
Testing and CLI.
Queries use the same tree: telatest.ByRole("switch", "Wi-Fi") finds the
switch above by what an assistive technology would announce, which is why they
survive restyling.
A Button is focusable and activates with Enter and Space. Any other element
becomes focusable with TabIndex, and you handle the keys with
OnKeyDown; see Events. The focus-visible: variant draws
the focus ring only for keyboard focus.