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.

$ deno add jsr:@ra9/tan-compose
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

# Deno deno add jsr:@ra9/tan-compose # Node / npm (via JSR) npx jsr add @ra9/tan-compose

Define and register

import { describe, build } from "@ra9/tan-compose"; const button = describe({ tag: "button", template: "Click 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>