R

@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

sh
pnpm add @robonen/platform

Subpath exports

The package splits along the platform boundary. Browser-only helpers live under /browsers; runtime-agnostic helpers live under /multi.

EntryScopeWhat you get
@robonen/platform/browsersDOMFocus, tabbable edges, hideOthers, animation lifecycle
@robonen/platform/multiAny 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.

ts
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:

ts
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);
}
SSR note: browser helpers touch the DOM, so call them inside event handlers or after mount. 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

fn
createGuardAttrstested
fn
dispatchAnimationEventtested

Dispatches a non-bubbling custom event on an element for animation lifecycle tracking

fn
findFirstVisibletested

Returns the first visible element from a list. Checks visibility up the DOM to `container` (exclusive).

fn
findLastVisibletested

Returns the last visible element from a list. Checks visibility up the DOM to `container` (exclusive).

fn
focustested

Focuses an element without scrolling. Optionally calls select on input elements.

fn
focusFirsttested

Attempts to focus the first element from a list of candidates. Stops when focus actually moves.

fn
focusGuardtested

Adds a pair of focus guards at the boundaries of the DOM tree to ensure consistent focus behavior

fn
getActiveElementtested

Returns the active element of the document (or shadow root)

fn
getAnimationNametested

Returns the current CSS animation name(s) of an element

fn
getTabbableCandidatestested

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`.

fn
getTabbableEdgestested

Returns the first and last tabbable elements inside a container

fn
hideOtherstested

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.

fn
isAnimatabletested

Checks whether an element has a running CSS animation or transition

fn
isHiddentested

Checks if an element is hidden via `visibility: hidden` or `display: none` up the DOM tree

fn
isSelectableInputtested

Checks if an element is an input element with a select method

fn
onAnimationSettletested

Attaches animation/transition end listeners to an element with fill-mode flash prevention. Returns a cleanup function.

fn
shouldSuspendUnmounttested

Determines whether unmounting should be delayed due to a running animation/transition change