Status: future · Priority: medium
Note: This is a future plan, not a commitment. The syntax and API shown here are proposals — they can be completely different when actually implemented.
Native screen readers (NVDA, Windows Narrator, macOS VoiceOver, Linux Orca) don't read pixels — they read an accessibility tree that the OS exposes. Windows, macOS, and Linux each have one: UI Automation (UIA), NSAccessibility, and AT-SPI.
The problem Morph must solve: it draws everything itself through OpenGL — there are no native widgets, so nothing is exposed to the OS by default. The OS sees a blank canvas. Native toolkits get this for free; custom renderers (Electron embeds Chromium, Qt has QAccessible) have to build the tree by hand.
Each frame, Morph walks the layout tree and produces a parallel accessibility tree, then publishes it to the OS:
| OS | Mechanism |
|---|---|
| Windows | UI Automation (UIA) — IUIAutomation provider on the HWND |
| macOS | NSAccessibility protocol / Accessibility API (AXUIElement) |
| Linux | AT-SPI over D-Bus |
The layout node → accessibility element mapping mirrors HTML → ARIA:
button, text, heading, link, list, listitem, image, navigation, dialog, tab, checkbox, radio, slider, table…aria-label)ARIA-style attributes on any element, resolved at compile time:
<button role="button" aria-label="Close" onClick={close}>×</button>
<div role="navigation" aria-label="Main">
<a href="#" tabindex={0}>Home</a>
</div>
<input type="text" placeholder="Search" aria-label="Search documents" />
<div aria-live="polite">{toastMessage}</div> {/* announced without stealing focus */}
<div aria-hidden={true}>decorative divider</div>const win = useWindow()
win.announce("Invoice saved") // push a live-region announcement
win.focus() // move focus to the window
win.focusNext() / win.focusPrev()tabindex)| Piece | State |
|---|---|
| Semantic, HTML-like JSX (the source of roles/names) | ✅ Shipped |
| OpenGL renderer (custom-drawn — nothing exposed to the OS today) | ✅ Shipped — this is what must change |
Keyboard focus plumbing (partial — TextInput work covers caret & focus) |
🚧 Scaffold (see Text Input) |
| OS accessibility bridge (UIA / NSAccessibility / AT-SPI) | ❌ Not built |
aria-* attribute parsing + accessible-tree IR |
❌ Not built |
layout node → role/name/value/state mapping in the compileraria-* attribute parsing + win.announce() / live regions