Skip to content

Design Accessibility

MeowLynxSea edited this page Jun 18, 2026 · 3 revisions

Accessibility

Yororen UI ships a foundation in the a11y module — focus management, keyboard navigation, click-outside detection, scroll lock. The screen-reader bridge is intentionally deferred until gpui-ce ships its public AccessKit integration.

The four a11y modules

Module When to use
a11y::focus_trap Modal-style overlays that must contain keyboard focus.
a11y::click_outside Popovers, dropdowns, context menus that should close on outside click.
a11y::scroll_lock Nested modals — prevent body scroll while a modal is open.
a11y::keyboard_nav FocusableList + cycle_focus for arrow-key navigation across rows.

All four are opt-in per element. There is no global "a11y mode".

FocusTrap

use yororen_ui::a11y::focus_trap;

let trap = focus_trap()
    .id("modal-trap")
    .on_escape(|window, cx| { /* close the modal */ })
    .child(modal_content);

The companion FocusTrapState provides activate(window, cx) / deactivate(window, cx) — they capture and restore the focused element so a modal returns focus to its trigger when closed.

ClickOutsideGuard

use yororen_ui::a11y::on_click_outside;

let popover_div = div()
    .absolute()
    .child(menu_content)
    .apply(|d| on_click_outside(d, |_ev, cx| {
        // popover_state.update(cx, |s, _| s.close());
    }));

ScrollLockGuard

A process-global counter that increments when acquired and decrements when dropped. RAII via Drop. Nested modals compose naturally.

use yororen_ui::a11y::ScrollLockGuard;

let _lock = ScrollLockGuard::acquire();   // body scroll now locked
// when the modal closes, `_lock` drops and the counter releases

FocusableList + cycle_focus

use yororen_ui::a11y::{cycle_focus, FocusDirection, FocusableList};

let mut list = FocusableList::new();
list.push(("row", "play"));
list.push(("row", "pause"));
list.push(("row", "stop"));

let next_id = cycle_focus(&list, current, FocusDirection::Next);
cx.focus(next_id);

Screen-reader bridge

yororen_ui_core::a11y says it plainly: Yororen UI does not ship a screen-reader bridge on top of gpui-ce 0.3.x. The underlying gpui-ce 0.3.x release has no public API to forward ARIA roles / labels to the OS accessibility tree, and gpui-ce's in-progress branch with a full AccessKit-based bridge is not yet published.

What works today:

  • Tab / Shift+Tab / arrow-key navigation.
  • Escape to close overlays.
  • Focus rings.
  • role semantics in your own code (use the right component for the job).

What doesn't work today:

  • VoiceOver / NVDA / Orca announcing component roles.
  • ARIA live regions.
  • Structural landmarks reaching the OS accessibility tree.

Until the AccessKit bridge lands, keep semantic intent in your component tree (button not div().on_click(...), checkbox not switch, select not a hand-rolled listbox). The day the bridge lands, your app is ready without rewrites.

Best practices

  1. Use the right component for the job. Buttons trigger actions; checkboxes toggle boolean state; selects open a listbox; modal dialogs block focus.
  2. Always provide stable ids for stateful elements. Tab order, focus restoration after modal close, and the future screen-reader bridge all depend on them.
  3. Wire on_escape on overlays. Modals, popovers, dropdowns should all close on Escape.
  4. Acquire ScrollLockGuard on modal open. RAII — don't forget to drop.
  5. Test with keyboard only. No mouse. Tab through every interactive element; verify Escape closes overlays; verify arrow keys move within listbox / tree / menu.

See also

Clone this wiki locally