Accessibility internals

Copy Markdown View Source

GPUI snapshots are renderer-independent, but programmatic accessibility only becomes real when a display maps snapshot semantics into its platform accessibility tree and handles assistive-technology actions. This guide records the boundary for the pinned native stack:

GPUI:           7bddd16a09cf0084cefb3d98468b178343b9f1e2
gpui-component: 5c5eb1db04dc1146bdec903a8ac74407bd6c2098

It is an implementation boundary, not a promise of web ARIA parity.

What the native stack provides

The pinned GPUI integrates with AccessKit. A GPUI element contributes a node only when it has both a stable element ID and an AccessKit role. GPUI currently exposes native builders for:

  • role, label, description, and announced keyboard shortcuts;
  • selected, expanded, and tri-state toggled state;
  • string, placeholder, numeric, minimum, maximum, and step values;
  • orientation, heading level, position/size in set, and table coordinates;
  • active-descendant focus for composite widgets;
  • synthetic accessibility children for custom text-like elements;
  • assistive-technology actions, with click handlers automatically accepting the accessible click action.

GPUI sends AccessKit trees and actions through platform adapters on:

  • macOS via accesskit_macos;
  • Windows via accesskit_windows;
  • Linux X11 and Wayland via accesskit_unix (the platform bridge used for AT-SPI environments).

This means bounded semantic metadata can truthfully reach native assistive technology on all three target families. Actual behavior still depends on the platform adapter, desktop accessibility services, and the completeness of each component's role, identity, state, and action wiring.

What gpui_ex already maps

The native renderer already emits accessibility nodes for several semantic components:

  • images: image role and label;
  • buttons: component-owned button/link role and label behavior;
  • inputs: input-specific roles, labels, values, and placeholders; password values are not exposed;
  • select and combobox: roles, labels, selected visible values, and placeholders;
  • switch and radio group: controlled state, labels, and orientation;
  • slider and progress/display values: numeric value bounds and orientation;
  • popover, dropdown, and dialog triggers: label and expanded state;
  • tree, virtual list, code viewer, and data table: collection roles, selected or expanded state, active descendants, set positions, and table coordinates.

These mappings are component policy. Applications should not need to recreate native roles for ordinary semantic controls.

Renderer-independent contract direction

Accessibility facts added to the public schema must remain bounded, serializable, and useful to every display. The first neutral vocabulary should be limited to facts directly supported by GPUI/AccessKit:

accessibility_role
accessibility_label
accessibility_description
accessibility_value
accessibility_selected
accessibility_expanded
accessibility_checked
accessibility_orientation

Numeric value metadata, heading/set/table coordinates, active descendants, and synthetic text children should be added only with a concrete component need and behavioral tests. Native semantic elements keep their implicit roles; explicit role overrides are primarily for custom generic elements and must use a closed role vocabulary.

Accessibility metadata belongs in the element snapshot. Therefore test and remote displays can inspect and transport the same facts even though only a native display publishes an operating-system accessibility tree.

The first neutral schema slice is available on generic container primitives:

<div
  id="retry"
  accessibility_role="button"
  accessibility_label="Retry connection"
  accessibility_description="Attempts to reconnect to the remote host"
  phx-click="retry"
/>

Explicit metadata requires a non-empty stable id, and labels or descriptions require an explicit role. Roles use a closed vocabulary and labels/descriptions are bounded to 512/2048 bytes. Semantic native components retain their component-owned roles; this generic contract does not override them. These facts remain in the renderer-independent snapshot, survive remote transport, and map to GPUI's AccessKit role, label, and description builders.

The closed vocabulary, metadata detection, stable identity requirement, and cross-field name/description rules are centralized in GPUI.Accessibility. Future selected, expanded, checked, value, and orientation attributes must add their role/state rules there rather than duplicating policy in serialization or native rendering.

The state vocabulary now also includes:

accessibility_value
accessibility_selected
accessibility_expanded
accessibility_checked
accessibility_orientation

Checked values are true, false, or :mixed; orientation is horizontal or vertical. Checked is restricted to checkbox/radio/switch, selected to list items/tabs/tree items, expanded to buttons/tree items, orientation to sliders/splitters/tab lists/trees, and string values to progress/sliders/text boxes. State changes retain the same element ID and travel through ordinary local or remote snapshots before mapping to AccessKit.

The Elixir specification is also the RustQ source for native role, checked, and orientation enums, the grouped AccessibilitySemantics struct, bounded decoders, and GPUI conversion matches. Handwritten Rust only applies those generated typed values to GPUI's accessibility builders; it does not repeat the transport vocabulary.

