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

@beardcoder/stitch-js

v1.0.4

Published

A tiny, composable progressive enhancement framework for the browser.

Readme

stitch-js

A tiny, composable progressive enhancement framework for the browser.

Enhance existing HTML with interactive behavior — without a virtual DOM, SPA framework, or build step requirement.

Features

  • Progressive enhancement first — HTML works without JS
  • Generic component modeldefineComponent gives you scoped queries, delegated events, attribute parsing, and auto-cleanup
  • Reactive storecreateStore, computed, and effect for state management
  • Tree-shakeable — import only what you use
  • Accessible — ARIA-aware utilities built in
  • Tiny — ~2.7 kB min+gz full bundle, no dependencies
  • TypeScript — strict types and great DX
  • Composable — stack multiple behaviors on one element
  • Idempotent — safe to call enhance() multiple times

Bundle Size

All sizes are minified + gzipped. Tree-shaking ensures you only pay for what you import.

| Import | min | min+gz | |---|--:|--:| | Full bundle (everything) | 6,284 B | 2,709 B | | Core only (enhance, defineComponent, etc.) | 2,130 B | 1,116 B | | Store only (createStore, computed, effect) | 748 B | 361 B | | Core + Tabs | 2,789 B | 1,387 B | | Core + Accordion | 2,841 B | 1,393 B | | Core + Form | 2,571 B | 1,352 B | | Core + Animate | 2,156 B | 1,131 B |

Install

bun add @beardcoder/stitch-js
# or
npm install @beardcoder/stitch-js

Quick Start

<div data-tabs>
  <div data-tab-list>
    <button data-tab>One</button>
    <button data-tab>Two</button>
  </div>
  <div data-tab-panel>Content one</div>
  <div data-tab-panel>Content two</div>
</div>

<script type="module">
  import { enhance, tabs } from "@beardcoder/stitch-js";
  enhance("[data-tabs]", tabs());
</script>

Core API

enhance(selector, factory, options?)

Find all elements matching selector and attach the component factory to each. Idempotent — calling twice on the same element is a no-op.

import { enhance } from "@beardcoder/stitch-js";
import { accordion } from "@beardcoder/stitch-js/components/accordion";

const instances = enhance("[data-accordion]", accordion({ multiple: true }));
instances.forEach((i) => i.destroy());

Options:

  • root — scope the query to a subtree (default: document)
  • options — override the factory options

destroyAll(selector, factory?, root?)

Destroy all enhanced instances on matching elements. Pass a factory to only remove that behavior.

register / init / autoInit

Declarative auto-initialization:

import { register, autoInit, tabs, accordion } from "@beardcoder/stitch-js";

register("[data-tabs]", tabs());
register("[data-accordion]", accordion());
autoInit(); // runs on DOMContentLoaded

defineComponent — Generic Component Builder

The core primitive for creating components. Provides a scoped ComponentContext with DOM queries, delegated event handling, attribute parsing, and automatic cleanup.

import { defineComponent, enhance } from "@beardcoder/stitch-js";

const toggle = defineComponent(
  { activeClass: "is-active" },   // typed defaults
  (ctx) => {
    ctx.on("click", () => {
      ctx.el.classList.toggle(ctx.options.activeClass);
    });
  },
);

enhance("[data-toggle]", toggle());
enhance("[data-toggle]", toggle({ activeClass: "open" }));

ComponentContext

Every setup function receives a ctx with these methods:

| Method | Description | |---|---| | ctx.el | The root HTMLElement | | ctx.options | Resolved options (defaults + overrides) | | ctx.query(selector) | querySelector scoped to root | | ctx.queryAll(selector) | querySelectorAll scoped to root | | ctx.attr(name, fallback?) | Read a data-* attribute | | ctx.attrJson(name) | Read + JSON.parse a data-* attribute | | ctx.on(event, handler) | Attach event listener (auto-cleanup) | | ctx.on(event, selector, handler) | Delegated event listener (auto-cleanup) | | ctx.aria(el, attrs) | Set aria-* attributes | | ctx.uid(prefix?) | Generate a unique ID | | ctx.onDestroy(fn) | Register cleanup callback | | ctx.emit(name, detail?) | Dispatch a CustomEvent from root | | ctx.sync(store, listener) | Subscribe to a store (auto-cleanup on destroy) | | ctx.data<T>() | Read structured data from HTML (see below) |

The setup function can also return a cleanup function directly:

const counter = defineComponent({ start: 0 }, (ctx) => {
  const interval = setInterval(() => { /* ... */ }, 1000);
  return () => clearInterval(interval);
});

ctx.data<T>() — Passing Data from HTML to JS

Pass structured data from server-rendered HTML into your components. This is essential for integrating external libraries (TanStack Table, charts, maps, etc.) that need configuration or datasets.

ctx.data<T>() reads from three sources in priority order:

  1. <script type="application/json"> — best for large payloads (column defs, row data, etc.)
  2. data-props attribute — convenient for smaller inline JSON
  3. All data-* attributes — automatic fallback, camelCased keys
<!-- Option 1: Script tag for large/complex data -->
<div data-table>
  <script type="application/json">
    {
      "columns": [
        { "header": "Name", "accessorKey": "name" },
        { "header": "Age", "accessorKey": "age" }
      ],
      "rows": [
        { "name": "Alice", "age": 30 },
        { "name": "Bob", "age": 25 }
      ],
      "endpoint": "/api/users"
    }
  </script>
  <table><!-- server-rendered fallback rows --></table>
</div>

<!-- Option 2: data-props attribute for smaller payloads -->
<div data-chart data-props='{"type":"bar","labels":["A","B","C"]}'></div>

<!-- Option 3: Individual data attributes (auto-collected) -->
<div data-map data-lat="48.137" data-lng="11.576" data-zoom="12"></div>
import { defineComponent, enhance } from "@beardcoder/stitch-js";

// TanStack Table example
const dataTable = defineComponent({}, (ctx) => {
  const { columns, rows, endpoint } = ctx.data<{
    columns: ColumnDef[];
    rows: Row[];
    endpoint: string;
  }>();

  const table = createTable({ columns, data: rows });
  // render table into ctx.el ...
});

enhance("[data-table]", dataTable());

Reactive Store

A minimal, dependency-free reactive primitive.

createStore(initial)

import { createStore } from "@beardcoder/stitch-js";

const count = createStore(0);

count.get();            // 0
count.set(1);           // updates + notifies
count.update(n => n + 1); // transform + notify

const unsub = count.subscribe((value, prev) => {
  console.log(`${prev} → ${value}`);
});
unsub(); // stop listening

computed(sources, derive)

Derived read-only store that auto-updates when sources change.

import { createStore, computed } from "@beardcoder/stitch-js";

const firstName = createStore("Jane");
const lastName = createStore("Doe");

const fullName = computed(
  [firstName, lastName],
  (first, last) => `${first} ${last}`,
);

fullName.get(); // "Jane Doe"
firstName.set("John");
fullName.get(); // "John Doe"

effect(sources, fn)

Run a side-effect when sources change. Runs immediately, re-runs on change, and supports cleanup.

import { createStore, effect } from "@beardcoder/stitch-js";

const theme = createStore("light");

const stop = effect([theme], (value) => {
  document.documentElement.dataset.theme = value;
  return () => {
    // cleanup before re-run or on stop
  };
});

theme.set("dark"); // side-effect fires
stop();            // tear down

Combining store with components

import { defineComponent, enhance, createStore, effect } from "@beardcoder/stitch-js";

const counter = defineComponent({ start: 0 }, (ctx) => {
  const count = createStore(ctx.options.start);
  const display = ctx.query("[data-count]");

  const stopEffect = effect([count], (n) => {
    if (display) display.textContent = String(n);
  });

  ctx.on("click", "[data-increment]", () => count.update(n => n + 1));
  ctx.on("click", "[data-decrement]", () => count.update(n => n - 1));
  ctx.onDestroy(stopEffect);
});

enhance("[data-counter]", counter());

ctx.sync(store, listener) — Sync a store with a component

Subscribe to a store inside a component with automatic cleanup on destroy. The listener fires immediately with the current value and re-fires on every change.

import { defineComponent, enhance, createStore } from "@beardcoder/stitch-js";

const count = createStore(0);

const display = defineComponent({}, (ctx) => {
  ctx.sync(count, (n) => {
    ctx.el.textContent = String(n);
  });
});

enhance("[data-display]", display());
count.set(5); // all [data-display] elements update to "5"

persistedStore(key, initial, options?)

A reactive store that persists its value to localStorage (or sessionStorage) and syncs across browser tabs via the storage event. It implements the same Store interface, so it works with effect, computed, and ctx.sync.

import { persistedStore, effect } from "@beardcoder/stitch-js";

const theme = persistedStore("theme", "light");

