@ra9/tan-compose-kit

Components, ready to drop in.

Battle-tested primitives built on @ra9/tan-compose. Every component is a real custom element — works in plain HTML, React, Vue, Astro, anywhere a custom element is allowed. Theme via CSS custom properties on the host.

deno add jsr:@ra9/tan-compose-kit
npx jsr add @ra9/tan-compose-kit
<script type="module" src="https://cdn.jsdelivr.net/gh/RA9/tan-compose@kit-v1.11.0/kit/dist/themes/tokens.min.js"></script> <script type="module" src="https://cdn.jsdelivr.net/gh/RA9/tan-compose@kit-v1.11.0/kit/dist/kit.min.js"></script>

Then import "@ra9/tan-compose-kit" for everything, or a single component like import "@ra9/tan-compose-kit/button". The CDN tags register every <tc-*> element on load — no import needed.

<tc-button>Docs →

Button

Variants (primary / secondary / ghost / danger), three sizes, disabled and loading states, full-width modifier. Content via the default slot.

Primary Secondary Ghost Danger Small Large Loading Disabled

Pass href and the button renders as an <a> instead — same styling, link semantics. Use target="_blank" for new-window links (gets rel="noopener" automatically).

Read the docs → Browse examples GitHub ↗ Disabled link

block stretches a button full-width — handy at the end of a mobile form. The default slot accepts inline icons alongside text.

Continue Save draft
↑ Upload Download ↓ ✕ Remove Accent override
import "@ra9/tan-compose-kit/button"; <tc-button variant="primary">Save</tc-button> <tc-button variant="danger" loading>Deleting…</tc-button> // Pass href to render as an anchor instead of a button. <tc-button variant="primary" href="/docs">Read the docs</tc-button> <tc-button href="https://github.com/..." target="_blank">GitHub</tc-button>
<tc-input>Docs →

Input (form-associated)

Submits with native <form> via ElementInternals. Label, helper or error text, disabled / required flags. Resets cleanly with the parent form.

Submit Reset
Click submit to see what gets sent.

The type prop forwards to the native input, so you get the right mobile keyboard and validity rules per field.

import "@ra9/tan-compose-kit/input"; <form> <tc-input label="Email" name="email" type="email" required></tc-input> <tc-button>Submit</tc-button> </form>
<tc-select>Docs →

Select

Form-associated dropdown wrapping a native <select>. Same chrome as <tc-input>; submits with <form>.

No selection.

Pass options as a JSON attribute to render entirely from markup — no script required. Match value to a value to preselect it, or mark an option "disabled": true.

import "@ra9/tan-compose-kit/select"; <tc-select label="Country" name="country"></tc-select> <script> document.querySelector("tc-select").options = [ { value: "us", label: "United States" }, { value: "ca", label: "Canada" }, ]; </script>
<tc-combobox> · new in 1.5Docs →

Combobox

Searchable dropdown with optional multi-select, tag chips, and per-option icon prefixes — the "select N from many" widget that the native <select multiple> isn't. Form-associated; submits one entry per selected value with a name.

No selection yet.

options is a JSON prop, so a combobox can render from pure markup too. Each entry takes an optional icon. Preselect via value (single) or a comma-separated list (multiple).

import "@ra9/tan-compose-kit/combobox"; // HTML <tc-combobox label="Countries" name="countries" multiple></tc-combobox> // JS — feed it an options array const combo = document.querySelector("tc-combobox"); combo.options = [ { value: "US", label: "United States", icon: "🇺🇸" }, { value: "FR", label: "France", icon: "🇫🇷" }, { value: "JP", label: "Japan", icon: "🇯🇵" }, ]; combo.addEventListener("tc-change", (e) => { const selected = e.detail.value; // Array<string> in multiple mode ... });
<tc-tabs>Docs →

Tabs

Tablist with arrow-key roving focus, named slots per panel, and ARIA roles. Active tab reflects to attribute.

Overview. A summary panel. Use slots named after the tab id.
Metrics. Anything you'd like — charts, tables, stat cards.
Settings. Form fields go here.

