npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

elements-kit

v0.38.3

Published

A lightweight reactive UI library that transforms native HTMLElements into reactive components with signals. Ideal for framework-agnostic applications and web components.

Readme

ElementsKit 🌱

Universal reactive primitives for the web. Signals, JSX, custom elements, and browser-API helpers. Import one at a time, compose them, or use any of them inside vanilla JS, React, Vue, or any framework.

import { signal, computed, resolve } from "elements-kit/signals";
import { render } from "elements-kit/render";
import type { Props } from "elements-kit/jsx-runtime";

function Counter(props: Props<{ initial?: number }>) {
  const count = signal(resolve(props.initial) ?? 0);
  const doubled = computed(() => count() * 2);

  return (
    <section>
      <p><strong>{count}</strong> × 2 = <strong>{doubled}</strong></p>
      <button on:click={() => count(count() + 1)}>+1</button>{" "}
      <button on:click={() => count(count() - 1)}>−1</button>
    </section>
  );
}

render(document.getElementById("app")!, () => <Counter initial={0} />);

Installation

npm install elements-kit

Configure JSX in your tsconfig.json:

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "elements-kit"
  }
}

Why ElementsKit

ElementsKit is a library of reactive primitives, not a framework. Each piece is its own import, runs on its own, and composes with the others — inside React, inside a custom element, or on its own in a script.

  • Compose, don't configure. Small focused APIs — signal, computed, on, fromEvent, async. Combine primitives instead of maintaining an overloaded interface.

  • Close to the platform. JSX compiles to document.createElement. promise extends Promise. Custom elements are HTMLElement. Thin or absent abstraction layers — no virtual DOM, no proxies, no build steps.

  • Predictable and explicit — no magic. signal/compose are reactive; nothing else is. No heuristic dependency tracking, no hidden subscriptions.

  • Designed for the AI age. Code is cheap; maintenance still isn't. Primitives compose into higher-level blocks. Swap one block at a time instead of maintaining long lines of code.

  • Bundler-friendly. Every primitive is its own subpath — elements-kit/signals, elements-kit/utilities/*, elements-kit/ui/*, elements-kit/integrations/*. Import only what you need.

Packages

Every feature is a separate subpath export — import only what you use.

| Entry | Purpose | |-------|---------| | elements-kit/signals | signal, computed, effect, effectScope, batch, untracked, trigger, onCleanup, MaybeReactive, resolve, computedProps, @reactive | | elements-kit/render | render(target, setup) — mount a node with a scoped lifetime; returns unmount | | elements-kit/attributes | @attributes decorator + ATTRIBUTES symbol | | elements-kit/slot | Slot class + @slot() decorator + SlotContent type — comment-marker DOM regions as plain properties | | elements-kit/custom-elements | defineElement, CustomElementRegistry, PropertiesOf, AttributesOf, EventsOf, PublicPropKeys — raw framework-agnostic extractors for a custom element's properties/attributes/events | | elements-kit/for | For keyed-list component | | elements-kit/jsx-runtime | JSX factory + type helpers (PropsOf, MaybeReactiveProps, Props, Require) — configure via jsxImportSource | | elements-kit/server | renderToStream, renderToString — streaming HTML rendering in any JS runtime (Node, edge/Workers), no DOM required | | elements-kit/hydrate | hydrate(container, () => <App/>) — adopt server-rendered DOM and make it interactive | | elements-kit/await | Await — loading boundary (Suspense equivalent); code splitting = async + dynamic import (experimental) | | elements-kit/integrations/react | useSignal, useScope React bridge hooks | | elements-kit/integrations/astro | elementsKit() Astro integration — elements-kit components as server-rendered, hydrated islands (experimental) | | elements-kit/utilities/* | Reactive browser-API utilities — see src/utilities/README.md | | elements-kit/ui/* | Accessible UI component styles (CSS) + an optional base theme (elements-kit/ui/styles.css); plus the otp-input custom element and overlay positioning/interaction behaviors. Catalog in src/ui/README.md |

Signals

Fine-grained reactive state. Signals track their dependencies automatically — only the exact computeds and effects that depend on a changed signal are re-evaluated.

import { signal, computed, effect, batch, untracked, onCleanup } from "elements-kit/signals";

const count = signal(0);
const doubled = computed(() => count() * 2);

const stop = effect(() => {
  console.log("count:", count()); // runs on every change
});

count(1);  // → count: 1
count(2);  // → count: 2
stop();    // unsubscribe

batch(() => { count(10); count(20); }); // single notification

const raw = untracked(() => count()); // read without subscribing

effect(() => {
  const id = setInterval(() => count(count() + 1), 1000);
  onCleanup(() => clearInterval(id)); // runs before re-run or on stop
});

Store

A store is a class whose fields are made reactive with @reactive. It holds shared state — no render(), no DOM — and any subscriber updates automatically.

import { reactive, computed } from "elements-kit/signals";

export class CartStore {
  @reactive() items: { name: string; price: number }[] = [];
  @reactive() discount = 0;

  total = computed(() =>
    this.items.reduce((s, i) => s + i.price, 0) * (1 - this.discount),
  );

  add(item: { name: string; price: number }) {
    this.items = [...this.items, item];
  }
}

export const cart = new CartStore();

Stores are framework-agnostic — the same instance drives a custom element, a React component, and a plain effect in sync.

JSX → DOM

JSX compiles directly to document.createElement. No virtual DOM, no diffing.

// This:
const el = <button on:click={() => count(count() + 1)}>{count}</button>;

// Is equivalent to:
const el = document.createElement("button");
el.addEventListener("click", () => count(count() + 1));
// `count` signal creates a live text node — updates in place on change

Passing a signal or () => T as a child or prop creates a live binding — the DOM updates in place, never re-rendering the surrounding tree.

const name = signal("Alice");

<p>Hello, {name}!</p>             // live text node
<input value={name} />             // live attribute
<div class:active={computed(() => name() !== "")} />  // reactive class
<span style:color={signal("red")} />  // reactive style

Prop namespaces

| Syntax | Effect | |--------|--------| | {signal} / {() => fn()} | Live-bound reactive child | | on:click={fn} | Event listener (case-preserving event name) | | class:active={bool} | Reactive classList.toggle | | style:color={value} | Reactive inline style property | | prop:foo={val} | Force property assignment (skips setAttribute) |

Class Components

Any class with a render() method returning an Element is a component. Components own their state and produce elements.

import { reactive, computed } from "elements-kit/signals";
import { render } from "elements-kit/render";

class Counter {
  @reactive() count = 0;
  doubled = computed(() => this.count * 2);

  render() {
    return (
      <section>
        <p>{() => this.count} × 2 = {this.doubled}</p>
        <button on:click={() => this.count++}>+1</button>
      </section>
    ) as Element;
  }
}

const unmount = render(document.getElementById("app")!, () => <Counter/>);

Custom Elements

ElementsKit enhances native HTMLElement subclasses — start with the platform, add only what you need.

import { reactive, computed } from "elements-kit/signals";
import { attributes, ATTRIBUTES as attr } from "elements-kit/attributes";
import { render } from "elements-kit/render";

@attributes
class CounterElement extends HTMLElement {
  static [attr] = {
    count(this: CounterElement, value: string | null) {
      this.count = Number(value ?? 0);
    },
  };

  @reactive() count = 0;
  doubled = computed(() => this.count * 2);

  #unmount?: () => void;

  connectedCallback() {
    this.#unmount = render(this, () => (
      <section>
        <p>{() => this.count} × 2 = {this.doubled}</p>
        <button on:click={() => this.count++}>+1</button>
      </section>
    ));
  }

  disconnectedCallback() {
    this.#unmount?.();
    this.#unmount = undefined;
  }
}

customElements.define("x-counter", CounterElement);

<x-counter count="5" /> — attribute bound, reactive, works in any HTML context.

Typed JSX for custom elements

Register the tag and augment ElementsKit.CustomElementRegistry in the global namespace — JSX infers the full prop shape (attributes, events, slots, children) from the class itself.

import { defineElement } from "elements-kit/custom-elements";

defineElement("x-counter", CounterElement);

declare global {
  namespace ElementsKit {
    interface CustomElementRegistry {
      "x-counter": typeof CounterElement;
    }
  }
}

// Now `<x-counter count={5} />` is fully typed.

See Types for the full set of prop-inference helpers.

React Integration

Connect signals and stores to React components via useSyncExternalStore:

import { useSignal, useScope } from "elements-kit/integrations/react";
import { cart } from "./cart-store";

function CartSummary() {
  // Reads a @reactive field — re-renders only when cart.items changes
  const items = useSignal(() => cart.items);
  const total = useSignal(cart.total); // Computed<T> works directly

  // Effects tied to this component's lifetime
  useScope(() => {
    effect(() => console.log("cart updated:", items));
  });

  return <p>{items.length} items — ${total.toFixed(2)}</p>;
}

The same cart store drives custom elements, React trees, and plain scripts — all in sync.

Utilities

Pre-built reactive wrappers around common browser APIs. Each utility lives at its own subpath (elements-kit/utilities/<name>) and ships as its own entry — you pay only for what you import. Full catalog in src/utilities/README.md.

createMediaQuery wraps window.matchMedia into a reactive signal — reads inside effects or computeds re-run automatically when the media query result changes.

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

const isDark = createMediaQuery("(prefers-color-scheme: dark)");
const isMobile = createMediaQuery("(max-width: 640px)");

effect(() => document.documentElement.classList.toggle("dark", isDark()));

Singletons like online, windowFocused, activeElement, and currentLocation are pre-instantiated — import and read them directly inside any reactive context.

import { effect } from "elements-kit/signals";
import { online } from "elements-kit/utilities/network";
import { windowFocused } from "elements-kit/utilities/window-focus";

effect(() => console.log("online:", online(), "focused:", windowFocused()));

Async & Promise

Two primitives convert imperative async work into reactive state: promise (minimal, any Promise → reactive state) and async (full controller with start/stop/run and optional reactive input).

promise

Wraps an async function (or raw Promise) into a ComputedPromise<T> — awaitable and callable as a reactive value. Exposes .state, .value, .reason, .result as reactive reads.

import { promise } from "elements-kit/utilities/promise";
import { effect } from "elements-kit/signals";

const user = promise(() => fetch("/api/user").then((r) => r.json()));

effect(() => {
  if (user.state === "pending")   console.log("loading…");
  if (user.state === "fulfilled") console.log("user:", user.value);
  if (user.state === "rejected")  console.log("error:", user.reason);
});

await user; // awaitable

ReactivePromise is the underlying class — use it when you want the reactive state getters without the Computed callable interface.

async

A controller around promise. The async function may be a plain function or a MaybeReactive<Fn> (so the body itself can re-read signals and rerun on change).

import { async } from "elements-kit/utilities/async";

const op = async(() => fetch("/api/items").then((r) => r.json()));

op.start();   // run with reactive tracking — reruns when tracked signals change
await op;     // awaitable (delegates to .then/.catch/.finally via .raw)
op.stop();    // halt reruns + fire registered cleanup
op.reset();   // stop + clear state back to `idle`

Reactive state getters: .state ("idle" | "pending" | "fulfilled" | "rejected" — idle before the first run, so .pending is false until you trigger one), .value, .reason, .result, .pending, .raw (the underlying ComputedPromise).

One-shot mutation (no tracking):

const del = async((id: number) =>
  fetch(`/api/items/${id}`, { method: "DELETE" }).then((r) => r.json()),
);

await del.run(42);

Async implements Symbol.dispose, so using auto-stops on scope exit:

{
  using poll = async(() => fetch("/api/poll").then((r) => r.json())).start();
  await poll;
} // poll.stop() here

Composing with retry, online, storage

async's reactive body composes with other utilities. Below: fetch a todo by id(), retry on failure with exponential backoff, pause while offline (returning the stale cached value), and refetch when the tab regains focus.

import { signal, effect, untracked, onCleanup } from "elements-kit/signals";
import { async } from "elements-kit/utilities/async";
import { retry } from "elements-kit/utilities/retry";
import { online } from "elements-kit/utilities/network";
import { windowFocused } from "elements-kit/utilities/window-focus";
import { createLocalStorage } from "elements-kit/utilities/storage";

const id = signal(1);
const cache = createLocalStorage<unknown>("todo-cache", null);

const fetchTodo = async(() => {
  if (!online()) return untracked(cache);   // pause while offline
  windowFocused();                          // refetch on tab focus
  return retry(() => {
    const controller = new AbortController();
    onCleanup(() => controller.abort());    // abort before each retry
    return fetch(`/api/todos/${id()}`, { signal: controller.signal })
      .then((r) => r.json())
      .then((value) => (cache(value), value));
  }, 3, (n) => n * 500)();                  // 0 ms, 500 ms, 1000 ms backoff
}).start();

effect(() => console.log(fetchTodo.state, fetchTodo.value));

For — Keyed List Rendering

Reconciles a reactive array into the DOM. Each item renders once per key — no full re-renders on reorder, add, or remove. T is inferred from each.

import { For } from "elements-kit/for";

<ul>
  <For each={todos} by={(todo) => todo.id}>
    {(todo) => (
      <li>
        <input type="checkbox" checked={computed(() => todo.done)} on:change={() => (todo.done = !todo.done)} />
        {todo.text}
      </li>
    )}
  </For>
</ul>

Prop types

A small set of type helpers derives JSX prop shapes from your components — no parallel declare global block to maintain. Full guide at docs/src/content/docs/elements/types.mdx.

| Helper | For | | ------ | --- | | PropsOf<C> | Unified — fn/class components give raw prop shapes; custom-element ctors give the full JSX surface (attrs, events, slots, children) | | Props<P> | Every prop accepts a value or a reactive source — declare it on a function component that should take signals (alias of MaybeReactiveProps<P>) | | MaybeReactiveProps<P> | The same wrap under its descriptive name — used for class constructor params and the intrinsic-element surface | | ComputedProps<P> | The mirror of Props<P> — what a body reads after computedProps: every key a Computed<T>, optional keys included | | MaybeReactive<T> | Scalar value-or-getter (from elements-kit/signals) | | Require<P, K> | Promote optional keys to required |

Intrinsic and custom-element attributes accept a static value or a signal at every JSX call site — the runtime's LibraryManagedAttributes handles that wrap for you.

Function components are different: props arrive exactly as the caller wrote them, so the declared type is the contract on both sides. Declare plain types for static-only props; declare Props<P> (or MaybeReactive<T> per key) to accept signals, and read with resolve or by handing the prop to JSX:

import { resolve } from "elements-kit/signals";
import type { Props } from "elements-kit/jsx-runtime";

function Greeting(props: Props<{ name: string }>) {
  return <p>Hello, {props.name}</p>; // JSX accepts either form
}

function Plain(props: { name: string }) {
  return <p>Hello, {props.name}</p>; // callers may not pass a signal
}

To read one uniform shape instead, convert the props with computedProps — every key becomes a getter, so no read needs resolve. Defaults go on the call, since a getter is always truthy:

import { computedProps } from "elements-kit/signals";

function Chat(raw: Props<{ placeholder?: string }>) {
  const props = computedProps(raw);
  return <input placeholder={props.placeholder() ?? "Ask anything…"} />;
}

A prop that takes arguments (a render prop, a handler with parameters) is rejected there — a callable cannot be told apart from a getter, since Computed<T> is () => T. Read those off the raw props instead.

@reactive() Decorator

Makes any class field reactive — reads subscribe, writes trigger updates.

import { reactive, computed } from "elements-kit/signals";

class TodoApp {
  @reactive() todos: Todo[] = [];
  @reactive() showDone = true;

  visible = computed(() =>
    this.showDone ? this.todos : this.todos.filter((t) => !t.done),
  );
}

@attributes Decorator

Wires observedAttributes and attributeChangedCallback from a static map:

import { attributes, ATTRIBUTES as attr } from "elements-kit/attributes";

@attributes
class MyElement extends HTMLElement {
  static [attr] = {
    value(this: MyElement, v: string | null) {
      this.value = v ?? "";
    },
  };

  @reactive() value = "";
}

For named slots, declare @slot() properties — reading places the region in your template, assigning fills it (from any framework, or none):

import { slot } from "elements-kit/slot";

class Card extends HTMLElement {
  @slot() header!: Node
  @slot() footer!: Node
}

For typed events, declare a static events map:

class XPicker extends HTMLElement {
  declare static events: { commit: CustomEvent<number> };
}
// PropsOf<typeof XPicker> now includes `on:commit`

Roadmap

Directions under consideration for the SSR/islands layer (nothing here is committed):

  • Declarative Shadow DOM & custom-element SSR — emit <template shadowrootmode> on the server and claim shadow roots at hydration, so elements-kit custom elements server-render inside any framework's HTML.
  • ErrorBoundary + Await error fallback — first-class rejection handling; today a rejected awaitable settles into its reason.
  • Out-of-order streaming + richer ek-data serialization — stream fallbacks immediately and swap content in when ready (today Await holds the stream); serialize Dates/Maps/cyclic values instead of JSON-only.
  • Astro extras — View Transitions survival, a custom client:interaction directive, a live server:defer demo.
  • Hardening toward stable — real-Astro end-to-end tests in CI (dev + build/preview), an Astro renderer-contract type-pin, SSR↔client parity and escaping property tests, seeding-alignment and stream-robustness suites. The await / integrations/astro entries drop their experimental badges when this lands.

Learn more