theme.get();        // reads from localStorage, or falls back to "light"
theme.set("dark");  // updates localStorage + notifies subscribers

// Works with effect, computed, ctx.sync — just like createStore
effect([theme], (value) => {
  document.documentElement.dataset.theme = value;
});

Options:

| Option | Default | Description | |---|---|---| | storage | localStorage | Storage backend (localStorage or sessionStorage) | | serialize | JSON.stringify | Custom value → string serializer | | deserialize | JSON.parse | Custom string → value deserializer |


Router

A simple hash-based client-side router. Exposes a store-compatible interface so it works with effect, computed, and ctx.sync.

createRouter(patterns?)

import { createRouter, effect } from "@beardcoder/stitch-js";

const router = createRouter(["", "about", "users/:id"]);

effect([router], (route) => {
  console.log(route.path, route.params);
});

router.push("users/42"); // logs "users/42" { id: "42" }

Route state:

| Property | Type | Description | |---|---|---| | path | string | The full hash path (without the leading #) | | params | Record<string, string> | Named params extracted from the pattern | | pattern | string | The matched pattern, or "" if none matched |

Methods:

| Method | Description | |---|---| | router.get() | Get the current route state | | router.subscribe(fn) | Subscribe to route changes | | router.push(path) | Navigate to a hash path | | router.destroy() | Stop listening to hash changes |


Example Components

The built-in components are examples built on defineComponent. Use them directly or as reference for your own.

Tabs

<div data-tabs>
  <div data-tab-list>
    <button data-tab>Tab 1</button>
    <button data-tab>Tab 2</button>
  </div>
  <div data-tab-panel>Panel 1</div>
  <div data-tab-panel>Panel 2</div>
</div>
enhance("[data-tabs]", tabs({ defaultIndex: 0 }));

Options: listSelector, tabSelector, panelSelector, defaultIndex Keyboard: Arrow Left/Right, Home, End.

Accordion

<div data-accordion>
  <div data-accordion-item>
    <button data-accordion-trigger>Section 1</button>
    <div data-accordion-content>Content</div>
  </div>
</div>
enhance("[data-accordion]", accordion({ multiple: false }));

Options: itemSelector, triggerSelector, contentSelector, multiple Keyboard: Arrow Up/Down, Home, End.

Form

<form data-form action="/api/contact" method="post">
  <input name="email" type="email" required />
  <button type="submit">Send</button>
</form>
enhance("[data-form]", form({
  onSuccess: (el, res) => console.log("Sent!", res),
  onError: (el, err) => console.error("Failed", err),
}));

Animate

<div data-animate class="fade-in">Appears on scroll</div>
.fade-in { opacity: 0; transition: opacity 0.4s ease; }
.fade-in.is-visible { opacity: 1; }
enhance("[data-animate]", animate({ once: true }));

Writing Custom Components

Use defineComponent for scoped DOM access, delegated events, and auto-cleanup:

import { defineComponent, enhance } from "@beardcoder/stitch-js";

const tooltip = defineComponent(
  { position: "top" as "top" | "bottom" },
  (ctx) => {
    const tip = document.createElement("span");
    tip.textContent = ctx.attr("tooltip") ?? "";
    tip.className = `tooltip tooltip--${ctx.options.position}`;

    ctx.on("mouseenter", () => ctx.el.appendChild(tip));
    ctx.on("mouseleave", () => tip.remove());
    ctx.onDestroy(() => tip.remove());
  },
);

enhance("[data-tooltip]", tooltip());
enhance("[data-tooltip-bottom]", tooltip({ position: "bottom" }));

Or use the low-level ComponentFactory type for maximum control:

import type { ComponentFactory } from "@beardcoder/stitch-js";

function myBehavior(): ComponentFactory {
  return (el) => {
    // attach behavior...
    return { destroy() { /* cleanup */ } };
  };
}

Development

bun install
bun test
bun run build
bun run typecheck

Rolling Releases (GitHub + npm)

Releases are fully automated via GitHub Actions + Semantic Release.

  • Every push to main runs verification (test, typecheck, build).
  • If verification succeeds, Semantic Release creates a GitHub Release and publishes to npm.

Required GitHub Secrets

  • NPM_TOKEN (npm automation token with publish rights)

Commit Style (required for automatic versioning)

  • fix:, perf:, refactor:, chore:, docs:, test:, build:, ci: -> patch release
  • feat: -> minor release
  • feat!: or BREAKING CHANGE: -> major release

License

MIT