Define tabs inline with the tabs JSON attribute and slot panels by id — no script needed. Set active to open a tab other than the first.

Overview. A summary of what this thing does.
Props. Opened by default via active="props".
Events. tc-tab-change fires on every switch.
import "@ra9/tan-compose-kit/tabs"; <tc-tabs active="overview"> <div slot="overview">…</div> <div slot="metrics">…</div> </tc-tabs> <script> document.querySelector("tc-tabs").tabs = [ { id: "overview", label: "Overview" }, { id: "metrics", label: "Metrics" }, ]; </script>
<tc-toast>Docs →

Toast

Inline notification with variant, auto-dismiss, and a close button. Set duration="0" to keep it open until the user closes it.

Info toast Success toast Warning toast Error toast

With open set, a toast renders inline immediately — here with duration="0" so it stays put. Leave message empty to use the default slot for links or formatting, and add dismissible="false" to drop the close button.

Saved. View it →
import "@ra9/tan-compose-kit/toast"; const t = document.createElement("tc-toast"); t.variant = "success"; t.message = "Saved."; t.duration = 4000; host.appendChild(t); t.open = true;
<tc-stat>Docs →

Stat

Pure-presentation card for a metric. Label, value, optional prefix/suffix, optional delta with up/down/neutral trend.

Drop the delta prop for a bare metric, or override the trend tokens for a custom palette. trend only colors the delta — pick it for the visual you want.

import "@ra9/tan-compose-kit/stat"; <tc-stat label="Revenue" value="12,840" prefix="$" delta="+8.2% vs last month" trend="up" ></tc-stat>
<tc-table>Docs →

Datatable

Paginated, filterable, sortable. Pass rows and columns as JSON props. Emits tc-row-click when a row is clicked.

Click a row to see it here.

declarative rows + columns via JSON attributes

empty state · custom empty-text · filter off

import "@ra9/tan-compose-kit/table"; const table = document.querySelector("tc-table"); table.columns = [ { key: "name", label: "Name" }, { key: "role", label: "Role", sortable: true }, { key: "joined", label: "Joined", sortable: true }, ]; table.rows = await fetch("/api/people").then(r => r.json()); table.addEventListener("tc-row-click", (e) => console.log(e.detail.row));
<tc-textarea>Docs →

Textarea

Multi-line form-associated input. Same chrome as <tc-input>; submits with <form>.

import "@ra9/tan-compose-kit/textarea"; <tc-textarea label="Notes" name="notes" rows="6"></tc-textarea>
<tc-radio-group>Docs →

Radio group

Single-choice form-associated control. Pass options as a JSON prop; layout horizontally or vertically.

import "@ra9/tan-compose-kit/radio-group"; <tc-radio-group label="Plan" name="plan"></tc-radio-group> <script> document.querySelector("tc-radio-group").options = [ { value: "free", label: "Free" }, { value: "pro", label: "Pro" }, { value: "team", label: "Team" }, ]; </script>
<tc-file>Docs →

File

Form-associated file picker with a styled trigger button. multiple and accept work the same as the native <input type="file">.

import "@ra9/tan-compose-kit/file"; <tc-file label="Avatar" name="avatar" accept="image/*"></tc-file>
<tc-code> · new in 1.3Docs →

Code block

Styled code surface with optional copy button and a language/filename label. The kit doesn't ship a syntax highlighter — pre-tokenize with .tc-kw, .tc-str, .tc-com, .tc-num, .tc-tag spans inside the slot. They project through and pick up the kit's colors.

import { describe, build } from "@ra9/tan-compose"; const button = describe({ tag: "button", template: "Click me", action: () => alert("hi"), }); build("my-btn", button); { "name": "tan-compose", "version": "1.1.0" }
deno add jsr:@ra9/tan-compose-kit deno task blog
<tc-callout variant="info" title="Tip"> Components ship as standalone tags. <!-- tree-shakable --> </tc-callout>
import "@ra9/tan-compose-kit/code"; <tc-code language="ts" copy> <span class="tc-kw">const</span> x = "hi"; </tc-code>
<tc-callout> · new in 1.3Docs →

