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.
Implicit roles
Section titled “Implicit roles”| 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.
Roles and ARIA props
Section titled “Roles and ARIA props”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 yet (they arrive with
the kit components in a later phase); a Button or Div with a Role, the
state props, TabIndex and key handlers is how you build them meanwhile.
A dialog named by its title
Section titled “A dialog named by its title”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")), )}Progress, with a value
Section titled “Progress, with a value”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))), )}Labels
Section titled “Labels”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.
Looking at the tree
Section titled “Looking at the tree”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.
Keyboard access
Section titled “Keyboard access”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.