Accessibility

Summary

Neura is keyboard-first: every interactive component can be operated without a pointer, keyboard focus is always visible, and overlay components manage focus for you (trap, restore, return-to-trigger). Screen-reader semantics (roles, accessible names, and live regions) are applied by the components themselves, and the stylesheet responds to prefers-reduced-motion and forced-colors so the library holds up under OS-level accessibility settings. The library is built with WCAG 2.1 in mind; the brand palette is chosen for contrast (primary on white 8.9:1, link on white 6.3:1). All of this is the library's part of the deal - your part as a consumer is at the bottom of this page.

Focus management

Focus rings

Every interactive element (a, button, input, select, textarea, [tabindex]) gets a 2px outline in --neura-color-focus on :focus-visible - that is, for keyboard focus only. Mouse interactions keep the AUI-parity hover/focus look with no extra ring. Inside the dark header bar the focus token would be invisible, so the ring switches to the header text color (white) there. Components whose CSS suppresses the mouse-focus outline (date-picker day cells, footer and month-nav buttons, quicksearch, progress-tracker steps) restate the keyboard ring at matching specificity, so no control loses it. All of this lives in one auditable file, src/css/_a11y.scss.

Dialogs: trap and restore

Opening a dialog moves focus to its first focusable element (or to the dialog container itself, via tabindex="-1", when it contains none). While the dialog is open, Tab and Shift+Tab are trapped: tabbing past the last control wraps to the first and vice versa. Closing the dialog restores focus to the element that was focused before it opened.

Popovers: focus return

Dropdown menus hand focus back to their trigger when they close, if keyboard navigation had moved focus into the menu; it is never dropped on <body>. The date picker's Escape returns focus to its input (without re-opening the popover from the input's focus event). The select control takes focus back after a choice is made in single mode.

Keyboard reference

Dialogs

KeysBehavior
Tab / Shift+Tab Move through the dialog's controls; focus is trapped and wraps at both ends.
Esc Close the dialog. Ignored on a strict modal (data-neura-modal="true") - those close only via the header close button or an explicit .hide().

Dropdown menus

The trigger is a button, so native activation (Enter/Space) toggles the menu. On open, the first enabled item is pre-marked with the cursor while DOM focus stays on the trigger; the first arrow or Tab press starts moving real focus through the items (so screen readers announce each one). Mouse hover moves the cursor visually without moving focus.

KeysBehavior
ArrowDown / Tab Next enabled item, wrapping past the end.
ArrowUp / Shift+Tab Previous enabled item, wrapping.
Enter Invoke the current item: follows links, toggles checkbox/radio items (the menu stays open for those).
Esc Close the menu; focus returns to the trigger.

Tabs

Selection follows focus: moving to a tab activates its panel; no separate activation key is needed.

KeysBehavior
ArrowRight / ArrowDown Next tab, wrapping. Horizontal arrows follow visual direction: in RTL, ArrowLeft is "next".
ArrowLeft / ArrowUp Previous tab, wrapping.
Home / End First / last tab.

Date picker

The popover opens when the input gains focus or is clicked, but never steals focus: the input stays typeable (you can enter an ISO date directly) and Tab users are not trapped. The day cells use a roving tabindex: only the current day is in the tab order.

KeysBehavior
ArrowDown (in the input) Open the popover if needed and move focus into the day grid.
ArrowLeft / ArrowRight Previous / next day. Follows visual direction: in the RTL grid days run right-to-left, so ArrowLeft moves forward a day.
ArrowUp / ArrowDown Same weekday, previous / next week.
Home / End Start / end of the current week.
PageUp / PageDown Previous / next month; with Shift, previous / next year.
Enter / Space Select the focused day.
Esc Close the popover and return focus to the input (without instantly re-opening it).

Select

Opening the single-select control moves focus into its search field; the multi variant has an inline search field that opens the menu on focus. The list cursor is virtual: it is reported to assistive tech via aria-activedescendant while focus stays in the search field.

KeysBehavior
Enter / Space / ArrowDown (closed) Open the menu.
Any printable character (closed, single) Open and start filtering with that character.
Typing Filter the options; a "no matches" row shows when nothing matches.
ArrowDown / ArrowUp Move the active option (stops at the ends; does not wrap).
Enter Choose the active option. In single mode the menu closes and focus returns to the control.
Esc Close the menu.
Backspace (multi, empty search) Remove the last selected pill.

