Accessibility from the start
Give controls stable semantics, keyboard behavior, focus, and useful touch targets.
Prefer semantic widgets
Built-in buttons, inputs, menus, dialogs, and collection widgets publish roles, names, values, states, and actions. Native targets use AccessKit; Web maintains real semantic HTML controls beside the canvas.
Describe custom controls
Attach Semantics when a custom interactive element has no built-in widget. Give it a role, accessible name, current value or selection state, and the actions it accepts. Hide purely decorative descendants to avoid duplicate announcements.
let semantics = Semantics::new(Role::Slider)
.label("Timeline zoom")
.value(format!("{:.0}%", zoom * 100.0));
control.semantics(semantics)Manage focus as retained state
Focusable elements keep stable NodeIds across keyed reconciliation. Use focus scopes for restoring, trapped, or modal focus. Modal scopes also remove background nodes from the active semantic tree.
Reference files
These are the implementation and guide files used for this chapter.
crates/argui/examples/accessibility.rscrates/argui-accessibility/src/schema.rsdocs/ui/interaction.mdAn accessible input in the browser semantic tree
The Rust file below is imported verbatim by this page and compiled into the WebAssembly application running underneath it.
use argui::{
accessibility::{LiveRegion, Role, Semantics},
runtime::{Context, Render},
text::TextStyle,
ui::{Element, Sides, percent},
widgets::{Button, default_theme},
};
#[derive(Default)]
pub struct Example {
announcements: u32,
}
impl Render for Example {
fn render(&mut self, cx: &mut Context<Self>) -> Element {
let themes = default_theme(cx.environment());
let theme = themes.resolve(cx.environment().color_scheme);
let status = format!("{} accessible activations", self.announcements);
Element::column([
Element::text("Accessible by construction")
.text_style(TextStyle {
color: theme.foreground,
..TextStyle::default()
})
.semantics(Semantics::new(Role::Heading).level(1)),
Button::new("announce", "Announce update", theme.button())
.on_click(cx.callback(|app| {
app.announcements = app.announcements.saturating_add(1);
}))
.build(),
Element::text(status.clone())
.text_style(TextStyle {
color: theme.foreground,
..TextStyle::default()
})
.semantics(
Semantics::new(Role::Status)
.label(status)
.live(LiveRegion::Polite),
),
])
.width(percent(1.0))
.height(percent(1.0))
.padding(Sides::length(28.0))
.gap(16.0)
.background(theme.background)
}
}
Starting this lesson’s WebAssembly module…