Core Primitives
Framework-agnostic accessibility utilities from @compa11y/core. Use these to build your own accessible components in any framework.
ARIA Helpers
Utilities for setting and managing ARIA attributes.
aria — chainable attribute setter
A fluent API for setting ARIA attributes on DOM elements imperatively.
import { aria } from "@compa11y/core";
aria.hide(element); // aria-hidden="true"
aria.show(element); // removes aria-hidden
aria.setExpanded(element, true); // aria-expanded="true"
aria.setSelected(element, true); // aria-selected="true"
aria.setChecked(element, "mixed"); // aria-checked="mixed"
aria.setDisabled(element, true); // aria-disabled="true"
aria.setRequired(element, true); // aria-required="true"
aria.setInvalid(element, true); // aria-invalid="true"
aria.setLabel(element, "Close"); // aria-label="Close"
aria.setLabelledBy(element, "title-id"); // aria-labelledby="title-id"
aria.setDescribedBy(element, "hint-id"); // aria-describedby="hint-id"
aria.setControls(element, "menu-id"); // aria-controls="menu-id"
aria.setLive(element, "polite"); // aria-live="polite"
aria.setRole(element, "dialog"); // role="dialog"
aria.setActiveDescendant(element, "opt-3"); // aria-activedescendant="opt-3"
aria.setOrientation(element, "vertical"); // aria-orientation="vertical"
aria.setCurrent(element, "page"); // aria-current="page"
aria.setPressed(element, true); // aria-pressed="true"
aria.setModal(element, true); // aria-modal="true"
aria.setHasPopup(element, "menu"); // aria-haspopup="menu"
aria.setBusy(element, true); // aria-busy="true"buildAriaProps(props)
Builds an object of ARIA attributes from a descriptive props object. Useful in React components.
import { buildAriaProps } from "@compa11y/core";
const ariaProps = buildAriaProps({
role: "dialog",
label: "Settings",
modal: true,
expanded: false,
});
// { role: "dialog", "aria-label": "Settings", "aria-modal": "true", "aria-expanded": "false" }
<div {...ariaProps} />mergeAriaIds(...ids)
Combines multiple element IDs into a space-separated string for use in aria-describedby or aria-labelledby. Null/undefined values are filtered out.
import { mergeAriaIds } from "@compa11y/core";
const describedBy = mergeAriaIds(hintId, hasError ? errorId : null);
// "hint-1 error-1" or "hint-1" depending on hasError
<input aria-describedby={describedBy} />hasAccessibleName(element)
Checks if an element has a computed accessible name via aria-label, aria-labelledby, or text content.
import { hasAccessibleName } from "@compa11y/core";
if (!hasAccessibleName(button)) {
console.warn("Button is missing an accessible name");
}Focus Trap
Constrain Tab/Shift+Tab focus within a container. Used internally by Dialog.
createFocusTrap(container, options?)
Creates a focus trap within a container element. Focus cycles between first and last focusable elements. Supports nested traps via pause/unpause.
import { createFocusTrap, hasFocusTrap, getActiveFocusTrap } from "@compa11y/core";
const trap = createFocusTrap(modalElement, {
initialFocus: closeButton, // Where to focus on activation
returnFocus: true, // Return focus to previous element on deactivation
escapeDeactivates: true, // Escape key deactivates trap
clickOutsideDeactivates: false, // Click outside behavior
onDeactivate: () => closeModal(), // Callback on deactivation
});
trap.activate(); // Start trapping
trap.pause(); // Pause (for nested traps)
trap.unpause(); // Resume
trap.deactivate(); // Stop trapping, restore focus
trap.destroy(); // Cleanup without callback
// Stack management
hasFocusTrap(); // Any trap active?
getActiveFocusTrap(); // Get the topmost trapFocus Visible
Distinguish keyboard focus from mouse/touch focus. Call initFocusVisible() once at app startup.
initFocusVisible()
Initializes global focus-visible tracking by listening to keyboard and pointer events. Returns a cleanup function.
import { initFocusVisible, isFocusVisible, getLastFocusSource, focusWithVisibleRing } from "@compa11y/core";
// Initialize once at app startup
const cleanup = initFocusVisible();
// Check focus type
isFocusVisible(); // true if last focus was keyboard-initiated
getLastFocusSource(); // 'keyboard' | 'mouse' | 'touch' | 'unknown'
hasVisibleFocus(element); // Does this specific element have visible focus?
// Force visible focus (useful for programmatic focus in modals)
focusWithVisibleRing(firstButton);
// Manual override
setFocusVisible(element, true);Focus Scope & Roving Tabindex
Navigate and manage focus within a container.
createFocusScope(container, options?)
Creates a focus scope that lets you move focus within a container — first, last, next, previous, or by index.
import { createFocusScope } from "@compa11y/core";
const scope = createFocusScope(container, {
contain: true, // Tab wraps within container
restoreFocus: true, // Restore focus on destroy
autoFocus: true, // Focus first element immediately
});
scope.focusFirst();
scope.focusLast();
scope.focusNext({ wrap: true });
scope.focusPrevious({ wrap: true });
scope.focusAt(2);
scope.getFocused(); // Currently focused element
scope.getElements(); // All focusable elements in scope
scope.destroy(); // CleanupcreateRovingTabindex(container, selector, options?)
Implements the roving tabindex pattern — only one item in a group has tabindex='0' at a time. Handles keyboard navigation automatically.
import { createRovingTabindex } from "@compa11y/core";
const roving = createRovingTabindex(toolbar, '[role="button"]', {
initialIndex: 0,
orientation: "horizontal",
wrap: true,
onSelectionChange: (index, element) => console.log("Selected:", index),
});
roving.next();
roving.previous();
roving.first();
roving.last();
roving.goto(2);
roving.getIndex(); // Current index
roving.update(); // Refresh when DOM changes
roving.destroy(); // CleanupFocus Neighbor & Focus Return
Gracefully handle focus when elements are removed or dialogs are closed.
findFocusNeighbor(element, options?)
Find the nearest focusable sibling when an element is about to be removed or disabled.
import { findFocusNeighbor } from "@compa11y/core";
const neighbor = findFocusNeighbor(deletedItem, {
scope: listContainer, // Search within (default: parent element)
prefer: "previous", // Try previous first, then next (default)
});
neighbor?.focus();createFocusReturn(initialElement?)
Save a focus target and restore it later. Automatically falls back to the nearest neighbour if the saved element is no longer focusable.
import { createFocusReturn } from "@compa11y/core";
const focusReturn = createFocusReturn();
// Before opening modal
focusReturn.save(); // Saves document.activeElement
focusReturn.save(triggerButton); // or save a specific element
// After closing modal
focusReturn.return(); // Focuses saved element, or its nearest neighbor
// With options
focusReturn.return({
prefer: "next", // Try next neighbor first if saved element is gone
fallback: someOtherButton, // Last resort fallback
});
focusReturn.element; // Read the saved element (readonly)
focusReturn.clear(); // Clear without focusingKeyboard Manager
Keyboard event management and pre-built interaction patterns.
createKeyboardManager(handlers, options?)
Creates a centralized keyboard handler. Attaches to a DOM element and dispatches to the correct handler. Supports key combinations like 'Ctrl+A' and 'Shift+Enter'.
import { createKeyboardManager } from "@compa11y/core";
const kb = createKeyboardManager(
{
ArrowDown: () => focusNext(),
ArrowUp: () => focusPrevious(),
Enter: () => selectItem(),
Escape: () => close(),
"Ctrl+A": () => selectAll(),
Space: () => toggleItem(),
},
{
preventDefault: true, // Default: true
stopPropagation: true, // Default: true
targetSelector: '[role="option"]', // Only handle when target matches
}
);
kb.attach(listElement); // Start listening
kb.on("Delete", () => remove()); // Add handler after creation
kb.off("Delete"); // Remove a handler
kb.detach(); // Stop listening
kb.destroy(); // Full cleanupnormalizeKey / getKeyCombo
Normalize keyboard event keys across browsers and build modifier key combos.
import { normalizeKey, getKeyCombo } from "@compa11y/core";
normalizeKey(event); // ' ' → 'Space', 'Esc' → 'Escape', 'Left' → 'ArrowLeft'
getKeyCombo(event); // 'Ctrl+A', 'Shift+Enter', 'Meta+Space'KeyboardPatterns
Pre-built keyboard patterns for common ARIA widgets.
import { KeyboardPatterns } from "@compa11y/core";
// Menu / Listbox pattern
KeyboardPatterns.menu({ onUp, onDown, onEnter, onEscape, onHome, onEnd });
// Tabs pattern
KeyboardPatterns.tabs({ onLeft, onRight, onHome, onEnd });
// Dialog pattern
KeyboardPatterns.dialog({ onEscape });
// 2D Grid pattern
KeyboardPatterns.grid({ onUp, onDown, onLeft, onRight, onHome, onEnd, onCtrlHome, onCtrlEnd });createTypeAhead(items, options?)
Creates a type-ahead search handler. Users type characters to jump to matching items. Resets after a configurable timeout.
import { createTypeAhead } from "@compa11y/core";
const typeAhead = createTypeAhead(["Apple", "Apricot", "Banana", "Cherry"], {
timeout: 500, // Reset after 500ms of no typing
});
typeAhead.type("a"); // Returns 'Apple'
typeAhead.type("p"); // Returns 'Apricot' (search is now 'ap')
// After 500ms, resets
typeAhead.type("b"); // Returns 'Banana'
typeAhead.reset(); // Manual reset
typeAhead.getSearch(); // Current search stringLive Announcer
Screen reader announcements via ARIA live regions. Call initAnnouncer() once at app startup.
initAnnouncer()
Initializes the live announcer by creating hidden ARIA live region elements in the DOM. Returns a cleanup function.
import { initAnnouncer } from "@compa11y/core";
// Call once at app startup
const cleanup = initAnnouncer();announce / announcePolite / announceAssertive
Make polite (non-interrupting) or assertive (interrupting) screen reader announcements.
import {
announce,
announcePolite,
announceAssertive,
announceStatus,
announceError,
announceProgress,
queueAnnouncement,
clearAnnouncements,
} from "@compa11y/core";
// Basic announcements
announce("Form submitted"); // Polite by default
announcePolite("3 search results found"); // Non-interrupting
announceAssertive("Error: invalid email address"); // Interrupts current speech
// Convenience functions
announceStatus("Saved"); // Polite status update
announceError("Connection lost"); // Assertive error
announceProgress(5, 10); // "5 of 10"
announceProgress(5, 10, "Step {current} of {total}"); // "Step 5 of 10"
// Advanced options
announce("Updated", {
politeness: "polite",
delay: 100, // Delay in ms
clearPrevious: true, // Clear previous messages first
timeout: 7000, // Auto-clear after 7s (default)
});
// Queue rapid announcements (debounced)
queueAnnouncement("Processing...", { debounce: 300 });
// Clear all pending announcements
clearAnnouncements();createAnnouncer(defaults?)
Create a scoped announcer instance with preset defaults.
import { createAnnouncer } from "@compa11y/core";
const myAnnouncer = createAnnouncer({ politeness: "polite" });
myAnnouncer.announce("Done");
myAnnouncer.assertive("Error!");ID Generation
Generate unique, stable IDs for ARIA attribute associations.
generateId / generateIds / createIdScope
Generate unique IDs for elements, multiple related IDs, or a scoped set of IDs for a component.
import { generateId, generateIds, createIdScope, resetIdCounter } from "@compa11y/core";
generateId(); // 'compa11y-1'
generateId("button"); // 'compa11y-button-2'
const ids = generateIds(["label", "input", "error"] as const, "search");
// { label: 'compa11y-search-3-label', input: 'compa11y-search-3-input', error: 'compa11y-search-3-error' }
const scope = createIdScope("dropdown");
scope.id; // 'compa11y-dropdown-4'
scope.generate("trigger"); // 'compa11y-dropdown-4-trigger'
scope.generate("menu"); // 'compa11y-dropdown-4-menu'
resetIdCounter(); // For testing onlyPlatform Detection
Detect user preferences and platform capabilities.
Platform utilities
Detect the current platform, device type, and user preferences.
import {
isBrowser,
isMac,
isIOS,
isAndroid,
isWindows,
isTouchDevice,
prefersReducedMotion,
prefersHighContrast,
prefersDarkMode,
getScreenReaderHints,
createMediaQueryListener,
} from "@compa11y/core";
isBrowser(); // true if running in browser (not SSR)
isMac(); // true on macOS
isIOS(); // true on iOS
isAndroid(); // true on Android
isWindows(); // true on Windows
isTouchDevice(); // true if device supports touch
// User preferences
prefersReducedMotion(); // true if user prefers reduced motion
prefersHighContrast(); // true if user prefers high contrast
prefersDarkMode(); // true if user prefers dark color scheme
// Screen reader hints (not 100% reliable)
const { possibleScreenReader, forcedColors } = getScreenReaderHints();
// Listen for preference changes
const cleanup = createMediaQueryListener(
"(prefers-reduced-motion: reduce)",
(prefersReduced) => setAnimations(!prefersReduced)
);Dev Warnings
Development-only warnings that help catch accessibility issues early. These are stripped in production builds.
warn / checks / createComponentWarnings
Emit accessibility warnings in development. Use pre-built checks or create a component-specific warning factory.
import { warn, checks, createComponentWarnings, setWarningHandler } from "@compa11y/core";
// Direct warning
warn("Button has no accessible label", "Button");
// Pre-built checks
checks.hasLabel(element, "Button"); // Warns if no accessible name
checks.hasLabelledBy(element, "Combobox"); // Warns if no aria-labelledby
// Component-specific warning factory
const warnings = createComponentWarnings("MyComponent");
warnings.missingProp("aria-label");
warnings.invalidValue("size", "huge", ["sm", "md", "lg"]);
// Custom warning handler (e.g., to throw in tests)
setWarningHandler((message, component) => {
throw new Error(`[${component}] ${message}`);
});