Intermediate 14 min

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.

src/view.rs
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.md
Compiled example

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

Open exact source
app_examples/docs-examples/src/examples/accessibility.rs
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)
    }
}