Callout

Note / info / success / warning / danger admonition box with an icon, optional title, and a colored left border. danger uses role="alert"; others use role="note".

The kit's components live in Shadow DOM, so they don't fight your global CSS. render on a column only takes effect when columns are set via the JS property, not via a JSON attribute. Changes published to all subscribers. SSR support is on the v1.2 roadmap; see CHANGELOG for details. Deleting a customer cannot be undone after 10 minutes.
Compact callouts drop the title and tighten padding for inline notes. Deprecated — switch to build() before v2. This action is irreversible.
No title needed for a single confirming line. Drop the title for short, standalone asides.
import "@ra9/tan-compose-kit/callout"; <tc-callout variant="warning" title="Coming soon"> SSR support is on the v1.2 roadmap. </tc-callout>
<tc-toc> · new in 1.3Docs →

Table of contents

Auto-builds a sticky sidebar of links by scanning a target container for h2/h3 headings. Tracks the currently-visible heading via IntersectionObserver. Headings without id get one assigned, so anchor links work out of the box.

Introduction

Click a link on the left to jump to a heading.

Setup

Install

Use deno add jsr:@ra9/tan-compose-kit.

First component

Drop a <tc-button> in your HTML.

Patterns

The kit's primitives compose nicely — see the admin demo.

Reference

Full API at docs.html.

Overview

A deeper TOC that also lists h4 headings.

Concepts

Core ideas before the API.

Tokens

Design tokens drive theming.

Slots

Project content into components.

Recipes

Forms

Compose fields with a stack.

import "@ra9/tan-compose-kit/toc"; <tc-toc target="article" levels="h2,h3"></tc-toc> <article> <h2>Introduction</h2> <h2>Setup</h2> … </article>
<tc-stack> · <tc-cluster> · <tc-grid>Docs →

Layout primitives

Three small components for the most common compositions: vertical flow (stack), horizontal flow with wrap (cluster), and an auto-fit grid (grid). Use the --tc-space-N scale or any CSS length for the gap.

tc-stack gap="3"

First Second Third

tc-cluster gap="2" justify="between"

a b c d

tc-grid min="160px"

tc-stack gap="5" align="center"

Centered In the middle

tc-grid columns="4" gap="3"

tc-cluster gap="2" justify="center"

design build review ship

tc-cluster gap="1.5rem" align="baseline" wrap="false"

One Two Three
import "@ra9/tan-compose-kit/stack"; import "@ra9/tan-compose-kit/cluster"; import "@ra9/tan-compose-kit/grid"; <tc-stack gap="4"> … </tc-stack> <tc-cluster gap="3" justify="between"> … </tc-cluster> <tc-grid min="220px" gap="4"> … </tc-grid>
<tc-checkbox>Docs →

Checkbox

Form-associated checkbox wrapping a native input. Label, helper/error text, indeterminate state.

import "@ra9/tan-compose-kit/checkbox"; <tc-checkbox label="Subscribe" name="opt-in"></tc-checkbox>
<tc-switch>Docs →

Switch

Accessible toggle with role="switch". Form-associated; keyboard-operable with Space and Enter.

import "@ra9/tan-compose-kit/switch"; <tc-switch label="Enable notifications" name="notify"></tc-switch>
<tc-card>Docs →

Card

Layout primitive with title/subtitle and named slots for media / header / footer. Padded, bordered, and elevated modifiers.

Drop any content in the default slot. Cards are pure layout primitives — no behavior of their own. Use elevated cards to lift important content off the page background.
Cancel Save
Media card
Full‑bleed media slot on top; header and body padded below it.
Sits flat on the surface — for nesting inside a panel that already has its own frame. Tighter padding and type for dense dashboards. Roomier padding for hero panels and marketing surfaces.
import "@ra9/tan-compose-kit/card"; <tc-card title="Settings" subtitle="Account preferences" elevated> <p>Body content here.</p> <div slot="footer"> <tc-button>Save</tc-button> </div> </tc-card>
<tc-badge>Docs →

