Skip to content

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 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.

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.