Declarative Web Components
in a hundred lines.

Tan Compose turns a plain JavaScript object into a real custom element — Shadow DOM, theming, lifecycle hooks, reactive state, and listener cleanup included. No framework, no compile step.

<script> import "https://cdn.jsdelivr.net/gh/RA9/tan-compose@v1.3.0/dist/mod.js" </script>
my-counter.ts
import { describe, build, html } from "@ra9/tan-compose";

build("my-counter", describe({
  template: ({ state }) => html`
    <span>${state.count ?? 0}</span>
    <button class="bump">+</button>
  `,
  events: {
    "click .bump": (_e, ctx) =>
      ctx.setState("count",
        (ctx.state.count ?? 0) + 1),
  },
}));
// That's it. Use <my-counter> anywhere.
Why tan-compose

Tiny, predictable, browser-native.

One describe() call to define a component, one build() call to register it. Everything else is the platform.

01 — runtime

~5 KB minified

Zero dependencies. Ships as a single ESM module.

02 — encapsulation

Shadow DOM by default

Styles never leak. Theme via CSS custom properties on :host.

03 — lifecycle

Predictable mount hooks

beforeMount / afterMount / unmount, errors caught and logged with the tag name.

04 — reactivity

Opt-in observed attrs

Declare observedAttributes and changes re-render. No magic.

05 — state

setState triggers render

Skips re-render when the value is Object.is-equal.

06 — safety

Listener cleanup

Per-render cleanup queue. No leaked handlers on re-render or unmount.

Quick start

From zero to a custom element.

Install with one command. Define your component as a plain object. Use it as HTML.

Install

<!-- No build step — load from jsDelivr, served straight from GitHub --> <script type="module" src="https://cdn.jsdelivr.net/gh/RA9/tan-compose@v1.3.0/dist/mod.js"></script>

Or import the ES module directly in your own code — pin a tag, swap @v1.3.0 for any release:

import { describe, build, html } from "https://cdn.jsdelivr.net/gh/RA9/tan-compose@v1.3.0/dist/mod.js";

Define and register

import { describe, build, html } from "@ra9/tan-compose"; const button = describe({ tag: "button", template: ({ props }) => html`Click ${props.label ?? "me"}`, props: { label: { type: "string", default: "me" } }, styles: { padding: "10px 18px", background: "#14171f", color: "white", border: "none", borderRadius: "6px", cursor: "pointer", }, action: () => alert("Hello, Tan Compose!"), }); build("my-button", button);

Use in HTML

<my-button></my-button>
Live demos

Each block below is a real custom element.

Built with the same library you'd npm install. Open DevTools to inspect the Shadow DOM.

Reactive counter
<demo-counter>
Themed buttons
<demo-buttons>
Composed card
<demo-card>