Badge

Compact label for status. Five variants and two sizes; opt into pill rounding.

neutral info success warning danger pill small
Live Beta Deprecated
Active Pending 3 12 v1.6
import "@ra9/tan-compose-kit/badge"; <tc-badge variant="success">active</tc-badge> <tc-badge variant="warning" pill>pending</tc-badge>
<tc-skeleton>Docs →

Skeleton

Placeholder shown while content loads. Configure width, height, rounded for circular shapes (avatars). Respects prefers-reduced-motion.

import "@ra9/tan-compose-kit/skeleton"; <tc-skeleton width="40px" height="40px" rounded></tc-skeleton> <tc-skeleton width="60%" height="14px"></tc-skeleton>
<tc-pagination> · new in 1.4Docs →

Pagination

Prev/next plus a windowed list of page numbers. Set current and total; the component emits tc-page-change with the requested page — the parent decides whether to accept. siblings and boundaries control the window shape.

current: 1 (click a page to change)
import "@ra9/tan-compose-kit/pagination"; const pager = document.querySelector("tc-pagination"); pager.addEventListener("tc-page-change", (e) => { pager.current = e.detail.page; loadPage(e.detail.page); }); <tc-pagination current="1" total="20" siblings="2"></tc-pagination>
<tc-accordion> · new in 1.6Docs →

Accordion

Disclosure group built on native <details>. Single-open by default; mode="multi" lets several stay open. Keyboard nav between summaries, animated caret.

What's inside the kit?
Form fields, layout primitives, overlays, data display, and content components — all themeable, all framework-neutral.
Does it work in SSR / static sites?
Yes. Web Components upgrade on the client; markup degrades to plain HTML in the meantime.
How do I theme it?
Override CSS custom properties on the host or globally. Every component documents its tokens.
Shipping
Orders ship within two business days. Tracking is emailed once your package leaves the warehouse.
Returns
Return any unused item within 30 days for a full refund. In mode="multi", opening this leaves the others open.
Warranty
Every product is covered by a one-year limited warranty against manufacturing defects.
Borderless variant
With bordered="false" the frame and dividers are dropped — useful when nesting inside a panel that already has its own border.
Still single-open
Default mode="single" still applies: opening one item closes the rest.
import "@ra9/tan-compose-kit/accordion"; <tc-accordion> <details open><summary>Item one</summary><p>…</p></details> <details><summary>Item two</summary><p>…</p></details> </tc-accordion>
<tc-tooltip> · new in 1.6Docs →

Tooltip

Hover or focus tooltip anchored to a slotted trigger. Renders in the browser top-layer (popover API), so nothing clips it. Auto-flips placement when it would overflow.

Copy Save With shortcut Open ⌘O
Top Bottom Left Right
0ms 200ms 600ms Delete
import "@ra9/tan-compose-kit/tooltip"; <tc-tooltip text="Copy to clipboard"> <tc-button variant="ghost">Copy</tc-button> </tc-tooltip>
<tc-popover> · new in 1.6Docs →

Popover

Click-triggered floating panel — menus, filter forms, quick actions. Outside click and Esc dismiss by default. Auto-flips placement on overflow.

Profile ▾
Mia Carter mia@example.com
Account settings Sign out
Filter ▾
Filter
Status
Reset Apply
Help
Keyboard shortcuts Press ? anywhere to reopen this panel.
Dark popover
Inverted surface Themed entirely through CSS tokens.
import "@ra9/tan-compose-kit/popover"; <tc-popover> <tc-button slot="trigger">Profile ▾</tc-button> <div>Menu content…</div> </tc-popover>
<tc-drawer> · new in 1.6Docs →

Drawer

Side sheet that slides in from any edge. Backed by the native <dialog>: focus is trapped and the page scroll stays put. side picks the edge, size the width or height.

Open right → ← Open left Open from bottom ↓
Cancel Apply

This pattern works well on mobile for action sheets.