Tooltip

KeysBehavior
Keyboard focus Shows the tooltip immediately; Tab users don't wait out the 500 ms pointer-rest delay. (Mouse-click focus keeps the delay.)
Esc Dismiss the tooltip.
Blur / focus out Hides the tooltip.

Sortable tables

KeysBehavior
Tab Sortable column headers are focusable (tabindex="0").
Enter / Space Sort by the focused column, toggling ascending/descending.

Sidebar

The resize handle is a focusable role="separator".

KeysBehavior
ArrowLeft / ArrowRight Shrink / grow the sidebar by 10 px; with Shift, 50 px. The keys follow visual direction (in RTL the sidebar grows to the left).
Home / End Jump to the minimum / maximum width.

Inline dialog

KeysBehavior
Enter / Space (on the trigger) Native trigger activation toggles the popover. Opening does not move focus - Tab continues from the trigger.
Esc Close the popover (topmost layer first). Clicking outside it, or scrolling the page, also closes it.

Expander

The expander adds no key handling of its own; expanding and collapsing ride on the trigger's native click activation, and the component keeps aria-expanded / aria-hidden in sync. Use a naturally keyboard-operable element as the trigger - a <button> (Enter and Space) or a link with an href (Enter). A bare <a> without href is neither focusable nor activatable from the keyboard.

Keyboard shortcuts

Neura.shortcuts registers app-level key sequences ("g then d") and modifier combos. Two guards keep them out of the way: shortcuts never fire while an input, textarea, select, or contenteditable element has focus, and never while a modal dialog is open. Sequences reset after one second of inactivity, and targets bound with click() and friends get a "(Type g then d)" note appended to their title.

Screen readers

ComponentSemantics
Dialog role="dialog" + aria-modal="true"; named via aria-labelledby wired to the header title, so it announces as "Create user, dialog" rather than an anonymous dialog.
Dropdown Menu gets role="menu"; items get menuitem, menuitemcheckbox, or menuitemradio (with aria-checked); structural ul/li get role="none". Roles are re-applied on every show, so ajax-replaced menu content is covered. The trigger carries aria-haspopup + aria-expanded, and keyboard navigation moves real DOM focus so each item is announced.
Tabs tablist / tab / tabpanel roles, aria-selected, a roving tabindex, aria-controls from tab to panel and aria-labelledby back. AUI's role="application" is deliberately not ported: it forced screen readers out of browse mode, making panel content unreadable.
Date picker Popover is a labelled role="dialog" over a role="grid". Each day cell carries a full-date aria-label ("24 July 2026"; the visible cell text is just a number), today gets aria-current="date", the chosen day aria-selected="true", out-of-range days are disabled with aria-disabled. The month title is an aria-live="polite" region, and the input advertises the popover via aria-haspopup="dialog" + aria-expanded.
Select combobox / listbox / option roles; aria-multiselectable in multi mode; the virtual cursor is exposed through aria-activedescendant on the search field; pill remove buttons are individually labelled.
Messages, flags, banners Dynamically created instances are live regions: role="alert" for error/warning, role="status" for the rest (banner announcements are status). Static server-rendered messages and banners deliberately get no live-region role: they are part of the page at load, not an update to announce.
Progress role="progressbar" with aria-valuemin="0" / aria-valuemax="1"; .update(v) sets aria-valuenow, and removing it (.setIndeterminate()) is what makes assistive tech read the bar as indeterminate.
Spinner Programmatic spinners get role="status" and a localized "Loading" aria-label, announced politely when they appear.
Tooltip role="tooltip"; while visible, the trigger points at it with aria-describedby, so the hint is read with the control.
Sortable table aria-sort="ascending|descending" on the sorted header; only one column carries it at a time.
Sidebar The handle is a labelled role="separator" with aria-orientation="vertical" and aria-valuenow / -valuemin / -valuemax tracking the width.

Reduced motion and forced colors

prefers-reduced-motion: reduce

forced-colors: active (Windows High Contrast)

RTL

Under dir="rtl", horizontal arrow keys follow visual direction per ARIA practice: in the tab strip ArrowLeft moves to the next tab, and in the date grid ArrowLeft moves forward a day (weeks, months, and Home/End stay temporal). The sidebar's resize keys mirror too. Layout, popover alignment, and the RTL locales themselves are covered on the i18n page.

Your part as a consumer

The library carries the component-level work; a few things only you can do: