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-liveregion is a singleton perpoliteness— a single pair of<div>elements per page. Created on first use ofannounce(), inserted intodocument.body. announce()clears the content then re-writes it viaPromise.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 thearia-prefix. Duplicate key: the last setter wins (Object.keysorder).prefersReducedMotion()returnsfalseby default outside a browser; do not use it for server-side decisions.- Security:
announcestringifiestext(String(text)) — no HTML injection sincetextContentis used, notinnerHTML.
See also
- focus — keyboard focus management (trap, tab order)
- keybindings — accessible keyboard shortcuts
- animate — respecting
prefersReducedMotionin animations