Skip to content

@absoluteui/utilities

Accessibility, focus, keyboard, drag, overlay, positioning, scrolling, and motion primitives used by components and available to applications.

import { ... } from '@absoluteui/utilities';

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; // => true
trap.deactivate();
document.activeElement === trigger; // => true

How 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; // => 1
document.activeElement === buttons[1]; // => true
roving.dispose();

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();

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(); // => false
stop();
  • A custom overlay outside the stack. Escape then closes an AbsoluteUI popover beneath yours. Open it with openPopover, or call registerOverlayLayer for your own modal, and check isTopOverlay before handling keys yourself.
  • Leaked listeners and observers. autoPosition, registerOverlayLayer, lockScroll, and the observe* 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(); // => true
releaseDrawer();
isScrollLocked(); // => false

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';

trackPointerDrag

function trackPointerDrag(element: HTMLElement, options: PointerDragOptions): PointerDrag;

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;
}

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;

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;
}

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;

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';

prefersReducedMotion

function prefersReducedMotion(): boolean;

shouldAnimate

Animate only when configuration allows it and the user has not requested reduced motion.

function shouldAnimate(): boolean;

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;

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;

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';

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;

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';

isScrollLocked

function isScrollLocked(): boolean;

lockScroll

function lockScroll(doc?: Document): Disposer;