@robonen/platform
Platform-dependent utilities for browser and multi-runtime JavaScript — focus management, ARIA isolation, animation lifecycle tracking, and environment-safe globals.
Most utility libraries stop at the platform boundary: the moment you need to reach for the DOM, shadow roots, aria-hidden, or globalThis, you are on your own. @robonen/platform fills that gap. It packages the gritty, well-tested primitives that overlays, dialogs, and editors depend on — focus guards, tabbable-edge detection, sibling hiding for screen readers, and CSS animation settling — and ships them SSR-aware and dependency-free. It is the low-level layer that powers @robonen/primitives and the editor.
Focus, done right
Shadow-DOM-aware active-element lookup, scroll-free focusing, and first/last tabbable-edge detection via a fast TreeWalker — the bones of any focus trap.
Accessible isolation
hideOthers marks every sibling aria-hidden, ref-counted across layers, preserving aria-live regions. A dependency-free port of aria-hidden.
Animation lifecycle
Detect running animations and transitions, then settle exit animations cleanly with fill-mode flash prevention — so unmounts wait for the CSS to finish.
Multi-runtime safe
A resolved _global and an isClient flag that work across Node, Bun, Deno, and the browser — guards baked in so SSR never throws.
Install
pnpm add @robonen/platformSubpath exports
The package splits along the platform boundary. Browser-only helpers live under /browsers; runtime-agnostic helpers live under /multi.
| Entry | Scope | What you get |
|---|---|---|
@robonen/platform/browsers | DOM | Focus, tabbable edges, hideOthers, animation lifecycle |
@robonen/platform/multi | Any runtime | _global, isClient |
Usage
A typical overlay flow: capture the focused element, hide siblings from assistive tech, drop focus onto the first tabbable target, and tear it all down on close.
import {
getActiveElement,
getTabbableEdges,
focus,
hideOthers,
} from '@robonen/platform/browsers';
function openDialog(dialog: HTMLElement) {
// Remember where focus was, so we can restore it on close.
const previouslyFocused = getActiveElement();
// Hide everything outside the dialog from screen readers (ref-counted).
const undoHide = hideOthers(dialog);
// Move focus to the first tabbable element inside the dialog.
const { first } = getTabbableEdges(dialog);
focus(first, { select: true });
return function close() {
undoHide();
focus(previouslyFocused);
};
} On the cross-runtime side, reach for a safe global and a reliable client check without sprinkling "undefined" guards through your code:
import { _global, isClient } from '@robonen/platform/multi';
// Works in Node, Bun, Deno and the browser — never throws in SSR.
if (isClient) {
_global.addEventListener('resize', onResize);
}hideOthers already no-ops when document is undefined, and /multi is import-safe everywhere. Where to next
Browse the full API reference below, or jump straight to the building blocks:
- focusGuard — add boundary guards for predictable focus wrapping
- getTabbableEdges — find the first and last focusable elements in a container
- hideOthers — isolate a subtree for assistive technology
- onAnimationSettle — run a callback once an animation or transition finishes
API Reference
Browsers · 17
Dispatches a non-bubbling custom event on an element for animation lifecycle tracking
Returns the first visible element from a list. Checks visibility up the DOM to `container` (exclusive).
Returns the last visible element from a list. Checks visibility up the DOM to `container` (exclusive).
Focuses an element without scrolling. Optionally calls select on input elements.
Attempts to focus the first element from a list of candidates. Stops when focus actually moves.
Adds a pair of focus guards at the boundaries of the DOM tree to ensure consistent focus behavior
Returns the active element of the document (or shadow root)
Returns the current CSS animation name(s) of an element
Collects all tabbable candidates via TreeWalker (faster than querySelectorAll). This is an approximate check — does not account for computed styles. Visibility is checked separately in `findFirstVisible`.
Returns the first and last tabbable elements inside a container
Marks every sibling of `target` (within `parentNode`, defaulting to `document.body`) as `aria-hidden="true"` so assistive technologies skip them. `aria-live` regions and `<script>` elements are preserved. Returns an undo function that restores the previous state; calls stack (ref-counted) across multiple layers. Port of the `aria-hidden` npm package, kept dependency-free.
Checks whether an element has a running CSS animation or transition
Checks if an element is hidden via `visibility: hidden` or `display: none` up the DOM tree
Checks if an element is an input element with a select method
Attaches animation/transition end listeners to an element with fill-mode flash prevention. Returns a cleanup function.
Determines whether unmounting should be delayed due to a running animation/transition change