Skip to main content

Accessibility

Accessibility principles, keyboard interaction models, and ARIA contracts in Moraine.

View as Markdown

Moraine implements keyboard navigation, focus management, and ARIA relationships based on the W3C WAI-ARIA Authoring Practices. Applications still need accessible names, readable color contrast, and appropriate semantics for custom renderers and polymorphic elements.

Keyboard Interaction Models#

Moraine components implement predictable keyboard contracts based on established UI patterns:

Collection Navigation#

Tabs uses roving tabindex: the highlighted tab has tabindex="0", and other tab triggers have tabindex="-1". Select-family controls keep DOM focus on their trigger or input and identify the highlighted option through aria-activedescendant. Menus move focus between their items. These models support arrow navigation without placing every option in the page’s tab order:

Key Action
↓ / → Moves to the next item along the component’s navigation axis.
↑ / ← Moves focus to the previous item.
↖ Moves focus directly to the first enabled item.
↘ Moves focus directly to the last enabled item.
↵ / Space Activates or selects where supported; editable inputs retain normal text entry.

Looping and activation behavior depend on the component and its options. See each component’s keyboard documentation for orientation, disabled items, typeahead, and selection behavior.

Overlay Dismissal#

Dismissible overlays (Dialog, Sheet, Popover, Tooltip, DropdownMenu, ContextMenu) support the Esc key. Only the topmost active overlay handles dismissal, so closing a nested overlay leaves its parent open. Disabling dismissal or preventing the component’s Escape callback can keep an overlay open.

Focus Management#

Focus Trapping in Modals#

Modal surfaces like Dialog and Sheet automatically constrain keyboard focus within their boundary while open. Tabbing past the last interactive element loops focus back to the first, preventing users from interacting with inactive background content.

Focus Restoration#

Modal overlays restore focus to their trigger after closing. Floating overlays restore focus when appropriate, such as keyboard dismissal, while allowing an outside pointer interaction to move focus to its target. Tooltips retain focus on their trigger. Check each component’s focus options when customizing this behavior.

Initial Auto-Focus#

Modal surfaces focus the first focusable child when opened, falling back to the content container when there is no focusable child. Menus and select-family controls use their own highlighted-item behavior. Tooltips do not move focus into their content.

ARIA Attribute Contracts#

Moraine manages dynamic ARIA attributes so you don’t have to manually wire state strings:

  • State Indication: Components maintain aria-expanded="true|false" on triggers, aria-selected="true|false" on selection items, and aria-checked="true|false|mixed" on checkboxes.
  • Relationship Wiring: Triggers and contents are linked via aria-controls pointing to deterministic element IDs generated by createId.
  • Form Association: Field uses its label and description props to bind aria-labelledby and aria-describedby to the registered input control.
  • Validation: When a field has an error, Moraine applies aria-invalid="true" to the input and includes the visible error message in aria-describedby.
import { Field, Input } from 'moraine'

export function AccountField() {
  return (
    <Field label="Email Address" description="We will never share your email.">
      {/* Automatically wires id, aria-labelledby, and aria-describedby */}
      <Input type="email" placeholder="[email protected]" />
    </Field>
  )
}

Screen Reader Considerations#

  • Semantic HTML: Moraine uses native controls such as <button> and <input> alongside ARIA roles for custom widgets and overlay surfaces. When using the as prop, ensure the delegated element provides equivalent semantic meaning.
  • Visually Hidden Labels: For icon-only buttons, provide an accessible name using aria-label:
    <Button aria-label="Close settings dialog" variant="ghost">
      <IconClose />
    </Button>