Accessibility
Introduction
Azul exposes accessibility metadata to assistive technologies (screen readers,
switch navigation, voice control). The way it works is that you attach an
AccessibilityInfo to a node, and the framework publishes it on the next
layout pass.
use azul::prelude::*;
let save = Dom::create_button(
"Save",
SmallAriaInfo::label("Save the document".into())
.with_role(AccessibilityRole::PushButton),
);
There are two entry points. SmallAriaInfo covers the common case (label,
role, description). AccessibilityInfo carries the full record (states,
actions, accelerators, live regions, label/describe-by relationships).
Adding a label, role, and description with SmallAriaInfo
SmallAriaInfo is a builder over three optional fields:
pub struct SmallAriaInfo {
pub label: OptionString,
pub role: OptionAccessibilityRole,
pub description: OptionString,
}
Construct with SmallAriaInfo::label(text) and chain with_role and
with_description:
use azul::prelude::*;
let aria = SmallAriaInfo::label("Submit form".into())
.with_role(AccessibilityRole::PushButton)
.with_description("Sends the form to the server.".into());
label is the accessible name screen readers announce. role overrides the
role that would otherwise be inferred from the HTML tag. description is read
after the label as supplementary context. Use it sparingly. It does not
replace a missing label.
Constructors that require accessibility info
The accessible variants of the Dom::create_* helpers take a SmallAriaInfo
parameter. Each interactive element comes in two forms.
Dom::create_button(text, aria), with the escape hatchcreate_button_no_a11y(text).Dom::create_a(href, text, aria), with the escape hatchcreate_a_no_a11y(href, text).Dom::create_input(ty, name, label, aria), with the escape hatchcreate_input_no_a11y(ty, name, label).Dom::create_textarea(name, label, aria), with the escape hatchcreate_textarea_no_a11y(name, label).Dom::create_select(name, label, aria), with the escape hatchcreate_select_no_a11y(name, label).Dom::create_table(caption, aria), with the escape hatchcreate_table_no_a11y().Dom::create_label(for_id, text, aria), with the escape hatchcreate_label_no_a11y(for_id, text).
The _no_a11y variants are deliberately verbose. Use them only when the
accessible name comes from somewhere else, for example an icon-only link
whose name comes from a sibling image's alt text.
For elements you build with Dom::create_div().with_child(...), attach a label
through Dom::with_accessibility_info(AccessibilityInfo).
Going beyond labels: AccessibilityInfo
AccessibilityInfo is the full record:
pub struct AccessibilityInfo {
pub accessibility_name: OptionString,
pub accessibility_value: OptionString,
pub description: OptionString,
pub accelerator: OptionVirtualKeyCodeCombo,
pub default_action: OptionString,
pub states: AccessibilityStateVec,
pub supported_actions: AccessibilityActionVec,
pub labelled_by: OptionDomNodeId,
pub described_by: OptionDomNodeId,
pub role: AccessibilityRole,
pub is_live_region: bool,
}
Set this when you need any of: dynamic states, custom action lists, keyboard accelerators, label-by/describe-by relationships, or live regions.
AccessibilityRole: what kind of element this is
AccessibilityRole is an enum over the standard role set. Most HTML tags are
mapped automatically. You set role only when the visual element does not
match its tag, for example a div styled as a tab strip:
use azul::prelude::*;
let tab = SmallAriaInfo::label("Settings tab".into())
.with_role(AccessibilityRole::PageTab);
Roles commonly used in app code:
PushButtonfor a clickable button.Linkfor a hyperlink.CheckButtonfor a checkbox.RadioButtonfor a radio button.ComboBoxfor a dropdown with text input.DropListfor a dropdown without text input.Sliderfor a slider.SpinButtonfor a numeric stepper.ProgressBarfor a progress indicator.MenuItemfor a menu item.PageTabandPageTabListfor an individual tab and its strip.OutlineandOutlineItemfor a tree view and its nodes.ListandListItemfor a list and its items.Table,Row, andCellfor a data grid.Dialogfor a modal dialog.Alertfor a non-modal notification.Tooltipfor a hover tooltip.
Unknown is the default fallback and tells the platform to infer from the
tag. Nothing explicitly hides the element from assistive tech.
AccessibilityState: dynamic state
States live on AccessibilityInfo.states. Push them whenever the visual state
changes. A toggled checkbox flips CheckedFalse to CheckedTrue. A busy
panel adds Busy until loading finishes.
Unavailable: disabled or grayed out.Selected: item is selected (independent of focus).Focused: has keyboard focus (managed automatically).CheckedTrueandCheckedFalse: binary toggle.Readonly: not editable.Default: the default action button in a dialog.ExpandedandCollapsed: disclosure state.Busy: element is unresponsive while working.Offscreen: scrolled out of view.Focusable: can receive focus.SelectableandMultiselectable: container supports selection.LinkedandTraversed: hyperlink, possibly visited.Protected: sensitive content (passwords).
State changes only become visible to assistive tech after the next layout pass
rebuilds the a11y tree. Return Update::RefreshDom from your callback when
you mutate states so the new tree is shipped.
AccessibilityAction: what assistive tech can do
Actions populate AccessibilityInfo.supported_actions. The platform routes
incoming requests back to your DOM node:
pub enum AccessibilityAction {
Default, Focus, Blur,
Collapse, Expand,
ScrollIntoView,
Increment, Decrement,
ShowContextMenu, HideTooltip, ShowTooltip,
ScrollUp, ScrollDown, ScrollLeft, ScrollRight,
ReplaceSelectedText(AzString),
ScrollToPoint(LogicalPosition),
SetScrollOffset(LogicalPosition),
SetTextSelection(TextSelectionStartEnd),
SetSequentialFocusNavigationStartingPoint,
SetValue(AzString),
SetNumericValue(FloatValue),
CustomAction(i32),
}
For most elements you don't need to list actions explicitly. The framework
adds Focus for focusable nodes, Click for nodes with activation behavior,
and the text-editing actions (SetTextSelection, ReplaceSelectedText,
SetValue) for inputs and contenteditable regions. List actions in
supported_actions only when you implement custom behavior, for example
Increment and Decrement on a div-based slider.
Live regions
Set AccessibilityInfo.is_live_region = true on an element whose content
changes while the user is doing something else. Screen readers announce
live-region updates without requiring focus. Typical uses are chat windows,
toast notifications, autosave indicators, and timers.
Headings
Headings use the standard h1 through h6 tags. The framework maps them to the accessibility tree's heading role with the level set, so screen-reader heading navigation works without per-element configuration. Use real heading tags rather than styled divs.
Focus and text editing
The focus manager (covered in events) tracks the focused node. The a11y
tree's focus follows that value so assistive tech moves with the keyboard.
For contenteditable, input, and textarea elements:
- The current text is exposed as the node's value, not its label, so screen readers can announce edits without re-reading the field name.
- The text-editing actions (
SetTextSelection,ReplaceSelectedText,SetValue) are added automatically. - When the focused element has an active selection or cursor, the tree reports character lengths and anchor/focus positions so the screen reader can announce the cursor position or selection length.
Action requests are events
When assistive tech invokes an action, the framework synthesizes the matching native event:
Default,Focus,Blurbecome focus and click events to your callbacks.- Scroll actions adjust the scroll offset of the matching scroll frame.
SetValue,ReplaceSelectedText, andSetTextSelectionbecome text input events delivered to the focused contenteditable.CustomAction(i32)fires whatever event filter your DOM declares for the default action. SetAccessibilityInfo.default_actionto a human-readable description so the screen reader can announce it.
You don't write separate code paths for assistive tech. The same callback that
handles a mouse click handles AccessibilityAction::Default.
Enabling the feature
Accessibility is gated behind the a11y Cargo feature. With the feature off,
the a11y manager is a no-op. You can still set SmallAriaInfo on your DOM
and the data is preserved, but no a11y tree is published.
# Cargo.toml
[dependencies]
azul = { version = "*", features = ["a11y"] }
Coming Up Next
- Built-in Widgets — Built-in widgets and how to write your own
- System Themes — System colors,
@theme,@os, and accessibility queries - Events — Callbacks, event filters, and how state triggers relayout