Primitives

Reactive building blocks for data, forms, routing, context, DOM events, observers, browser state and timing. One import each, any framework.

Primitives are small factories that return signals. Each one is its own import, needs no framework, and cleans up with the scope it was created in. Read them in plain scripts, custom elements, JSX, or React.

import { effect } from "elements-kit/signals";
import { online } from "elements-kit/utilities/network";
import { createMediaQuery } from "elements-kit/utilities/media-query";

const dark = createMediaQuery("(prefers-color-scheme: dark)");

effect(() => {
  document.title = `${online() ? "Online" : "Offline"} · ${dark() ? "dark" : "light"}`;
});

Catalog

Every primitive imports from elements-kit/utilities/<module>. Universal primitives run anywhere; browser ones need a DOM and return neutral defaults on the server.

PrimitiveModuleReturnsRuns in
Data
promisepromiseComputedPromise<T>Universal
asyncasyncAsync<T>Universal
retryretry() => Promise<T>Universal
createLocalStoragestorageSignal<T>Browser
createSessionStoragestorageSignal<T>Browser
Forms
FormObjectform-object{ toObject, fromObject, clear }Browser
Routing
currentLocationlocation{ hash, href, pathname, search }Browser
createSearchParamsearch-paramsComputed<string | null>Browser
navigateroutingvoidBrowser
matches / matchroutingComputed<boolean> / Computed<URLPatternResult | null>Browser
isLocalNavigationEventroutingbooleanBrowser
Context
setContext / getContextcontextvoid / T | undefinedUniversal
DOM events
onevent-listener() => voidBrowser
createHoverhoverComputed<boolean>Browser
createFocusWithinfocus-withinComputed<boolean>Browser
onClickOutsideon-click-outside() => voidBrowser
createLongPresslong-press() => voidBrowser
activeElementactive-elementComputed<Element | null>Browser
Observers
createElementRectelement-rectElementRectBrowser
createElementScrollelement-scroll{ x, y } & DisposableBrowser
createResizeObserverresize-observerDisposableBrowser
createIntersectionObserverintersection-observerDisposableBrowser
createMutationObservermutation-observerDisposableBrowser
Browser state
createMediaQuerymedia-queryComputed<boolean>Browser
windowSizewindow-size{ width, height }Browser
orientationorientationComputed<…>Browser
onlinenetworkComputed<boolean>Browser
windowFocusedwindow-focusComputed<boolean>Browser
createMediaDevicesmedia-devicesComputed<MediaDeviceInfo[]>Browser
Timing
createTimeouttimeout{ pending, start, stop, reset }Universal
createIntervalinterval{ pending, start, stop, reset }Universal
createDebounceddebouncedComputed<T>Universal
createThrottledthrottledComputed<T>Universal
createPreviouspreviousComputed<T | undefined>Universal
Media
createMediaPlayermedia-player{ playing, muted, volume, … }Browser
Building primitives
fromEventevent-drivenSubscribeUniversal
syncevent-driven[Computed<T> | Signal<T>, () => void]Universal

Use them anywhere

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

See also