Utilities
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] |
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:
import { useSignal } from "elements-kit/integrations/react";import { windowSize } from "elements-kit/utilities/window-size";import { currentLocation } from "elements-kit/utilities/location";
function Layout() { const width = useSignal(windowSize.width); const path = useSignal(currentLocation.pathname); return <p>{path} β {width}px wide</p>;}Factory utilities with per-component lifetime go inside useScope so they are created and cleaned up with the component:
import { useSignal, useScope } from "elements-kit/integrations/react";import { createDebounced } from "elements-kit/utilities/debounced";import { signal } from "elements-kit/signals";
const query = signal("");
function Search() { const debounced = useScope(() => createDebounced(query, 300)); return ( <> <input onInput={(e) => query(e.currentTarget.value)} /> <p>Searching for: {debounced}</p> </> );}Writable signals from storage or scroll work as both getter (via useSignal) and setter (call directly):
import { useSignal } from "elements-kit/integrations/react";import { createLocalStorage } from "elements-kit/utilities/storage";
const theme = createLocalStorage("theme", "light");
function ThemeToggle() { const current = useSignal(theme); return ( <button onClick={() => theme(current === "light" ? "dark" : "light")}> {current} </button> );}createMediaQuery
Creates a Computed<boolean> that tracks a CSS media query. Returns true when the query matches, false otherwise.
import { function createMediaQuery(query: string, defaultState?: boolean): Computed<boolean>
Creates a signal that tracks a CSS media query.
createMediaQuery } from "elements-kit/utilities/media-query";
const const isDark: Computed<boolean>
isDark = function createMediaQuery(query: string, defaultState?: boolean): Computed<boolean>
Creates a signal that tracks a CSS media query.
createMediaQuery("(prefers-color-scheme: dark)");
const isDark: () => boolean
isDark(); // true or false β live, updates on OS changeSignature
function createMediaQuery( query: string, defaultState?: boolean,): Computed<boolean>| Parameter | Description |
|---|---|
query | Any valid CSS media query string |
defaultState | Value returned during SSR (where window is unavailable). Defaults to false. |
Examples
Dark mode toggle
import { effect } from "elements-kit/signals";import { createMediaQuery } from "elements-kit/utilities/media-query";
const isDark = createMediaQuery("(prefers-color-scheme: dark)");
effect(() => { document.documentElement.classList.toggle("dark", isDark());});Responsive layout
const isMobile = createMediaQuery("(max-width: 640px)");const isTablet = createMediaQuery("(max-width: 1024px)");const prefersReducedMotion = createMediaQuery("(prefers-reduced-motion: reduce)");
effect(() => { if (isMobile()) { // render compact layout } else if (isTablet()) { // render medium layout }});SSR / hydration
Pass defaultState to control the value returned on the server before the browser environment is available:
// Server renders as if dark mode is offconst isDark = createMediaQuery("(prefers-color-scheme: dark)", false);Cleanup
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 automaticeffectScope(() => { const isDark = createMediaQuery("(prefers-color-scheme: dark)"); effect(() => document.body.classList.toggle("dark", isDark()));});// β stop() cleans up the MediaQueryList listener too
// Explicit disposalusing isDark = createMediaQuery("(prefers-color-scheme: dark)");// β listener removed when `isDark` goes out of scope (TC39 `using` keyword)Timing
createTimeout
Reactive setTimeout wrapper. Fires callback once after delay ms. Starts immediately unless immediate is false.
import { createTimeout } from "elements-kit/utilities/timeout";
const { pending, stop, reset } = createTimeout(() => { console.log("fired");}, 1000);function createTimeout( callback: () => void, delay: number | (() => number), immediate?: boolean,): { pending: Computed<boolean>; start(): void; stop(): void; reset(): void } & DisposablecreateInterval
Pausable setInterval wrapper. Starts running immediately on creation.
import { createInterval } from "elements-kit/utilities/interval";
const { pending, stop } = createInterval(() => { console.log("tick");}, 1000);
stop(); // pausefunction createInterval( callback: () => void, delay: number | (() => number),): { pending: Computed<boolean>; start(): void; stop(): void; reset(): void } & DisposablecreateDebounced
Returns a Computed<T> that mirrors getter but only updates after delay ms of silence.
import { function createDebounced<T>(getter: () => T, delay: number | (() => number)): Computed<T>
Returns a Computed that mirrors getter but only updates after delay
milliseconds of silence (i.e. no new values from getter).
The initial value is read synchronously, so the computed is never undefined.
createDebounced } from "elements-kit/utilities/debounced";import { function signal<T>(): Updater<T> & Computed<T> (+1 overload)
Creates a mutable reactive signal.
- Read: call with no arguments β returns the current value and
subscribes the active tracking context.
- Write: call with a value β updates the signal and schedules
downstream effects if the value changed.
signal } from "elements-kit/signals";
const const input: Updater<string> & Computed<string>
input = signal<string>(initialValue: string): Updater<string> & Computed<string> (+1 overload)
Creates a mutable reactive signal.
- Read: call with no arguments β returns the current value and
subscribes the active tracking context.
- Write: call with a value β updates the signal and schedules
downstream effects if the value changed.
signal("");const const debounced: Computed<string>
debounced = createDebounced<string>(getter: () => string, delay: number | (() => number)): Computed<string>
Returns a Computed that mirrors getter but only updates after delay
milliseconds of silence (i.e. no new values from getter).
The initial value is read synchronously, so the computed is never undefined.
createDebounced(const input: Updater<string> & Computed<string>
input, 300);
any
effect(() => var console: Console
console.Console.log(...data: any[]): void
The console.log() static method outputs a message to the console.
log(const debounced: () => string
debounced())); // fires 300ms after input stops changingfunction createDebounced<T>(getter: () => T, delay: number | (() => number)): Computed<T>createThrottled
Returns a Computed<T> that mirrors getter but updates at most once per interval ms. A trailing-edge update ensures the final value is never dropped.
import { createThrottled } from "elements-kit/utilities/throttled";
const throttledScroll = createThrottled(() => window.scrollY, 100);function createThrottled<T>(getter: () => T, interval: number): Computed<T>DOM Events
on
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";
const cleanup = on(document, "keydown", (e) => console.log(e.key));// cleanup() to remove manually, or call inside an effectScope for auto-cleanupfunction on( target: EventTarget | Computed<EventTarget | null>, type: string, handler: EventListener, options?: AddEventListenerOptions,): () => voidcreateHover
Returns Computed<boolean> β true while pointer is over target.
import { createHover } from "elements-kit/utilities/hover";
const hovered = createHover(document.querySelector("#btn")!);
effect(() => console.log("hovered:", hovered()));function createHover(target: Element): Computed<boolean>createFocusWithin
Returns Computed<boolean> β true while focus is anywhere inside target (including target itself).
import { createFocusWithin } from "elements-kit/utilities/focus-within";
const focused = createFocusWithin(document.querySelector("form")!);function createFocusWithin(target: Element): Computed<boolean>onClickOutside
Fires handler whenever a pointer-down event occurs outside target. Returns a cleanup function.
import { onClickOutside } from "elements-kit/utilities/on-click-outside";
const cleanup = onClickOutside(menuEl, () => closeMenu());function onClickOutside(target: Element, handler: (e: PointerEvent) => void): () => voidcreateLongPress
Fires handler when a pointer is held over target for at least delay ms (default 500 ms).
import { createLongPress } from "elements-kit/utilities/long-press";
const cleanup = createLongPress(el, (e) => openContextMenu(e), { delay: 600 });function createLongPress( target: Element, handler: (e: PointerEvent) => void, options?: { delay?: number },): () => voidactiveElement
Module-level singleton. Computed<Element | null> bound to document.activeElement.
import { activeElement } from "elements-kit/utilities/active-element";
effect(() => console.log("focused:", activeElement()));Element Observation
createElementRect
Observes the full bounding rect of target via ResizeObserver. All eight DOMRect properties are reactive computeds.
import { createElementRect } from "elements-kit/utilities/element-rect";
const { width, height, top } = createElementRect(document.querySelector("#box")!);
effect(() => console.log(width(), height()));function createElementRect(target: Element): { x: Computed<number>; y: Computed<number>; width: Computed<number>; height: Computed<number>; top: Computed<number>; right: Computed<number>; bottom: Computed<number>; left: Computed<number>;} & DisposablecreateElementScroll
Returns writable x / y signals for an elementβs scroll position. Reading returns scrollLeft / scrollTop; writing scrolls the element.
import { createElementScroll } from "elements-kit/utilities/element-scroll";
const { x, y } = createElementScroll(document.querySelector(".list")!);
effect(() => console.log("scroll:", x(), y()));y(200); // scrolls to top 200pxfunction createElementScroll(target: Element): { x: Signal<number>; y: Signal<number> } & DisposablecreateResizeObserver
Raw ResizeObserver wrapper with automatic cleanup via onCleanup. Use elementRect for the common case.
import { createResizeObserver } from "elements-kit/utilities/resize-observer";
createResizeObserver(el, (entries) => { for (const entry of entries) console.log(entry.contentRect);});function createResizeObserver(target: Element, callback: ResizeObserverCallback): DisposablecreateIntersectionObserver
Raw IntersectionObserver wrapper with automatic cleanup.
import { createIntersectionObserver } from "elements-kit/utilities/intersection-observer";
createIntersectionObserver(el, ([entry]) => { console.log("visible:", entry.isIntersecting);}, { threshold: 0.5 });function createIntersectionObserver( target: Element, callback: IntersectionObserverCallback, options?: IntersectionObserverInit,): DisposablecreateMutationObserver
Watches target for DOM mutations with automatic cleanup.
import { createMutationObserver } from "elements-kit/utilities/mutation-observer";
createMutationObserver(el, { childList: true }, (records) => { console.log("mutations:", records.length);});function createMutationObserver( target: Element, options: MutationObserverInit, callback: (records: MutationRecord[]) => void,): DisposableBrowser APIs
createMediaDevices
Returns a reactive list of available media devices, refreshed when devices are added or removed.
import { createMediaDevices } from "elements-kit/utilities/media-devices";
const devices = createMediaDevices();
effect(() => console.log(devices().map((d) => d.label)));function createMediaDevices(): Computed<MediaDeviceInfo[]>windowSize
Module-level singleton. Reactive innerWidth and innerHeight of the browser window.
import { windowSize } from "elements-kit/utilities/window-size";
effect(() => console.log(windowSize.width(), windowSize.height()));orientation
Module-level singleton. Reactive screen.orientation angle and type.
import { orientation } from "elements-kit/utilities/orientation";
effect(() => console.log(orientation.type(), orientation.angle()));online
Module-level singleton. Computed<boolean> β true when navigator.onLine is true. Reacts to online / offline window events.
import { online } from "elements-kit/utilities/network";
effect(() => { if (!online()) showOfflineBanner();});retry
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.
import { retry } from "elements-kit/utilities/retry";
const fn = retry( () => fetch("/api/data").then((r) => r.json()), 3, // up to 3 attempts (n) => n * 500, // 0ms, 500ms, 1000ms between failures);
await fn(); // retries automatically on failurefunction retry<T>( fn: () => Promise<T>, attempts: number, delay?: number | ((attempt: number) => number),): () => Promise<T>Compose with async() for reactive retries:
import { async } from "elements-kit/utilities/async";import { retry } from "elements-kit/utilities/retry";import { onCleanup } from "elements-kit/signals";
const fetchTodo = async((id: number) => retry(() => { const controller = new AbortController(); onCleanup(() => controller.abort()); // aborts before each retry return fetch(`/api/todos/${id}`, { signal: controller.signal }).then((r) => r.json()); }, 3, 500)(),).start();createLocalStorage
Returns a Signal<T> persisted to localStorage. Writes in other tabs/windows are synchronised automatically via StorageEvent.
import { function createLocalStorage<T>(key: string, initialValue: T, options?: StorageOptions<T>): Signal<T>
Returns a Signal persisted to localStorage.
Changes made in other tabs/windows are synchronised automatically via
the StorageEvent.
createLocalStorage } from "elements-kit/utilities/storage";
const const theme: Signal<string>
theme = createLocalStorage<string>(key: string, initialValue: string, options?: StorageOptions<string> | undefined): Signal<string>
Returns a Signal persisted to localStorage.
Changes made in other tabs/windows are synchronised automatically via
the StorageEvent.
createLocalStorage("theme", "light");
const theme: () => string (+1 overload)
theme(); // "light"const theme: (value: string) => void (+1 overload)
theme("dark"); // persisted immediatelyfunction createLocalStorage<T>( key: string, initialValue: T, options?: { serialise?: (v: T) => string; deserialise?: (raw: string) => T },): Signal<T>createSessionStorage
Same as createLocalStorage but scoped to the current tab β no cross-tab sync.
import { createSessionStorage } from "elements-kit/utilities/storage";
const draft = createSessionStorage("draft", "");function createSessionStorage<T>(key: string, initialValue: T, options?: StorageOptions<T>): Signal<T>Media
createMediaPlayer
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.
import { createMediaPlayer } from "elements-kit/utilities/media-player";
const player = createMediaPlayer(document.querySelector("video")!);
effect(() => console.log("playing:", player.playing()));
player.volume(0.5); // set volumeplayer.time(30); // seek to 30splayer.toggle(); // play/pausefunction createMediaPlayer<T extends HTMLMediaElement>(element: T): { element: T; playing: Computed<boolean>; muted: Signal<boolean>; volume: Signal<number>; duration: Computed<number>; time: Signal<number>; ended: Computed<boolean>; play(): void; pause(): void; toggle(): void;} & DisposableURL & Routing
All location signals react to popstate (back/forward) automatically. They also listen for pushstate and replacestate custom events, which must be dispatched by your router or by patching history:
// Patch history so pushState/replaceState fire custom eventsfor (const method of ["pushState", "replaceState"] as const) { const original = history[method].bind(history); history[method] = (...args) => { original(...args); window.dispatchEvent(new Event(method.toLowerCase())); };}Without this patch, signals still update on back/forward β only programmatic navigation via pushState/replaceState wonβt be reflected.
currentLocation
Returns reactive signals for hash, href, pathname, and search β all sharing a single event listener set.
currentLocation is a module-level singleton suitable for most apps.
import { currentLocation } from "elements-kit/utilities/location";
// Singleton β shared across the appeffect(() => console.log(currentLocation.pathname()));type LocationResult = { hash: Computed<string>; href: Computed<string>; pathname: Computed<string>; search: Computed<string>;};
const currentLocation: LocationResultcreateSearchParam
Returns Computed<string | null> for a single URL search parameter.
import { createSearchParam } from "elements-kit/utilities/search-params";
const tab = createSearchParam("tab");
effect(() => console.log("tab:", tab())); // null when absentfunction createSearchParam(key: string): Computed<string | null>createURLPattern
Reactively tests a URL source against a URLPattern. The source can be a plain string/URL or a reactive getter.
No polyfill needed for modern browsers (Chrome 95+, Safari 16.4+, Firefox 117+). Use urlpattern-polyfill on npm for legacy targets.
import { createURLPattern } from "elements-kit/utilities/url-pattern";import { currentLocation } from "elements-kit/utilities/location";
const match = createURLPattern(currentLocation.href, { pathname: "/users/:id" });
effect(() => console.log(match()?.pathname.groups.id));function createURLPattern( source: string | URL | Computed<string | URL>, input?: URLPatternInput, options?: URLPatternOptions,): Computed<URLPatternResult | null>navigate
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.
import { navigate } from "elements-kit/utilities/routing";
navigate("/users/42");navigate("/users/42", { replace: true }); // replaceState β no new history entrynavigate("/users/42", { state: { id: 42 } }); // pass history statefunction navigate(url: string | URL, options?: { replace?: boolean; state?: unknown }): voidisLocalNavigationEvent
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.
import { isLocalNavigationEvent, navigate } from "elements-kit/utilities/routing";
document.querySelector("nav")!.addEventListener("click", (e) => { if (isLocalNavigationEvent(e)) { e.preventDefault(); navigate((e.target as HTMLAnchorElement).href); }});function isLocalNavigationEvent(e: MouseEvent): booleanmatches
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.
Requires urlpattern-polyfill for Safari < 26 and Firefox < 142.
import { matches } from "elements-kit/utilities/routing";
const isHome = matches({ pathname: "/" });effect(() => { if (isHome()) showHomeNav();});Conditional rendering with React:
import { useSignal } from "elements-kit/integrations/react";import { matches } from "elements-kit/utilities/routing";
const isSettings = matches({ pathname: "/settings" });
function App() { return useSignal(isSettings) ? <Settings /> : <NotFound />;}function matches(input: URLPatternInput, options?: URLPatternOptions): Computed<boolean>match
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.
Requires urlpattern-polyfill for Safari < 26 and Firefox < 142.
import { match } from "elements-kit/utilities/routing";
const postMatch = match({ pathname: "/posts/:slug" });effect(() => { const slug = postMatch()?.pathname.groups.slug; if (slug) loadPost(slug);});Extracting params in React:
import { useSignal } from "elements-kit/integrations/react";import { match } from "elements-kit/utilities/routing";
const userMatch = match({ pathname: "/users/:id" });
function App() { const result = useSignal(userMatch); const id = result?.pathname.groups.id; return id ? <UserPage id={id} /> : <NotFound />;}function match(input: URLPatternInput, options?: URLPatternOptions): Computed<URLPatternResult | null>State
createPrevious
Returns a Computed that always holds the previous value of source. Starts as undefined until the source changes for the first time.
import { createPrevious } from "elements-kit/utilities/previous";import { signal } from "elements-kit/signals";
const count = signal(0);const prev = createPrevious(count);
count(1);effect(() => console.log(prev())); // 0Pass ignore to skip updates when a condition is met:
// Only track previous when value actually changesconst prev = createPrevious(count, (a, b) => a === b);function createPrevious<T>( source: Computed<T>, ignore?: (next: T, current: T) => boolean,): Computed<T | undefined>Low-level Primitives
fromEvent
Returns a Subscribe function for one or more DOM events on a target. Use with sync to build reactive wrappers around DOM APIs.
import { fromEvent } from "elements-kit/utilities/event-driven";
const onResize = fromEvent(window, "resize");// pass to sync() as the subscribe argumentfunction fromEvent(target: EventTarget, events: string | string[]): Subscribesync
Keeps a reactive value in sync with an external source. Pass a Subscribe + a getter to get a Computed<T>; add a setter to get a writable Signal<T>.
import { fromEvent, sync } from "elements-kit/utilities/event-driven";
// Read-only: re-reads getter whenever events fireconst [scrollY] = sync( fromEvent(window, "scroll"), () => window.scrollY,);
// Writable: setter syncs writes back to the external sourceconst [volume, cleanup] = sync( fromEvent(audioEl, "volumechange"), () => audioEl.volume, (v) => { audioEl.volume = v; },);
volume(0.5); // writes to audioEl.volumetype Subscribe = (notify: () => void) => () => void;
function sync<T>(subscribe: Subscribe, getter: () => T): [Computed<T>, () => void];function sync<T>(subscribe: Subscribe, getter: () => T, setter: (v: T) => void): [Signal<T>, () => void];See also
- Signals
- Data fetching
- Full catalog: src/utilities/README.md