Skip to main content

Accessibility

The widget is built to comply with WCAG 2.2 AA. This page lists what the widget does for you and what your integration must keep respecting.

Compliance per criterion

WCAG criterionImplementation
1.4.3 Minimum contrastLight/dark tokens with contrast ≥ 4.5:1 on text and 3:1 on UI components.
1.4.10 ReflowResponsive layout without horizontal scroll at 320 px.
1.4.13 Content on hover/focusTooltips dismissible with Esc and persistent until blur.
2.1.1 KeyboardThe full UI is keyboard-accessible. No traps.
2.1.2 No keyboard trapModals trap focus with Esc escape and focus restoration.
2.4.3 Focus orderLogical order: input → toolbar (voice/image) → results → filters.
2.4.7 Focus visibleFocus rings using --nrn-primary at 4 px.
3.2.2 On inputSubmit does not fire until the user presses Enter or the button.
3.3.2 LabelsEvery input has an aria-label or an associated <label>.
4.1.2 Name, role, valuecombobox, listbox, option, dialog, status correctly applied.
4.1.3 Status messagesState changes announced with role="status" (results loaded, filters applied).

Shadow DOM and assistive tools

The widget mounts in a Shadow DOM in open mode. Modern screen readers (NVDA, JAWS, VoiceOver, TalkBack) navigate correctly across the Shadow Tree nodes and the host document.

Keyboard navigation

KeyAction
Tab / Shift+TabMove focus across widget elements.
EnterTrigger search with the input query. Activates buttons and links.
EscClose any open modal/drawer/dropdown. If nothing is open, clears the query.
/ Navigate suggestions and results.
/ Navigate carousels (top products, comparison, kit).
Cmd+K / Ctrl+KOptional shortcut to open the widget from the host (you declare it in your template and call widget.openChat?. or programmatically focus the input).

Focus trap in modals

The widget includes a generic focus trap (createFocusTrap) that activates when any dialog opens (voice search, image search, comparator, filter drawer, cart drawer). Behavior:

  • On activation, focus moves to the first focusable element or the declared initialFocus.
  • Cycles with Tab / Shift+Tab inside the container.
  • On close, focus is restored to the element that opened it.

ARIA on key components

// SearchInput
<input
role="combobox"
aria-expanded={showSuggestions}
aria-controls="neuroon-suggestions"
aria-activedescendant={activeSuggestionId}
aria-autocomplete="list"
aria-label={t('search.inputAriaLabel')}
/>

<ul role="listbox" id="neuroon-suggestions" aria-label={t('suggestions.autocompleteAriaLabel')}>
<li role="option" aria-selected={isActive}></li>
</ul>

aria-label keys live in the i18n bundle and are translated to the active locale.

Reduced motion

The widget honors prefers-reduced-motion: reduce:

export function prefersReducedMotion(): boolean {
if (typeof window === 'undefined') return false
return window.matchMedia('(prefers-reduced-motion: reduce)').matches
}

Screen reader announcements

The widget emits role="status" / aria-live="polite" announcements when:

  • Results finish loading (Showing N of T products).
  • A filter is applied/removed (Brand filter: Apple applied).
  • A product is added/removed from the comparator.
  • The AI assistant finishes "thinking".

Keys live under filters.filtersAndSuggestions, comparison.panelExpanded, cart.a11y.itemAdded, etc..

Mobile inputs

All inputs use font-size >= 16px on mobile to avoid iOS auto-zoom. The widget also sets touch-action: manipulation and -webkit-text-size-adjust: 100% on root to normalize behavior across browsers.

Best practices for the host

  • Avoid wrapping the widget inside a container with overflow: hidden that clips modals — the widget renders modals into its own portalContainer inside the Shadow Root, but if the host constrains position: fixed from an ancestor with transform, the modal can be clipped.
  • If your theme forces prefers-reduced-motion: reduce via its own toggle, propagate the media query: the widget reads it directly from window.matchMedia.
  • Do not reorder shadow tree nodes from the host. The tree is controlled by Preact.

Further reading