Generic elements with phx-click register both GPUI pointer activation and an explicit AccessKit Click action listener. Both callbacks call the same native event helper and therefore produce the ordinary NativeEvent::Click route; there is no accessibility-specific Elixir callback or state mutation. A role without phx-click remains semantic and does not register an accessible Click action.

Role specifications classify interaction as structural, simple activation, composite, or value-oriented. RustQ generates the native exhaustive is_activatable match. Generic button, checkbox, link, radio, and switch roles require phx-click; native rendering makes those controls Tab stops with a visible keyboard focus border. GPUI's pinned click interactivity then handles Enter/Space on focused controls, including key-up-only activation and focus-generation cancellation, through the same pointer click callback. Structural, composite, and value roles do not automatically become Tab stops.

accessibility_disabled is a controlled boolean for button, checkbox, link, radio, slider, splitter, switch, and textbox roles. RustQ generates a concrete disabled: bool field defaulting to false. Disabled nodes retain stable IDs and controlled value/check state, report AccessKit disabled state, and install no pointer, keyboard, Tab-stop, or explicit AccessKit Click activation. Re-enabling the same node restores its normal generated interaction policy.

Controlled dialogs use gpui-component's native Role::Dialog, focus trap, and selection scope. Opening focuses the dialog content handle once. Every close path—Escape, overlay/close-button callback, or an application-controlled false transition—restores the stable trigger focus handle when that trigger is still present. Triggerless dialogs skip restoration safely. Escape requests the ordinary controlled close event only when both keyboard and closable are true; Rust does not independently own open state. The trigger continues to report the title and controlled expanded state.

Validation rules

A future schema must reject misleading combinations instead of silently approximating them. At minimum:

  • a node with an explicit role must also have a stable renderer identity;
  • checked is restricted to check box, radio, switch, and equivalent toggle roles;
  • selected is restricted to selectable collection items and tabs;
  • expanded is restricted to disclosures and expandable composite items;
  • orientation is restricted to roles such as slider, splitter, radio group, tab list, and applicable collections;
  • masked input values must never be serialized as accessible values;
  • disabled, read-only, modal, focus restoration, and hidden/decorative behavior must not be claimed until the pinned GPUI API and component action paths can represent them truthfully.

Unknown roles and unsupported state combinations should fail schema validation while preserving their source spelling in diagnostics.

Identity is part of accessibility

A role alone is insufficient. GPUI derives AccessKit node identity from the hierarchy of element IDs. IDs must be unique within a frame and stable across frames; otherwise assistive technology observes removal and insertion rather than a meaningful update. Collection renderers must not derive repeated text nodes from one source location without a stable per-item ancestor or explicit ID.

The public accessibility contract must reuse the framework's renderer identity rather than introduce a second application-visible accessibility ID namespace.

Keyboard and focus are separate obligations

An accessibility tree does not make a component operable. Each semantic component also needs coherent:

  • Tab and Shift-Tab traversal;
  • Enter/Space or arrow-key activation where appropriate;
  • disabled-state exclusion from focus and actions;
  • composite active-descendant behavior;
  • modal focus containment and restoration;
  • monotonic focus requests for imperative restoration.

Accessible actions must invoke the same consumer-owned event path as pointer or keyboard activation. They must not mutate application assigns independently in Rust.

Explicitly unsupported claims

Until separately implemented and validated, gpui_ex must not claim:

  • arbitrary ARIA attribute compatibility;
  • browser accessibility-tree or DOM behavior;
  • live regions, relationships such as labelledby/describedby, or arbitrary ownership graphs;
  • portable disabled, read-only, modal, hidden, or invalid state solely because similarly named HTML attributes exist;
  • complete screen-reader parity among VoiceOver, Narrator, and Linux AT-SPI;
  • accessibility for visual-only overlays that lack stable IDs, roles, actions, and coherent focus behavior.
  1. Add a generated, closed accessibility vocabulary to renderer-independent element attributes.
  2. Map label and description plus a small role set for generic elements.
  3. Preserve semantic component defaults and reject conflicting overrides.
  4. Add selected, expanded, checked, value, and orientation only alongside role/state validation.
  5. Add snapshot and remote round-trip tests.
  6. Add native AccessKit-tree or platform inspection tests where the adapter offers a deterministic harness.
  7. Audit keyboard and assistive-action equivalence for buttons, choices, inputs, sliders, dialogs, tabs, trees, and split handles.

Visual screenshots are not accessibility evidence. Native accessibility claims require inspecting the platform/AccessKit tree or driving an accessible action.