import "@ra9/tan-compose-kit/drawer"; <tc-drawer id="filters" title="Filters" side="right"> <div>…body…</div> <div slot="footer"><tc-button>Apply</tc-button></div> </tc-drawer> // Toggle open document.querySelector("#filters").open = true;
<tc-progress> · new in 1.6Docs →

Progress

Linear or circular, determinate or indeterminate, three sizes. Proper ARIA progressbar semantics, and the indeterminate animations respect prefers-reduced-motion.

import "@ra9/tan-compose-kit/progress"; <tc-progress value="68" showLabel></tc-progress> <tc-progress variant="circular" value="80" size="lg" showLabel></tc-progress> <tc-progress indeterminate></tc-progress>
<tc-stepper> · new in 1.6Docs →

Stepper

Multi-step indicator for wizards, onboarding, checkout. Horizontal or vertical, optional clickable mode to let users jump between steps.

import "@ra9/tan-compose-kit/stepper"; <tc-stepper active="1" steps='[ {"title":"Account"}, {"title":"Profile"}, {"title":"Workspace"}, {"title":"Done"} ]' ></tc-stepper>
<tc-avatar> · <tc-avatar-group> · new in 1.6Docs →

Avatar

User avatar with image and deterministic-tint initials fallback — Mia is always blue, Jamal is always brown. Status dot, ring for stacking, and an avatar-group for clusters with overflow "+N".

import "@ra9/tan-compose-kit/avatar"; import "@ra9/tan-compose-kit/avatar-group"; <tc-avatar name="Mia Carter" status="online"></tc-avatar> <tc-avatar-group max="4"> <tc-avatar name="Mia"></tc-avatar> <tc-avatar name="Jamal"></tc-avatar> ... </tc-avatar-group>
<tc-rating> · new in 1.6Docs →

Rating

Star rating input with optional half-stars, keyboard nav, hover preview, and a read-only display mode. Clip-path keeps the half-fill exact at any zoom.

4.5 (281 reviews)
pick a rating above
import "@ra9/tan-compose-kit/rating"; <tc-rating value="3" allowHalf size="lg"></tc-rating> <tc-rating value="4.5" allowHalf readonly></tc-rating>
<tc-slider> · new in 1.6Docs →

Slider

Themed range input. Built on native <input type="range"> so keyboard and touch behavior come for free. Optional ticks, label, suffix.

import "@ra9/tan-compose-kit/slider"; <tc-slider label="Volume" value="60" suffix="%" showValue></tc-slider> <tc-slider min="12" max="24" step="1" value="16" showTicks></tc-slider>
<tc-chart> · new in 1.7Docs →

Chart

Pure-SVG charts in five flavors — line, area, bar, sparkline, donut — themeable through CSS tokens and accessible by default. Every point and segment is a real DOM node a screen reader can reach. ~11 KB minified, no canvas.

Revenue this quarter +216%
import "@ra9/tan-compose-kit/chart"; <tc-chart type="line" data='{ "labels": ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug"], "series": [ {"name":"Sessions","values":[420,460,510,530,640,720,790,830]}, {"name":"Signups","values":[80,95,110,130,140,180,210,240]} ] }' ></tc-chart> <tc-chart type="bar" data='{ ... }'></tc-chart> <tc-chart type="donut" showValues data='{ "series": [...] }'></tc-chart> <tc-chart type="sparkline" height="32px" showLegend="false" data='{ ... }'></tc-chart>
<tc-block-login> · <tc-block-signup> · <tc-block-dashboard> · <tc-block-settings> · <tc-block-pricing> Live demo →

Page templates (blocks)

Complete page templates composed from the kit primitives — a login page, signup, an admin shell, a settings page, and pricing. Each one is a single element that emits an event on interaction, and each has its own docs page.

Emits tc-block-login-submit with the typed credentials.

Docs →

Emits tc-block-signup-submit with name, email, password, and consent.

Docs →

Emits tc-block-dashboard-nav. Content is slotted into the main region.

Docs →

Emits tc-block-settings-change. Each section is a named slot.

Docs →

Emits tc-block-pricing-select. Tiers come from a single JSON prop.

Docs →