Accessibility (§13)

Day starts with the accessibility behavior of its native controls. App code supplies the meaning those controls cannot infer: a label for an icon-only button, a summary for a chart, or the role and value of a custom drawing.

A11yProps carries those annotations to the native accessibility APIs. The dayscript a11y_audit step checks the declared metadata against the native tree on Apple targets. Test keyboard navigation and screen-reader behavior on the platforms you ship; annotations alone do not establish that a screen is usable.

Authoring

button(icon("trash"))
    .a11y(|a| a.label(tr("delete-item").format()).hint(tr("delete-item-hint").format()))
    .id("delete-button")

image("chart").a11y(|a| a.label(tr("q3-chart-summary").format()))   // or .decorative()

gauge(level).a11y(|a| a.role(Role::Meter).label("Volume").value("72"))   // canvas → explicit role

A11yBuilder: .label, .hint, .value, .role(Role), .hidden(), .decorative() (decorative ⇒ hidden + exempt from the “needs a label” lint). .id(_) sets the identifier. Annotations merge onto a node: a piece default, .a11y(), and .id() accumulate (day-core stores the merged A11yProps on the node, re-applies the full picture on each change, and hands it to a11y_audit as the expectation).

Put .id/.a11y before .frame()/.padding() on canvas/leaf pieces. Those wrap in a handle-less layout node, so annotations placed after them wouldn’t reach a native widget.

Roles

Role: None, Button, Toggle, Slider, TextInput, Heading(u8), Image, Meter, Group.

Day only applies an explicit role (the canvas/custom cases, e.g. a Meter gauge). Native controls already report the right role, so Day records their kind-default (Role::for_kind) as the audit expectation but never overrides the widget. resolved_role(kind) = explicit role, else the kind default.

Per-backend mapping (set_a11y)

fieldAppKitUIKitGTKQtAndroid
labelsetAccessibilityLabelaccessibilityLabelProperty::LabelsetAccessibleName (+tooltip)contentDescription
hintsetAccessibilityHelpaccessibilityHintProperty::DescriptionsetAccessibleDescription— (follow-up)
valuesetAccessibilityValueaccessibilityValueProperty::ValueText— (QAccessible subclass)stateDescription (API 30+)
role (explicit)setAccessibilityRole (AXButton/Slider/CheckBox/TextField/StaticText/Image/LevelIndicator/Group)accessibilityTraits (Button/Adjustable/Header/Image)construction-time (follow-up)widget-deriveddelegate (follow-up)
hidden/decorativesetAccessibilityElement(false)accessibilityElementsHiddenState::HiddenimportantForAccessibility=NO
identifiersetAccessibilityIdentifieraccessibilityIdentifierset_widget_name (Inspector)setObjectName

Current state (from §13’s truth table): apple + Qt have full native a11y on all their OSes. GTK has no AT bridge on macOS (it works via AT-SPI on Linux). Android role/hint need an AccessibilityDelegate (deferred). This is why those are secondary combos.

Verification: a11y_audit (§14.2)

The dayscript step a11y_audit: { id? } walks Day’s id’d nodes, reads each widget’s actual native a11y (Toolkit::read_a11y), and diffs identifier + label + value + explicit-role against Day’s stored expectation. Backends that can’t read their native tree (found=false) skip. Apple targets implement read_a11y (NSAccessibility / UIAccessibility → Day Role); it is required in the CI walkthrough on apple targets and passes there (the showcase gauge audits as role=Meter/AXLevelIndicator + label + value + id). Role is diffed only for explicit roles Day applied, since native controls own their roles, which vary per platform.

Follow-ups

  • Reactive a11y strings (.value_with(|| …) / IntoText on a11y; currently build-time snapshots).
  • GTK/Android/canvas construction-time roles; Qt QAccessibleInterface value/role.
  • day lint a11y rule: interactive piece without a derivable label → warning (--strict error).
  • read_a11y for Qt/GTK so a11y_audit runs on the desktop-toolkit combos too.