@absoluteui/utilities
Accessibility, focus, keyboard, drag, overlay, positioning, scrolling, and motion primitives used by components and available to applications.
import { ... } from '@absoluteui/utilities';How to use
Section titled “How to use”The accessibility and interaction primitives AbsoluteUI components are built from: live announcements, focus trapping and restoration, roving focus, keyboard helpers, positioning, the overlay stack, scroll locking, observers, and reduced motion. Use them when your application builds its own widget, so it behaves like, and cooperates with, the built-in ones. Overlays and focus explains the behavior they implement.
How do I announce a status change to screen readers?
Section titled “How do I announce a status change to screen readers?”announce writes to a shared, visually hidden live region (inside the topmost modal dialog when one is open). Use 'assertive' only for errors that need immediate attention.
import { announce } from '@absoluteui/utilities';
announce('3 leave requests approved.');await new Promise((resolve) => requestAnimationFrame(resolve));document.querySelector('[aria-live="polite"]')?.textContent; // => '3 leave requests approved.'How do I keep focus inside a custom panel?
Section titled “How do I keep focus inside a custom panel?”createFocusTrap keeps Tab and Shift+Tab inside the container while active and, on deactivate, returns focus to the element that had it. Modal <dialog> elements are already inert-backed; use the trap for non-modal panels.
import { createFocusTrap } from '@absoluteui/utilities';
const trigger = document.createElement('button');trigger.textContent = 'Filters';const panel = document.createElement('div');panel.setAttribute('role', 'region');panel.setAttribute('aria-label', 'Employee filters');const department = document.createElement('select');const apply = document.createElement('button');apply.textContent = 'Apply';panel.append(department, apply);document.body.append(trigger, panel);
trigger.focus();const trap = createFocusTrap(panel);trap.activate();document.activeElement === department; // => truetrap.deactivate();document.activeElement === trigger; // => trueHow do I add arrow-key navigation to a toolbar?
Section titled “How do I add arrow-key navigation to a toolbar?”createRovingTabindex keeps one item in the Tab sequence and moves between items with the arrow keys, Home, and End. It mirrors left and right for direction: 'rtl' and skips items with disabled or aria-disabled="true".
import { createRovingTabindex } from '@absoluteui/utilities';
const toolbar = document.createElement('div');toolbar.setAttribute('role', 'toolbar');toolbar.setAttribute('aria-label', 'Timesheet actions');const buttons = ['Approve', 'Reject', 'Export'].map((label) => { const button = document.createElement('button'); button.textContent = label; return button;});toolbar.append(...buttons);document.body.append(toolbar);
const roving = createRovingTabindex(toolbar, { getItems: () => buttons, orientation: 'horizontal', loop: true });buttons.map((b) => b.tabIndex); // => [0, -1, -1]buttons[0]?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));roving.activeIndex; // => 1document.activeElement === buttons[1]; // => trueroving.dispose();How do I show a popup next to a button?
Section titled “How do I show a popup next to a button?”openPopover puts the element in the top layer and registers it in the shared overlay stack (Escape and outside clicks close only the topmost overlay). autoPosition places it next to the anchor, flips it when there is no room, and follows scrolling and resizing until you call its disposer.
import { autoPosition, computePosition, openPopover } from '@absoluteui/utilities';
const button = document.createElement('button');button.textContent = 'Leave balance';const popup = document.createElement('div');popup.textContent = 'Annual: 12 days';document.body.append(button, popup);
const stopPositioning = autoPosition(button, popup, { placement: 'bottom-start' });const handle = openPopover(popup, { inside: [button], onDismiss: () => { handle.close(); stopPositioning(); },});handle.open; // => true// Pure geometry, e.g. for a canvas: no room below, so it flips above.computePosition({ x: 100, y: 700, width: 120, height: 32 }, { width: 200, height: 160 }, { x: 0, y: 0, width: 1024, height: 768 }).placement; // => 'top-start'handle.close();stopPositioning();How do I respect reduced motion?
Section titled “How do I respect reduced motion?”shouldAnimate is false when the user asks for reduced motion or the application sets animation: 'none'. Check it before every animation; observeMediaQuery reports later changes to the preference.
import { configureAbsoluteUI } from '@absoluteui/core';import { observeMediaQuery, shouldAnimate } from '@absoluteui/utilities';
const toast = document.createElement('div');document.body.append(toast);if (shouldAnimate()) toast.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 150 });
const stop = observeMediaQuery('(prefers-reduced-motion: reduce)', (reduce) => { toast.dataset.motion = reduce ? 'off' : 'on';});configureAbsoluteUI({ common: { animation: 'none' } });shouldAnimate(); // => falsestop();What goes wrong?
Section titled “What goes wrong?”- A custom overlay outside the stack. Escape then closes an AbsoluteUI popover beneath yours. Open it with
openPopover, or callregisterOverlayLayerfor your own modal, and checkisTopOverlaybefore handling keys yourself. - Leaked listeners and observers.
autoPosition,registerOverlayLayer,lockScroll, and theobserve*functions return disposers; call each one when the widget closes. - Scroll locks are counted. The page scrolls again only after every lock is released:
import { isScrollLocked, lockScroll } from '@absoluteui/utilities';
const releaseDrawer = lockScroll();const releaseDialog = lockScroll();releaseDialog();isScrollLocked(); // => truereleaseDrawer();isScrollLocked(); // => falseAPI reference
Section titled “API reference”Functions
Section titled “Functions”addAriaReference
Link an element to one or more describing elements without clobbering existing ids.
function addAriaReference(el: Element, attribute: 'aria-describedby' | 'aria-labelledby' | 'aria-controls' | 'aria-owns', id: string): void;announce
Announce a message through a shared live region. Text is set via textContent (never parsed as HTML). Repeated identical messages are re-announced. Without a browser window (server rendering) there is nobody to announce to: nothing happens.
function announce(message: string, politeness?: Politeness, doc?: Document): void;applyVisuallyHidden
Hide an element visually while keeping it available to assistive technology.
function applyVisuallyHidden(el: HTMLElement): void;removeAriaReference
function removeAriaReference(el: Element, attribute: 'aria-describedby' | 'aria-labelledby' | 'aria-controls' | 'aria-owns', id: string): void;Politeness
type Politeness = 'polite' | 'assertive';Functions
Section titled “Functions”trackPointerDrag
function trackPointerDrag(element: HTMLElement, options: PointerDragOptions): PointerDrag;Interfaces
Section titled “Interfaces”DragMove
interface DragMove { readonly start: DragPoint; readonly point: DragPoint; readonly dx: number; readonly dy: number; readonly pointerType: string;}DragPoint
interface DragPoint { readonly x: number; readonly y: number;}PointerDrag
interface PointerDrag { readonly dragging: boolean; cancel(): void; dispose(): void;}PointerDragOptions
interface PointerDragOptions { threshold?: number; canStart?: (event: PointerEvent) => boolean; onStart?: (move: DragMove) => void; onMove?: (move: DragMove) => void; onEnd?: (move: DragMove) => void; onCancel?: () => void;}Functions
Section titled “Functions”createFocusRestore
Capture the currently focused element so focus can be returned later.
function createFocusRestore(doc?: Document, fallback?: () => HTMLElement | null): FocusRestore;createFocusTrap
Keep keyboard focus within container while active.
function createFocusTrap(container: HTMLElement, options?: FocusTrapOptions): FocusTrap;focusElement
function focusElement(el: HTMLElement | null | undefined, options?: FocusOptions): boolean;focusFirst
function focusFirst(root: ParentNode, options?: FocusOptions): boolean;getFocusableElements
Focusable (or tabbable) descendants in tab order.
function getFocusableElements(root: ParentNode, options?: { tabbableOnly?: boolean;}): HTMLElement[];isFocusable
function isFocusable(el: Element): el is HTMLElement;isTabbable
function isTabbable(el: Element): el is HTMLElement;Interfaces
Section titled “Interfaces”FocusRestore
interface FocusRestore { restore(options?: FocusOptions): boolean;}FocusTrap
interface FocusTrap { activate(): void; deactivate(): void; readonly active: boolean;}FocusTrapOptions
interface FocusTrapOptions { initialFocus?: HTMLElement | null; restoreFocus?: boolean;}Keyboard
Section titled “Keyboard”Functions
Section titled “Functions”createRovingTabindex
Roving tabindex (WAI-ARIA APG): one item in the tab sequence, arrows move between items.
function createRovingTabindex(container: HTMLElement, options: RovingTabindexOptions): RovingTabindex;createTypeahead
function createTypeahead(timeoutMs?: number): Typeahead;getNextIndex
Next active index for a navigation key, or null when the key is not a
navigation key for this orientation. Returns current when no move is possible.
function getNextIndex(key: string, current: number, count: number, options: ListNavigationOptions): number | null;isPrintableKey
True when the key event produces a single printable character without command modifiers.
function isPrintableKey(event: KeyboardEvent): boolean;matchesHotkey
Match a hotkey such as “Mod+K”, “Shift+ArrowDown”, “Alt+Enter”. “Mod” is Meta on Apple platforms and Control elsewhere.
function matchesHotkey(event: KeyboardEvent, hotkey: string): boolean;Interfaces
Section titled “Interfaces”ListNavigationOptions
interface ListNavigationOptions { orientation: Orientation; loop: boolean; direction?: Direction; pageSize?: number; isDisabled?: (index: number) => boolean;}RovingTabindex
interface RovingTabindex { setActive(index: number, focus?: boolean): void; readonly activeIndex: number; dispose(): void;}RovingTabindexOptions
interface RovingTabindexOptions extends Omit<ListNavigationOptions, 'isDisabled'> { getItems: () => HTMLElement[]; onActivate?: (item: HTMLElement, index: number) => void; typeahead?: boolean;}Typeahead
interface Typeahead { handle(char: string, labels: readonly string[], current: number, isDisabled?: (index: number) => boolean): number; reset(): void;}Orientation
type Orientation = 'horizontal' | 'vertical' | 'both';Motion
Section titled “Motion”Functions
Section titled “Functions”prefersReducedMotion
function prefersReducedMotion(): boolean;shouldAnimate
Animate only when configuration allows it and the user has not requested reduced motion.
function shouldAnimate(): boolean;Observe
Section titled “Observe”Functions
Section titled “Functions”observeIntersection
Observe intersection with the viewport.
function observeIntersection(target: Element, callback: (entry: IntersectionObserverEntry) => void, options?: IntersectionObserverInit): Disposer;observeMediaQuery
Observe a media query. Callback runs immediately and on change; without a window (server) it reports no match once.
function observeMediaQuery(query: string, callback: (matches: boolean) => void): Disposer;observeMutations
Observe DOM mutations.
function observeMutations(target: Node, callback: (records: MutationRecord[]) => void, options?: MutationObserverInit): Disposer;observeResize
function observeResize(target: Element, callback: ResizeCallback): Disposer;Overlay
Section titled “Overlay”Functions
Section titled “Functions”getOverlayDepth
function getOverlayDepth(): number;isTopOverlay
function isTopOverlay(element: HTMLElement): boolean;openPopover
Show element as a manual popover in the top layer and register it in the
overlay stack. Closing is idempotent.
function openPopover(element: HTMLElement, options?: OverlayOptions): PopoverHandle;registerOverlayLayer
Register an element as the topmost overlay layer. Returns a disposer that removes it.
function registerOverlayLayer(element: HTMLElement, options?: OverlayOptions): Disposer;Interfaces
Section titled “Interfaces”OverlayLayer
interface OverlayLayer { readonly element: HTMLElement; readonly options: OverlayOptions;}OverlayOptions
interface OverlayOptions { dismissOnEscape?: boolean; dismissOnOutsidePointer?: boolean; inside?: readonly Element[]; onDismiss?: (reason: DismissReason) => void;}PopoverHandle
interface PopoverHandle { close(): void; readonly open: boolean;}DismissReason
type DismissReason = 'escape' | 'outside-pointer' | 'programmatic';Position
Section titled “Position”Functions
Section titled “Functions”autoPosition
Position floating (fixed positioning; intended for top-layer popovers) next to
anchor and keep it updated. anchor is an element or a virtual anchor (only an
element’s size is observed). Returns a disposer.
function autoPosition(anchor: Element | VirtualAnchor, floating: HTMLElement, options?: AutoPositionOptions): Disposer;computePosition
function computePosition(anchor: Rect, floating: Size, viewport: Rect, options?: PositionOptions): PositionResult;Interfaces
Section titled “Interfaces”AutoPositionOptions
interface AutoPositionOptions extends PositionOptions { matchAnchorWidth?: boolean; constrainHeight?: boolean; onPosition?: (result: PositionResult) => void;}PositionOptions
interface PositionOptions { placement?: Placement; offset?: number; flip?: boolean; shift?: boolean; padding?: number; direction?: Direction;}PositionResult
interface PositionResult { x: number; y: number; placement: Placement; availableWidth: number; availableHeight: number;}Rect
interface Rect { x: number; y: number; width: number; height: number;}Size
interface Size { width: number; height: number;}VirtualAnchor
A positioning reference that is not an element, for example the pointer position of
a context menu. getBoundingClientRect is read on every update, so a virtual anchor
can follow scrolling content.
interface VirtualAnchor { getBoundingClientRect(): Rect;}Alignment
type Alignment = 'start' | 'center' | 'end';Placement
type Placement = Side | `${Side}-start` | `${Side}-end`;Side
type Side = 'top' | 'bottom' | 'left' | 'right';Scroll lock
Section titled “Scroll lock”Functions
Section titled “Functions”isScrollLocked
function isScrollLocked(): boolean;lockScroll
function lockScroll(doc?: Document): Disposer;