Accessibility
Native controls expose roles and actions to assistive technology, but an app must still label ambiguous controls and describe meaningful custom graphics. Day’s accessibility API supplies these annotations and excludes decorative content from the accessibility tree. This guide covers the annotations, testing, and current platform limits.
Annotating pieces
// An icon-only button: its accessible name can't be derived, so provide one.
button("✕")
.a11y(|a| a.label(tr("close").format()))
.id("close-button")
// A custom-drawn gauge: no native control underneath, so declare role and value.
canvas(move |d, size| draw_gauge(d, size, value.get()))
.a11y(|a| a.role(Role::Meter).label(tr("cpu_usage").format()))
.frame(120.0, 120.0)
// Decorative artwork: remove it from the accessibility tree entirely.
image(res::images::hero_banner).a11y(|a| a.decorative())
Put .id and .a11y before .frame() and .padding() on canvas and leaf pieces: those
modifiers wrap the piece in a layout-only node, so annotations placed after them don’t reach
the native widget.
The builder covers label, hint, value, role, hidden, and decorative. Labels are
plain strings (tr(...).format() localizes one), taken as a snapshot at build time: they don’t
re-resolve on a locale switch yet (a listed follow-up). Native controls report their roles on
their own, but no built-in invents a label for you; a toggle has no title parameter, so a
titled switch is labeled("Subscribe", toggle(subscribed)). Your annotations merge onto the
node’s defaults.
Hold yourself to these rules (day lint has no a11y rules yet; a missing-label warning for
interactive pieces is a listed follow-up):
- give every interactive Piece whose purpose isn’t its text an accessible name;
- never let an element id double as a label; a screen reader would read
"save-button"aloud.
Identifiers
.id("save-button") sets a stable identifier used by three consumers: dayscript
element targeting, external automation tools, and Day’s diagnostics. For external tools the
platform mapping is uneven:
| Platform | Identifier surface |
|---|---|
| iOS / macOS | accessibilityIdentifier — full support (XCUITest etc.) |
| Windows (XAML, Qt) | UIA AutomationId — full support |
| Android | — (the backend maps label, value, and hidden; it sets no automation id today) |
| GTK | no public settable AT-SPI id today — inspector-visible only |
| Web | DOM id |
dayscript is unaffected by this table; it resolves ids inside the app, uniformly everywhere. The table matters only when pointing external tooling (Appium, UIA scrapers) at a Day app.
Auditing the native tree
Setting an attribute and the platform exposing it are different things, so Day includes an audit step that reads the native accessibility tree back and diffs it against what your code declared:
# in a dayscript flow
- a11y_audit:
The audit walks id’d nodes, asks the toolkit for the realized role, label, and value (via each backend’s read-back hooks), and fails the script on mismatch. Run in CI, it fails the build when a declared label stops reaching the native tree. Read-back is implemented on the Apple backends (AppKit and UIKit); backends that can’t yet read their native tree skip the audit (read-back for Qt and GTK is a listed follow-up).
Current limits
- GTK off Linux has no accessibility tree. GTK’s AT-SPI bridge is Linux-only, so the
macos-gtkandwindows-gtkdevelopment combos (Tier 4) are invisible to screen readers. Ship the platform-native target for real users. - Android annotations are partial: labels and values map today (
contentDescription, state description); role and hint refinement is still open, and audit read-back isn’t implemented. - Qt’s
QAccessiblelayer bridges to the native accessibility API on every OS (no other Day backend does that today), which makeslinux-qta reasonable choice when Linux accessibility is a hard requirement. - Reactive values: an a11y
valueset at build time is a snapshot; live values (a slider announcing as it moves) work through the control’s native behavior, but custom reactive a11y values on canvas pieces are still a designed-not-built refinement. - Focus order follows layout order; explicit focus groups and custom sort priority aren’t exposed yet.
day doctor and the audit step report what each target supports. The
accessibility reference has the full per-backend mapping tables.