Accessibility primitives — ARIA live regions, aria-* attributes, role, prefers-reduced-motion.

Module a11y | Source packages/front/fw/src/dom/utils/a11y.js | Deps none | Worker-safe no

This module covers the low-level mechanics: screen-reader announcements via aria-live, aria-* attribute hygiene helpers, and prefers-reduced-motion detection. Business messages (localised text, domain logic) are the responsibility of the application layer or sde/sdc.

Resolve

const a11y = runtime.resolve('a11y');
// Returns: { announce, clearAnnouncements, aria, labelledBy, describedBy, setRole, removeRole, prefersReducedMotion }

API

Method Signature Returns
announce (text: string, politeness?: 'polite'|'assertive') => void void
clearAnnouncements () => void void
aria (el: Element, attrs: Object) => void void
labelledBy (el: Element, idsOrEls: string|Element|Array) => void void
describedBy (el: Element, idsOrEls: string|Element|Array) => void void
setRole (el: Element, role: string|null|false) => void void
removeRole (el: Element) => void void
prefersReducedMotion () => boolean true if prefers-reduced-motion: reduce

a11y.announce(text, politeness?)

Writes text into a hidden aria-live region. The screen reader reads the text asynchronously.

  • politeness = 'polite' (default): waits until the user is idle.
  • politeness = 'assertive': interrupts the current utterance — reserve for critical errors.

The region is created lazily on the first call (import-safe in Workers, pre-DOM tests). Sending the same text twice in a row re-triggers the announcement (clear + set via microtask).

a11y.clearAnnouncements()

Clears both live regions (polite + assertive). Useful on route changes to prevent stale announcements from being re-read.

a11y.aria(el, attrs)

Batch aria-* setters with conditional semantics: null / false / undefined → removeAttribute; any other value → setAttribute(String(v)). The aria- prefix is added automatically if absent from the key.

a11y.aria(btn, { expanded: isOpen, controls: 'menu-id', pressed: false });
// false/null → removeAttribute, true → setAttribute('aria-expanded', 'true')

a11y.labelledBy(el, idsOrEls) / a11y.describedBy(el, idsOrEls)

Sets aria-labelledby / aria-describedby from a string id, an Element with an id, or a mixed array. Entries without an id are ignored. An empty array or one producing no ids removes the attribute.

a11y.setRole(el, role) / a11y.removeRole(el)

Sets or removes the role attribute. Passing null, false, or undefined to setRole removes the attribute. removeRole is a shortcut for setRole(el, null).

a11y.prefersReducedMotion()

Returns true if window.matchMedia('(prefers-reduced-motion: reduce)').matches. Returns false outside a browser context (worker, SSR) or if matchMedia is unavailable.

Examples

Loading announcement

const a11y = runtime.resolve('a11y');

async function loadData() {
    a11y.announce('Loading…');
    const items = await fetch('/api/items').then(r => r.json());
    renderList(items);
    a11y.announce(`${items.length} results loaded`);
}

Error announcement (assertive)

function showError(msg) {
    a11y.announce(msg, 'assertive');
    errorEl.textContent = msg;
    errorEl.focus();
}

Aria-batch on an accordion

const a11y = runtime.resolve('a11y');

function toggleAccordion(btn, panel, open) {
    a11y.aria(btn, { expanded: open, controls: panel.id });
    a11y.aria(panel, { hidden: !open });
}

Relational labels

const titleEl = document.getElementById('dialog-title');
const descEl  = document.getElementById('dialog-desc');

a11y.labelledBy(dialog, titleEl);
a11y.describedBy(dialog, [titleEl, descEl]);

Motion-safe animation

function animateFade(el) {
    if (a11y.prefersReducedMotion()) {
        el.style.opacity = '1'; // pas d'animation
        return;
    }
    el.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 300 });
}

Notes

  • The aria-live region is a singleton per politeness — a single pair of <div> elements per page. Created on first use of announce(), inserted into document.body.
  • announce() clears the content then re-writes it via Promise.resolve().then(...) to force two distinct mutations; without this clear-and-set, assistive technologies ignore the same text sent twice.
  • clearAnnouncements() must be called on route transitions to prevent residual regions from being re-read by the AT after navigation.
  • aria(el, attrs) accepts keys with or without the aria- prefix. Duplicate key: the last setter wins (Object.keys order).
  • prefersReducedMotion() returns false by default outside a browser; do not use it for server-side decisions.
  • Security: announce stringifies text (String(text)) — no HTML injection since textContent is used, not innerHTML.

See also

  • focus — keyboard focus management (trap, tab order)
  • keybindings — accessible keyboard shortcuts
  • animate — respecting prefersReducedMotion in animations