Guides
Accessibility
What screen readers see, naming elements, roles, descriptions and announcements.
Assistive technology, such as VoiceOver on macOS, Orca on Linux and Narrator or NVDA on Windows, reads a window's native UI as it reads other apps: the widgets with their roles, names, values and states, the texts, and the elements that take the focus, as the content changes. It acts on them as the keyboard and the pointer would: it presses buttons, checks boxes, moves sliders, edits text inputs and moves the focus.
Names
Widgets describe themselves, and the text inside an element names it, as a
button's does. Elements without text need a Label, which also finds them
in tests:
ui.Slider(c, &app.volume, 0, 100).Label("Volume")
ui.TextInput(c, &app.query).Placeholder("Search").Label("Search")
ui.Box(c).Size(24, 24).Draw(drawIcon).Label("Unread messages")A field names its control with its label, and LabelledBy
names a control by a text the view draws as its caption, a click on which
goes to the control (labels of your own).
Roles
Other elements get a role from what they do: one that is clickable and
takes the focus is a button, a scroll container a scroll area, and one with
a Label, or that takes the focus, a group. Role sets it for an element
drawn as a widget it is not built from, and ui.RoleNone leaves an element
out but not its children (AccessHidden
leaves them out too):
toggle := ui.Box(c).Size(36, 20).Focusable().Role(ui.RoleSwitch).Label("Wi-Fi")Besides the roles of MyGo's widgets, ui.RoleHeading is the title of a
section, ranked with Level, ui.RoleNavigation a landmark of links
that move around the app or a document, as a breadcrumb trail or a
navigation bar, which screen readers list and jump to (as
Breadcrumbs is), and ui.RoleMenu, ui.RoleMenuBar,
ui.RoleMenuItem, ui.RoleMenuItemCheckBox and ui.RoleMenuItemRadio
make menus drawn in the window, as a drop-down menu or a menu bar of your
own (every menu MyGo draws in the window,
PopoverMenu's,
PopoverMenuButton's and a
window's, has them, as do
MenuBase and ContextMenuBase,
menus without a look that keep their keys and focus):
ui.Text(c, "Appearance").Bold().Role(ui.RoleHeading).Level(2)
ui.Row(c).Role(ui.RoleNavigation).Label("Sections").Children(sectionLinks)A table of your own builds its rows with ui.RoleRow, its cells with
ui.RoleCell and the headers of its columns with ui.RoleColumnHeader.
A header is named by the text inside it, and keeps the elements inside it
in the tree, as ARIA's columnheader does: a sort button and a column's
menu button in it are reached and pressed as any other button.
ui.Row(c).Role(ui.RoleColumnHeader).Children(func() { // named "Sort Options"
if ui.Button(c, "Sort").Clicked() {
app.sortBy("name")
}
ui.Button(c, "Options")
})ui.RoleGrid and ui.RoleTreeGrid are tables of your own whose cells
take the focus and the arrow keys, as a spreadsheet's, where
ui.RoleTable is one to read: see Grids.
ui.RoleListBox is a list of options to choose from, and ui.RoleOption
one of its options, selected with Checked, as ARIA's listbox, option
and aria-selected: a list box on Windows (UI Automation's List, with
Selection and SelectionItem), on Linux (ATK's list box, with
AtkSelection) and on macOS (a table whose rows are selected, as
AppKit's lists are), as the
popups of SelectBase and
ComboboxBase are. Suggestions shown at a text area's
caret, as for mentions, are one, the text area keeping the focus and
pointing at the option highlighted with ActiveDescendant:
area := ui.TextArea(c, &app.draft)
if len(app.matches) > 0 {
ui.Column(c).Role(ui.RoleListBox).Label("People").Children(func() {
for i, p := range app.matches {
opt := ui.Row(c).Padding(4, 8).Role(ui.RoleOption).Checked(i == app.hi)
if opt.Clicked() {
app.mention(p)
}
opt.Children(func() { ui.Text(c, p.Name) })
if i == app.hi {
area.ActiveDescendant(opt)
}
}
})
}Pressing an option, as assistive technology choosing it does, clicks it;
it takes the focus only if it is Focusable.
States
The bases tell assistive technology what their widgets show. A widget of your own tells it with these, as ARIA's states do on the web:
| Method | What it tells |
|---|---|
Checked(on), Mixed() |
whether a check box, switch, radio button, toggle or menu item is on, or partly on, or whether an option is selected |
AccessSelected(on) |
whether a tab, option, row, cell or tree item is selected, without the accent Selected paints (below) |
Expanded(open) |
whether what the element opens shows, as a popup or a section |
Value(s) |
its value, as the choice a button opening a popup shows |
Range(lo, hi, value), Step(step) |
the range and value of a slider, progress bar, meter or stepper, and how far the keys move it |
Level(n) |
the rank of a heading, or how deep an item of a tree is |
PositionInSet(pos, size) |
which item of how many an item of a list, tree, menu or tab list is, from 1, where what is built does not say (below) |
ActiveDescendant(e) |
the option that has the focus while the element keeps it, as the one the arrows are on in a menu or a list |
Busy(on) |
that the element's content is about to change, as a region loading: screen readers wait for it before reading it |
Current(kind) |
that the element is the current one of the items around it: ui.CurrentPage (the page the user is on), CurrentStep, CurrentLocation, CurrentDate, CurrentTime or CurrentItem |
AutoComplete(kind) |
what a text input's suggestions complete as the user types: ui.AutoCompleteList (a popup of them), AutoCompleteInline (the text completed in place) or AutoCompleteBoth (below) |
RoleDescription(s) |
what to call its role, in place of the role's own name, as "carousel" for a group of slides and "slide" for each |
Controls(e) |
that it controls the content or presence of e, as a disclosure button the region it opens; once for each |
HasPopup(kind) |
the kind of popup it opens: ui.PopupMenu, PopupListBox, PopupTree, PopupGrid or PopupDialog |
Sort(dir) |
the order a table's rows are sorted in by the column whose header it is: ui.SortAscending, SortDescending, SortOther or SortNone |
Orientation(o) |
which way a slider, separator, splitter, toolbar, tab list or menu of your own runs: ui.Vertical or ui.Horizontal (below) |
DescribedBy(e), Invalid(on), ErrorMessage(e) |
the element e whose text describes it, that its value is not valid, and the element holding its error (below) |
Multiselectable(on) |
that the user chooses several of the items of a list, grid, tree or tree grid of your own |
ReadOnly(on) |
that an element of your own shows a value the user cannot change, as aria-readonly: a text box, combo box, check box, switch, slider, stepper, radio group, list box, grid or tree grid, and the cells and headers of a grid or tree grid (not of a table); assistive technology then neither sets nor steps its value. It is AXValue not settable on macOS, ATK's read-only state and UI Automation's IsReadOnly |
Multiline(on) |
that a text box of your own (RoleTextField) takes several lines, as aria-multiline |
RowCount(n), ColumnCount(n) |
how many rows and columns a table or grid has, built or not |
RowIndex(i), ColumnIndex(i) |
which of all the rows a row or a cell is in, and which of all the columns a cell is in, from 1 |
all := ui.CheckboxBase(c, &app.all).Label("Select all")
if app.some {
all.Mixed()
}
date := ui.ButtonBase(c).Role(ui.RolePopUpButton).Label("Due").Value(app.due.Format("Jan 2")).Expanded(app.picking)A region loading says so while a placeholder stands in for it, and the link to the page shown in a navigation bar says it is the current page:
ui.Column(c).Label("Messages").Busy(app.loading).Children(func() {
if app.loading {
ui.Spinner(c).Label("Loading")
return
}
messageList(c, app.messages)
})
ui.Row(c).Role(ui.RoleNavigation).Label("Main").Children(func() {
for _, p := range pages {
l := ui.Link(c, p.Title, p.Path)
if p.Path == app.path {
l.Current(ui.CurrentPage)
}
}
})An element with no role of its own and no Label is still seen once
Busy or Current marks it, as a group, as ARIA's states make a
generic element seen on the web. Busy is AppKit's AXElementBusy,
ATK's busy state and UI Automation's AriaProperties (busy=true);
Current is AppKit's AXARIACurrent, ATK's current attribute with the
active state, and AriaProperties (current=page).
A cell of a grid says whether it is chosen with AccessSelected, or
Checked, as ARIA's aria-selected on a gridcell, without painting
it, as a calendar's days or the cells of a spreadsheet of your own;
Selected paints it in the accent color too. Cells (RoleCell, RoleColumnHeader,
RoleRowHeader) that call neither are not chosen among others, and a
table or a grid (RoleTable, RoleGrid, RoleTreeGrid)
holding cells that do chooses them. A carousel, a
disclosure and a button opening a list box, built on the others:
ui.Column(c).Role(ui.RoleGroup).Label("Highlights").RoleDescription("carousel").Children(func() {
for i, s := range slides {
ui.Column(c).Role(ui.RoleGroup).Label(fmt.Sprintf("%d of %d", i+1, len(slides))).
RoleDescription("slide").AccessHidden(i != app.slide).Children(s)
}
})
toggle := ui.Button(c, "Details").Expanded(app.open)
if app.open {
toggle.Controls(ui.Column(c).Role(ui.RoleRegion).Label("Details").Children(details))
}
ui.ButtonBase(c).Role(ui.RolePopUpButton).Label("Fruit").Value(app.fruit).
HasPopup(ui.PopupListBox).Expanded(app.picking)
day := ui.Box(c).Role(ui.RoleCell).Label(d.Format("January 2")).AccessSelected(d.Equal(app.picked))They reach each platform as WAI-ARIA's Core Accessibility API Mappings
say. RoleDescription is AppKit's AXRoleDescription, ATK's
roledescription attribute and UI Automation's LocalizedControlType.
Controls is AppKit's AXLinkedUIElements, ATK's controller-for
relation (and controlled-by on what it controls) and UI Automation's
ControllerFor, which screen readers offer to move to (VoiceOver's
VO-J). HasPopup is AppKit's AXPopupValue with the AXShowMenu
action, which presses the element, ATK's has-popup state and
haspopup attribute, and UI Automation's ExpandCollapse pattern (with
Expanded) and AriaProperties (haspopup=listbox). Sort is
AppKit's AXSortDirection (unknown for another order) on a header that
is a sort button (AXSortButton), as NSTableHeaderView's are, ATK's
sort attribute, and UI Automation's ItemStatus and AriaProperties
(sort=other), as Table's headers say from
ListState.Sort. A chosen cell is AppKit's AXSelected with its
table's AXSelectedCells, and its row in the table's AXSelectedRows,
as NSTableView tells its choice; as VoiceOver reads neither the state of
a cell nor its change, the cell says "selected" after its name, in the
app's language, and a cell chosen away from the focus is read out. It is
ATK's selectable and selected states, and UI Automation's SelectionItem,
in the table's Selection.
A test reads them as
assistive technology gets them, with Tester.AccessByRole:
if a, _ := tt.AccessByRole(ui.RoleGroup, "Messages"); !a.Busy {
t.Error("Messages is not busy while loading")
}
if day, _ := tt.AccessByRole(ui.RoleCell, "November 2"); !day.Selected {
t.Error("the day picked is not selected")
}A check box that is mixed as often as not is better built on
CheckboxStateBase, which tells it itself
from its ui.CheckState, and checks it as it is pressed.
Suggestions
AutoComplete(kind) tells screen readers what a text input's suggestions
complete as the user types, as ARIA's aria-autocomplete, so that they
say a field has them: ui.AutoCompleteList for a popup of the values
matching what was typed, AutoCompleteInline for the rest of the value
inserted after the caret, selected, and AutoCompleteBoth for both. A
combobox, an autocomplete and a
token field with suggestions tell AutoCompleteList
themselves; a combobox of your own, with Expanded for its popup and
ActiveDescendant for the option the arrows are on, tells it too:
in := ui.TextInputBase(c, &app.query).Role(ui.RoleComboBox).Label("Search")
in.Expanded(len(app.matches) > 0).AutoComplete(ui.AutoCompleteList)It only tells: the view shows the suggestions itself. AutoCompleteNone,
the default, tells nothing, as for a combobox whose popup shows the same
options whatever is typed. It is AppKit's AXAutocompleteValue on macOS,
as WebKit gives it, ATK's autocomplete object attribute with the
supports-autocompletion state on Linux, as Core-AAM maps it, and
autocomplete=list in UI Automation's AriaProperties on Windows, the
place of ARIA's properties UI Automation has none of. A
test reads it in the
AutoComplete of Tester.AccessByRole's answer, and the
automation plugin in a node's
autoComplete.
Descriptions and errors
Description tells more than the name, as help text read after it, as a
tooltip does, and Error marks a value as invalid, for a
message read with it; a field gives its control the texts below
it.
A hint or a help text the view draws as an element of its own describes a
control with DescribedBy, as ARIA's aria-describedby: screen readers
read its text after the control's name and value, after the control's
Description, and a sentence with a link in it reads as one text. It
counts even when hidden from assistive technology, so the hint can be left
out of the reading order and still be read with the field:
hint := ui.Text(c, "16 digits, no spaces").TextColor(t.TextMuted)
ui.TextInput(c, &app.card).Label("Card number").DescribedBy(hint)Invalid marks a value as invalid without a message, as aria-invalid
alone: a required field marked as the user leaves it empty, whose message
waits for the form to be submitted. A text input then draws its border in
the theme's Danger color, and its description stays as it is.
ErrorMessage points at the element holding the message, as
aria-errormessage: while the control is invalid, its text is read first,
before the description, and screen readers that follow the relation go to
it; while the control is valid, it says nothing:
in := ui.TextInput(c, &app.email).Label("Email").Invalid(app.emailTouched && app.email == "")
if app.submitted && app.email == "" {
in.ErrorMessage(ui.Text(c, "Enter your email").TextColor(t.Danger))
}The description is AppKit's AXHelp, ATK's description with the
described-by relation (and description-for on the hint), and UI
Automation's FullDescription with DescribedBy. The message is WebKit's
AXErrorMessageElements, ATK's error-message relation (and error-for
on the message), and UI Automation's ControllerFor, as Core-AAM maps
them; invalid is AXInvalid, ATK's invalid-entry state and UI
Automation's IsDataValidForForm.
Orientation
Orientation tells which way an element of your own runs, as
aria-orientation: a slider, separator, splitter, toolbar, tab list or
menu drawn up and down, whose keys go Up and Down, is ui.Vertical, which
screen readers read with it. It only tells it: the view lays the element
out and takes its keys (FocusGroup moves the focus
with the arrows). A slider or a separator told nothing runs across, a
separator taller than wide up and down, and a
SliderBase going up (Vertical) says so itself:
ui.Box(c).Size(6, 160).Draw(drawTrack).Focusable().
Role(ui.RoleSlider).Label("Volume").Range(0, 100, app.volume).
Orientation(ui.Vertical)
ui.Column(c).Role(ui.RoleToolbar).Label("Tools").Orientation(ui.Vertical).Children(tools)It is AppKit's AXOrientation, ATK's vertical or horizontal state, and UI
Automation's Orientation.
Keyboard shortcuts
KeyShortcuts(mods, key) tells screen readers a key that presses or
focuses the element, as ARIA's aria-keyshortcuts: a toolbar button
whose command the window's shortcut runs, or the button opening a command
palette, is read with its key after its name. It only tells the key,
which the view handles itself, as with
c.Shortcut; call it once for each key that does:
if c.Shortcut(ui.Cmd, ui.KeyK) {
app.palette = true
}
ui.Toolbar(c, func() {
if ui.Button(c, "Search").KeyShortcuts(ui.Cmd, ui.KeyK).Clicked() {
app.palette = true
}
ui.Button(c, "Redo").KeyShortcuts(ui.Cmd|ui.Shift, ui.KeyZ).KeyShortcuts(ui.Cmd, ui.KeyY)
})The keys are written as the platform's menus write them, separated by
spaces: ⌘K and ⇧⌘Z ⌘Y on macOS, in AppKit's AXKeyShortcutsValue as
WebKit gives it; Ctrl+K and Ctrl+Shift+Z Ctrl+Y on Linux, in ATK's
keyshortcuts object attribute, and on Windows, in UI Automation's
AcceleratorKey, where Super is Win. A
test reads them in the
KeyShortcuts of Tester.AccessByRole's answer, and the
automation plugin in a node's keyShortcuts.
Formats of text
The rich text editor tells screen readers the formats
of its runs as they read them: bold, italic, underlined, struck through,
links, fonts, colors and headings. VoiceOver reads them through
AXAttributedStringForRange, Orca through ATK's run attributes, and
Narrator through UI Automation's Text pattern, which every text field has,
by characters, formats, words and lines. Screen readers move the selection
of text fields too.
Lists and tables
A list's rows are list items, and a table's rows,
named by the text inside them, and each says which of all the rows it is,
"5 of 10,000", although the list builds only those in view. A list choosing
its rows gives assistive technology the focus on the row chosen: Up and
Down are read as they move the choice, and focusing a row chooses it. A
screen reader moving out of view asks the list to scroll there, which
builds the rows it reaches. Label names a list or a table. A table of
your own, or one whose cells take the arrow keys, is a
grid.
Place in a set
Screen readers read an item with its place, "3 of 50", which they count
from the items around it. A list, list box, tree, menu or tab list of
your own whose items are not all built, as one showing only those in
view, or whose items are not siblings, as a tree laid out flat in one
column, tells each item's place with PositionInSet(pos, size), as
ARIA's aria-posinset and aria-setsize: pos from 1, size how many
the set has, built or not. Either at 0 or less is not given.
ui.Column(c).Role(ui.RoleListBox).Label("Results").Children(func() {
for i := app.first; i < app.last; i++ { // only those in view
ui.Row(c).Role(ui.RoleOption).PositionInSet(i+1, len(app.results)).
Children(func() { ui.Text(c, app.results[i]) })
}
})
ui.Column(c).Role(ui.RoleTree).Label("Files").Children(func() {
for _, f := range app.flat { // each item at its depth, one below the other
ui.Row(c).Role(ui.RoleTreeItem).Label(f.Name).Level(f.Depth+1).PositionInSet(f.Index+1, f.Siblings)
}
})The place the app gives wins over the one a ComboboxBase,
SelectBase, MenuBase, Tree or GridView gives its items, which
count those they build, as for a combobox showing the first 50 of 1,000
results; a List's and a Table's rows keep theirs,
which already say which of all their rows they are. The rows and cells
of a grid of your own say their place with RowIndex and
ColumnIndex. It is ATK's posinset and setsize attributes on Linux,
UI Automation's PositionInSet and SizeOfSet on Windows, and AppKit's
AXIndex on macOS, where a list's AXRowCount is the size its items
give when it has none of its own. A
test reads them as
PosInSet and SetSize.
Selected, without the look
Selected(true) shows an element as chosen in the accent color, with
the accent's text color, and tells assistive technology it is selected.
A tab, an option, a row, a cell or an item of a tree that shows its
choice its own way, underlined, bold or with a check mark, tells only
the state with AccessSelected(on), ARIA's aria-selected:
ui.Row(c).Role(ui.RoleTabList).Label("Views").Children(func() {
for i, name := range views {
tab := ui.ButtonBase(c).Role(ui.RoleTab).Label(name).AccessSelected(i == app.view)
if tab.Clicked() {
app.view = i
}
tab.Children(func() {
label := ui.Text(c, name)
if i == app.view {
label.Bold()
}
})
}
})It sets the state Selected sets, which Checked also sets on these
roles, so it reaches every platform as Selected does: a row or a cell
saying whether it is selected, false or true, is one its table or
grid chooses among others, the table or grid holding the choice. It
only tells: the view shows the choice and changes it as the element is
clicked. A test reads it
as Selected.
Grids
A grid is a table whose cells take the focus and move it with the arrow
keys, as a spreadsheet's or a data grid's, where ui.RoleTable is one to
read; ui.RoleTreeGrid is a grid whose rows open and close as a tree's
items do, with Level and Expanded on its rows. Their rows, cells and
headers are ui.RoleRow, ui.RoleCell, ui.RoleColumnHeader and
ui.RoleRowHeader, as a table's: build the rows directly inside the
grid (an element without a role between them is left out), and the
cells directly inside the rows.
One of thousands of rows builds only those in view, so it tells screen
readers how many it has, RowCount and ColumnCount (ARIA's
aria-rowcount and aria-colcount, header rows counted), and each row
which of all it is, RowIndex from 1 (aria-rowindex); a cell of a
column not built, or after one left out, says its column with
ColumnIndex (aria-colindex), and the cells after it follow it.
Screen readers then read "row 4,121 of 10,000". A cell covering several
columns or rows, as a total under two columns, says so with
ColumnSpan and RowSpan (aria-colspan, aria-rowspan), which also
lay it out across them where its row is a Grid: the cell after
it in its row is in the column after those it covers, and the cells of
the rows below one spanning rows say their columns with ColumnIndex
where they follow it. Multiselectable(true)
says that the user chooses several rows (aria-multiselectable), and
each row says whether it is chosen with Selected, or Checked, which
does not paint it:
ui.Column(c).Role(ui.RoleGrid).Label("Sheet").RowCount(len(app.rows)+1).ColumnCount(len(cols)).Multiselectable(true).Children(func() {
ui.Row(c).Role(ui.RoleRow).RowIndex(1).Children(func() {
for _, col := range cols {
ui.Box(c).Role(ui.RoleColumnHeader).Children(func() { ui.Text(c, col.Title) })
}
})
for _, r := range app.visibleRows() { // 4120 to 4160
ui.Row(c).Role(ui.RoleRow).RowIndex(r + 2).Selected(app.chosen[r]).Children(func() {
for j, col := range cols {
ui.Box(c).Role(ui.RoleCell).Focusable().Children(func() { ui.Text(c, app.cell(r, j)) })
}
})
}
})A List shown as a grid builds only the rows in view itself:
Role(ui.RoleGrid) makes its rows rows rather than list items, which
already say which of all they are, as its Selection chooses several:
ui.List(c, &app.rows, len(app.data), func(i int) {
ui.Row(c).Children(func() {
for j := range cols {
ui.Box(c).Role(ui.RoleCell).Children(func() { ui.Text(c, app.cell(i, j)) })
}
})
}).Role(ui.RoleGrid).Label("Sheet").ColumnCount(len(cols))A cell across two columns of a hand-built data grid, whose rows lay out their cells in the grid's columns:
ui.Grid(c).Role(ui.RoleRow).ColumnTracks(ui.Fixed(120), ui.Fr(1), ui.Fr(1), ui.Fr(1)).Children(func() {
ui.Box(c).Role(ui.RoleRowHeader).Children(func() { ui.Text(c, "Design") })
ui.Box(c).Role(ui.RoleCell).ColumnSpan(2).Children(func() { ui.Text(c, "First half: 9") })
ui.Box(c).Role(ui.RoleCell).Children(func() { ui.Text(c, "Total: 14") }) // the fourth column
})A Table's rows say which of all they are themselves, and so
does a list's: the counts and indexes are for grids and tables of your
own. A grid is AppKit's AXTable, its rows and cells telling the counts
and indexes in AXARIARowCount, AXARIAColumnCount, AXARIARowIndex
and AXARIAColumnIndex as WebKit's do, and its rows AXIndex from them;
ATK's table (a tree grid's tree table) with the xml-roles attribute
grid (treegrid) and the rowcount, colcount, rowindex and
colindex attributes, as Chromium gives them; and UI Automation's
DataGrid, whose Grid and Table patterns, and its cells' GridItem
and TableItem, say where each cell is among all, from 0, and which
column and row headers name it, as tables' do. A cell's spans are the
lengths of its AXColumnIndexRange and AXRowIndexRange on macOS, its
colspan and rowspan attributes on Linux, and GridItem's
ColumnSpan and RowSpan on Windows, where the grid finds the cell at
each place it covers. Cells say whether they
are chosen with Checked too, the grid holding their
choice, as a table's do. A grid choosing several rows is
multiselectable on Linux and CanSelectMultiple on Windows, its rows
selectable and selected there, and in its AXSelectedRows on macOS.
Unavailable, still reachable
Disabled(true) does three things: the element reads as disabled, it
leaves the focus and the Tab order and takes no clicks, and it shows at
half its opacity (unless DimDisabled(false)). A screen reader moving
by Tab then never reaches it. AccessDisabled(true) does the first alone,
as HTML's aria-disabled="true", for what the user must still find while
it cannot be used, as the APG asks of a menu item whose action is
blocked, a tab disabled but still in the arrows' order, or a submit
button while the form is not valid: the element, and those inside it,
read as disabled (dimmed, unavailable), and keep the focus, their place
in the Tab order, the pointer and their look.
del := ui.Button(c, "Delete project").AccessDisabled(app.locked)
if app.locked {
del.Description("Unlock the project to delete it")
}
if del.Clicked() && !app.locked {
app.deleteProject()
}It only tells: clicks, Enter and Space still press the element, which
the view ignores while it is unavailable, as on the web, and it shows as
the view styles it. Assistive technology focuses it and reads it, its
Description with it, but neither presses nor edits it. It is AppKit's
AXEnabled false on macOS, ATK's enabled and sensitive states left out
on Linux, and UI Automation's IsEnabled false on Windows, with
IsKeyboardFocusable true and its SetFocus taken, as Chromium's
aria-disabled. A test
reads it as Disabled with Focusable in Tester.AccessByRole's
answer.
Out of the pointer's way
IgnoreInput(true) leaves an element, and those inside it, to assistive
technology alone: the pointer goes through it to what is under it, Tab
and clicks give it no focus, but assistive technology still finds it, with
its role and name, and presses it, which Clicked reports. A button that
shows while its row is hovered can so stay built at rest, at Opacity(0),
where a screen reader finds it and a click on the row is the row's:
row := ui.Row(c).Padding(8)
hovered := row.Hovered()
if row.Clicked() {
app.open(item)
}
row.Children(func() {
ui.Text(c, item.Title).Grow(1)
del := ui.Button(c, "Delete").IgnoreInput(!hovered)
if !hovered {
del.Opacity(0)
}
if del.Clicked() {
app.remove(item)
}
})Assistive technology can neither focus it nor move a slider inside it, as
both go through the keyboard focus; it can set a text input's value.
Unlike HTML's inert, which hides an element from assistive technology
too, it stays in the tree: Invisible and Hidden take it out of it,
AccessHidden too while it still shows, and Disabled keeps it there
without its press, as AccessDisabled does with its focus too.
Out of assistive technology's way
AccessHidden(true) leaves an element, and those inside it, out of what
assistive technology reads, while they still paint, as HTML's
aria-hidden="true" does: none of them is in its tree, their text does
not name the element around them, and it can neither press, focus nor edit
them. It is for what shows but is not there to read, or is read already:
- a page or a panel sliding out beside the one replacing it, while both show;
- the cards peeking from behind the front one of a stack, the slides beside the current one of a carousel;
- a copy of a text drawn twice, as a label fading into its next one.
With IgnoreInput(true), it is HTML's inert: the pointer goes through
them, Tab passes them by, they lose the focus and their text cannot be
selected, as for the copy of an
element leaving or a
page going away:
ui.Box(c).Fill().Children(func() {
if app.leaving >= 0 { // the tab that was selected, sliding out
page(c, app.leaving).Absolute().Translate(-x, 0).IgnoreInput(true).AccessHidden(true)
}
page(c, app.tab).Translate(width-x, 0)
})Alone, it leaves them to the pointer and the keyboard, and their text to
selection, as aria-hidden does: keep the keyboard focus out of them, as
assistive technology cannot follow it there. RoleNone leaves out an
element alone, which suits a leaf, as a decorative icon.
Announcements
News that does not move the focus, as a search done or a file saved,
reaches screen readers with c.Announce(text), which they read once, after
what they are reading: VoiceOver's and Orca's announcements, and a live
region that Narrator and NVDA read on Windows. Toasts announce
their text, and a router the title of a page shown while
the focus stays outside it.
if ui.Button(c, "Search").Clicked() {
app.results = search(app.query)
c.Announce(fmt.Sprintf("%d results", len(app.results)))
}MyGo describes frames only once assistive technology asked, so apps pay nothing for it otherwise.
Landmarks, live regions and rules
These roles are ARIA's of the same names, which reach each platform as WAI-ARIA's Core Accessibility API Mappings say:
| Role | What it is | macOS | Linux (ATK) | Windows (UI Automation) |
|---|---|---|---|---|
ui.RoleRegion |
a landmark with a Label, as an activity feed or a panel; without one, a group |
AXGroup, AXLandmarkRegion |
landmark, xml-roles:region |
Group, the custom landmark type "region" |
ui.RoleSearch |
a landmark holding what searches the app or a document, as the search field and its button at the top of a window | AXGroup, AXLandmarkSearch |
landmark, xml-roles:search |
Group, the search landmark type |
ui.RoleArticle |
a self-contained piece of content, as a post of a feed or a comment of a thread, named by its Label; no landmark |
AXGroup, AXDocumentArticle |
article | Group "article" |
ui.RoleLog |
a live region of messages added in order, as a chat's transcript | AXGroup, AXApplicationLog |
log | Group "log", LiveSetting polite |
ui.RoleTimer |
a count of time, as a countdown, read only when the user goes to it | AXGroup, AXApplicationTimer |
timer | Group "timer" |
ui.RoleAlert |
a live region of important news, as a field's error, read at once | AXGroup, AXApplicationAlert |
notification | Group "alert", LiveSetting assertive |
ui.RoleStatus |
a live region of advisory information, as "3 rows selected" or "Saved", whose whole text is read out politely when it changes | AXGroup, AXApplicationStatus |
notification, or status bar before ATK has it | StatusBar, LiveSetting polite |
ui.RoleSeparator |
a rule between groups of content or controls, running up and down when taller than wide | AXSplitter |
separator, vertical or horizontal | Separator, with Orientation |
ui.RoleRowHeader |
the cell naming its row of a table, named by its content, whose buttons and other elements stay reachable, as a cell's | AXCell |
row header | HeaderItem "row header" |
ui.RoleDeletion |
content removed from a document, as a diff's removed lines or an old price struck out, as HTML's <del>; its text is inside it |
AXGroup, AXDeleteStyleGroup |
content deletion, xml-roles:deletion |
Text "deletion" |
ui.RoleInsertion |
content added to a document, as a diff's added lines, as HTML's <ins> |
AXGroup, AXInsertStyleGroup |
content insertion, xml-roles:insertion |
Text "insertion" |
ui.RoleMain |
the main content of a window, apart from its bars and sidebars, as HTML's <main>; a window has one |
AXGroup, AXLandmarkMain |
landmark, xml-roles:main |
Group, the main landmark type |
ui.RoleBanner |
the top bar of a window, with the app's name or the window's title, as a page's <header>; a window has at most one |
AXGroup, AXLandmarkBanner |
landmark, xml-roles:banner |
Group, the custom landmark type "banner" |
ui.RoleContentInfo |
the footer or status area, with information about the content, as a page's <footer>; a window has at most one |
AXGroup, AXLandmarkContentInfo |
landmark, xml-roles:contentinfo |
Group, the custom landmark type "content information" |
ui.RoleComplementary |
content that completes the main content and stands on its own, as a sidebar or an inspector, as HTML's <aside>; name each with Label when there are several |
AXGroup, AXLandmarkComplementary |
landmark, xml-roles:complementary |
Group, the custom landmark type "complementary" |
ui.RoleTabPanel |
the content of a tab, named after its tab with Label and pointed to by the tab's Controls, as ARIA's Tabs pattern; no landmark |
AXGroup, role description "tab panel" |
scroll pane, xml-roles:tabpanel |
Pane "tab panel" |
Live regions say how they are read (AXARIALive, ATK's live and
container-live attributes, LiveSetting), and MyGo reads them out as
the web does: once a log has shown, what is added to it is announced,
after what screen readers are reading, as c.Announce does; an alert's
text is read out at once, interrupting, as it appears and whenever it
changes (VoiceOver's announcements of high priority, ATK's notification
signal, assertive, where ATK has it (2.50), else an announcement, and an
assertive live region on Windows); a status's (ui.RoleStatus), when its
text changes, is read out whole, politely, as aria-atomic says. What
they show as assistive technology comes, or as a log or a status first
shows, is not read out, and a timer reads nothing by itself:
ui.Column(c).Role(ui.RoleLog).Label("Messages").Children(func() {
for _, m := range app.messages {
ui.Row(c).Key(m.ID).Gap(4).Children(func() {
ui.Text(c, m.From+":").Bold()
ui.Text(c, m.Text)
})
}
})
if app.nameErr != "" {
ui.Text(c, app.nameErr).TextColor(t.Danger).Role(ui.RoleAlert)
}
ui.Row(c).Role(ui.RoleToolbar).Label("Format").Children(func() {
boldAndItalic(c)
ui.Divider(c).Role(ui.RoleSeparator)
alignment(c)
})A log reads out the elements it gained, those of each row together; an
alert, its text; a status, all its text, once it changed (nothing when it
is emptied). A status is told from the last frame's by its element, so
give it a Key when elements may come before it; one inside a log, an
alert or another status is read as part of it, not on its own. Keyed rows (Key) keep what a log showed apart from
what it gained when rows come before others.
Screen readers list landmarks and jump to them, so a search field and its
button at the top of a window go in a search landmark, as HTML's
<search>:
ui.Row(c).Role(ui.RoleSearch).Label("Search mail").Gap(6).Children(func() {
ui.SearchField(c, &app.query).Label("Search mail").Grow(1)
if ui.Button(c, "Search").Clicked() {
app.search()
}
})Each post of a feed, or comment of a thread, is an article, as HTML's
<article>, which screen readers step through one by one:
for _, p := range app.posts {
ui.Column(c).Key(p.ID).Role(ui.RoleArticle).Label(p.Title).Gap(4).Children(func() {
ui.Text(c, p.Title).Bold().Role(ui.RoleHeading).Level(3)
ui.Text(c, p.Body)
})
}Content removed and added, as the lines of a diff or an old price beside the new one, says so, rather than being read as plain text:
ui.Row(c).Gap(6).Children(func() {
ui.Text(c, "$40").Strikethrough().TextColor(t.TextMuted).Role(ui.RoleDeletion)
ui.Text(c, "$30").Role(ui.RoleInsertion)
})Each component's page says what assistive technology sees of it.