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
| Keys | Behavior |
|---|---|
| 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.
| Keys | Behavior |
|---|---|
| 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.
| Keys | Behavior |
|---|---|
| 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.
| Keys | Behavior |
|---|---|
| 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.
| Keys | Behavior |
|---|---|
| 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
| Keys | Behavior |
|---|---|
| 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
| Keys | Behavior |
|---|---|
| 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".
| Keys | Behavior |
|---|---|
| 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
| Keys | Behavior |
|---|---|
| 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
| Component | Semantics |
|---|---|
| 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
- The indeterminate progress bar's animation freezes; the static striped bar still reads as "in progress".
- Spinners keep turning (the motion is the signal) but slow from 0.7 s to 1.6 s per revolution.
- Flag slide-in/out transitions are disabled; flags appear and disappear instantly.
forced-colors: active (Windows High Contrast)
- Icons are
mask-image+ background-color, and forced-colors strips author backgrounds; they repaint with the systemButtonTextcolor so glyphs survive. - Selection states that rely on background alone (the
dropdown's active item and the date picker's selected day)
switch to
Highlight/HighlightText. - Progress bars keep their shape with a
CanvasTextborder and paint the value withHighlight. - The spinner is drawn with borders (not backgrounds), so it stays visible without extra rules.
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:
- Label icon-only controls. A button or
trigger whose only content is an icon needs an
aria-label:<button class="neura-button" aria-label="Close"><span class="neura-icon neura-icon-close"></span></button>. - Give avatars alt text.
<img alt="Tomas">when the identity matters,alt=""when the avatar is decorative next to the visible name. - Don't remove the focus ring. If it clashes
with your design, restyle
:focus-visible; never useoutline: nonewithout a visible replacement. - Pair inputs with labels. Every form field
needs a
<label for="…">(or a wrapping label); placeholder text is not a label. - Escape user data in HTML-injecting hooks.
bodyHtml(messages, flags, banners), tooltiphtml: true, selectformatResult/formatSelection, and restful-tablereadViewall inject raw markup: escape anything user-supplied before it goes in, or use the plain-text options (body, default formatters), which are escaped for you.