@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
<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
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.
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.
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.
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.
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
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.

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.
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" }
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.
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.

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"

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
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
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.
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
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 ▾
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>