Utilities are pre-built factories that wrap common browser APIs in signals β similar in spirit to react-use hooks or Svelte runes, but framework-agnostic. They return a Computed<T> or Signal<T> you can read anywhere signals work.
Quick Reference
Utility
Import
Returns
createMediaQuery
utilities/media-query
Computed<boolean>
createTimeout
utilities/timeout
{ pending, start, stop, reset } & Disposable
createInterval
utilities/interval
{ pending, start, stop, reset } & Disposable
createDebounced
utilities/debounced
Computed<T>
createThrottled
utilities/throttled
Computed<T>
on
utilities/event-listener
() => void
createHover
utilities/hover
Computed<boolean>
createFocusWithin
utilities/focus-within
Computed<boolean>
onClickOutside
utilities/on-click-outside
() => void
createLongPress
utilities/long-press
() => void
activeElement
utilities/active-element
Computed<Element | null>
createElementRect
utilities/element-rect
{ x, y, width, height, β¦ } & Disposable
createElementScroll
utilities/element-scroll
{ x: Signal, y: Signal } & Disposable
createResizeObserver
utilities/resize-observer
Disposable
createIntersectionObserver
utilities/intersection-observer
Disposable
createMutationObserver
utilities/mutation-observer
Disposable
createMediaDevices
utilities/media-devices
Computed<MediaDeviceInfo[]>
windowSize
utilities/window-size
{ width, height } & Disposable
orientation
utilities/orientation
{ angle, type } & Disposable
online
utilities/network
Computed<boolean>
windowFocused
utilities/window-focus
Computed<boolean>
retry
utilities/retry
() => Promise<T>
createLocalStorage
utilities/storage
Signal<T>
createSessionStorage
utilities/storage
Signal<T>
createMediaPlayer
utilities/media-player
{ playing, muted, volume, β¦ } & Disposable
currentLocation
utilities/location
{ hash, href, pathname, search }
createSearchParam
utilities/search-params
Computed<string | null>
navigate
utilities/routing
void
isLocalNavigationEvent
utilities/routing
boolean
matches
utilities/routing
Computed<boolean>
match
utilities/routing
Computed<URLPatternResult | null>
createPrevious
utilities/previous
Computed<T | undefined>
fromEvent
utilities/event-driven
Subscribe
sync
utilities/event-driven
[Computed<T> | Signal<T>, () => void]
setContext
utilities/context
void
getContext
utilities/context
T | undefined
<dom-lifecycle>
utilities/dom-lifecycle
Custom element
DomLifecycleElement
utilities/dom-lifecycle
class
Using utilities with React
Utilities return plain signals and computeds β useSignal from elements-kit/integrations/react connects them to React components with no special glue.
Singleton utilities are module-level values shared across the whole app. Pass them directly to useSignal:
The underlying MediaQueryList event listener is automatically removed when the signal goes out of scope β via onCleanup if created inside an effect, or Symbol.dispose for explicit resource management:
// Inside an effect or effectScope β cleanup is automatic
Attaches a type-safe event listener with automatic cleanup. When the target is a reactive getter, the listener re-registers whenever the target changes.
import { on } from"elements-kit/utilities/event-listener";
Wraps a () => Promise<T> with retry logic. Retries up to attempts times on failure. The optional delay is inserted between failures only β not after the final one. Each attempt runs in an effect scope, so onCleanup inside the function fires before each retry.
Wraps an HTMLMediaElement (<audio> or <video>) with reactive state and playback controls. muted, volume, and time are writable β writing them updates the element. playing, duration, and ended are read-only.
All location signals react to popstate (back/forward) automatically. They also listen for pushstate and replacestatecustom events, which must be dispatched by your router or by patching history:
// Patch history so pushState/replaceState fire custom events
for (constmethodof ["pushState", "replaceState"] asconst) {
Navigates to a URL via history.pushState (or replaceState). Patches history once on first call so all programmatic navigation β including third-party router calls β fires the pushstate / replacestate custom events that currentLocation signals react to.
Returns true when a click event on an <a> element should be handled client-side β same origin, primary button, no modifier keys, no download attribute, no target="_blank". Walks up to the nearest anchor via closest("a"), so it works on container elements too.
Use alongside navigate() to intercept anchor clicks without hardwiring routing logic into this utility.
Returns Computed<boolean> β true when the current URL matches input. Uses URLPattern.test() β faster than match when you donβt need captured groups.
Always use the object form { pathname: "..." } β relative string patterns require a base URL and will throw.
Returns Computed<URLPatternResult | null> β the full match result when the current URL matches input, null when it does not. Use when you need captured groups. For a boolean gate, prefer matches().
Always use the object form { pathname: "..." } β relative string patterns require a base URL and will throw.
Pass ignore to skip updates when a condition is met:
// Only track previous when value actually changes
constprev=createPrevious(count, (a, b) => a === b);
functioncreatePrevious<T>(
source:Computed<T>,
ignore?: (next:T, current:T) =>boolean,
):Computed<T|undefined>
DOM lifecycle
Drop-in custom element. Place inside any element (or wrap children) to be notified when the surrounding subtree connects, disconnects, moves, or is adopted into another document. Built on the platformβs own custom-element callbacks β no MutationObserver, no global registry. Useful when a JSX ref callback fires too early β e.g. resolving getContext, measuring layout, attaching observers that need a connected ancestor.
Position-tracking callbacks (onConnect, onDisconnect, onMove) receive the lifecycle element itself. Read self.parentElement for the surrounding element, self.firstElementChild / self.children for wrapped content, or self.getRootNode() to walk through a shadow root. self is always non-null β even when the lifecycle element is the direct child of a ShadowRoot (where parentElement is null).
Render-inert by default: display: contents removes its layout box so it doesnβt affect the parentβs layout, and role="none" strips its implicit a11y role. Children passed inside it participate in layout and a11y as if the wrapper werenβt there. Caveat: structural CSS selectors (:empty, :first-child, :nth-child) still see the element in the DOM tree.
Call getContext(self, β¦) inside onConnect β the walk goes from the wrapper up through its ancestors, so any outer provider resolves. Expose the result as a signal so wrapped children read it without each one running its own lookup.
The wrapper is transparent in the ancestor walk, so wrapped children may also call getContext directly on themselves and reach the same outer provider. Use onConnect when you want to read once at mount and fan the value out to multiple children via a single signal.
Observing descendant mutations
<dom-lifecycle> only fires on its own (re)connection β it does not observe descendant mutations (children being added, removed, or replaced while the wrapper stays mounted). For per-child mount/unmount inside its subtree, either nest a <dom-lifecycle> per child or use createMutationObserver on el inside onConnect:
every connection β self.parentElement is the surrounding element, self.firstElementChild is the wrapped content
onDisconnect
disconnectedCallback
self: DomLifecycleElement
every disconnection β self.parentElement is null per spec; capture the parent inside onConnect if you need it on disconnect
onMove
connectedMoveCallback
self: DomLifecycleElement
move via Node.moveBefore() (in browsers without that API, the disconnect+connect pair fires instead)
onAdopted
adoptedCallback
(oldDocument, newDocument)
when the element is adopted into a new document
<div>
<dom-lifecycle
onConnect={(el) => {
constio=newIntersectionObserver((entries) => {});
if (el.parentElement) io.observe(el.parentElement);
}}
onDisconnect={(el) => {
// pair cleanup with the resource you opened in onConnect
// el.parentElement is null here per spec
}}
onMove={(el) => {
// fires instead of disconnect+connect when moveBefore() is used
}}
/>
</div>
onConnect / onDisconnect re-fire on every (re)connection (item moves in <For>, portal moves between parents). The user removes the element themselves; it does not self-remove. To make a callback one-shot, set the property to null after the first fire.
Works inside open and closed shadow roots, after cloneNode(true), after innerHTML upgrade, and under strict CSP β same guarantees the platform gives any custom element.
Context
DOM-tree dependency injection β pass values down a custom-element subtree without prop drilling or globals. Provider registers a value on a host element; descendants look it up by walking parentNode (crossing into shadow hosts via getRootNode().host). Innermost provider wins. Reactivity is opt-in: pass a Signal / Computed and read it inside an effect.
setContext
Registers value under key on host. Auto-removed via onCleanup when the surrounding scope disposes β must run inside a reactive scope (an effect, effectScope, the render setup, or a JSX ref callback, which all run inside one).
Walks up from consumer (across open shadow boundaries) and returns the first registered value for key, or undefined. One-shot β does not subscribe. Caller decides how to read the result (call signals inside an effect for reactive reads).
The consumer must be in the DOM tree at call time. Inside a JSX ref callback the element has not yet been inserted, so a synchronous getContext(el, β¦) returns undefined. Defer the lookup to mount time with <dom-lifecycle>:
For a component body that needs getContext after connection, defer with <dom-lifecycle> β itβs render-inert (zero layout box, no a11y role) and its onConnect fires once the consumer is in the tree:
Inside a custom element, setContext(this, ...) registers on the host directly β no wrapper needed. Use the render(this, template) lifecycle pattern so the entry is auto-cleaned on disconnect: