One token contract, themeable atoms & molecules. Current theme: Dark. Try the pickers top-right — every section below re-themes live and scales with the text control. Showcase Plum is not built in: this page calls theme.register() and ships its own [data-theme] block.

Foundations

Typography

Heading level 1

Heading level 2

Heading level 3

Heading level 4


Body — the default reading text. Label — form labels & field captions. Caption — small meta text. Code — monospace inline.
default muted faint accent success warn danger
normal medium semibold bold
numeric 1234.50% default 1234.50%
eyebrow italic nowrap text uppercase leading none scale=false 13px
Font-scale slider vs a pinned run — only the first line follows the picker:
size="lg" scales with --fs-scale size="lg" scale=false stays pinned
measure="40ch" — a paragraph capped at forty characters wide so long lines stay readable.

Truncated heading that runs past its container

Theme tokens

The picker on the right passes auto: it adds a row that follows the system light/dark setting, remembering one light and one dark palette (dashed swatches).
--bg
--bg-elevated
--bg-elevated-2
--surface
--border
--border-strong
--text
--text-muted
--text-faint
--accent
--link
--ok
--warn
--danger
--info

Text scaling opt-in

Optional in-app text-size control (drives --fs-scale, text tokens only). Most apps should rely on browser zoom and the user's OS/browser font-size instead — the kit is rem-based and never resets the root size, so both are respected out of the box. Reach for this only in reading-dense apps (chat, docs) that want to grow body text while keeping chrome compact.

Style hooks

Your scoped CSS cannot reach inside a kit component, and its internal class names are private. Each component instead publishes CSS custom properties named --<prefix>-<axis> (bg, fg, border, size, radius) that you set from style or any ancestor rule. Reaching in with :global() is unsupported. The README lists every published property.
--badge-bg --txt-fg
Badge size axis — numeric reserves and centres the box, --badge-min-size squares it, --badge-fs / --badge-fw set the typography:
7 7 42
A pill Popover trigger sitting in a row of Toggles — pill + size="sm" + the --pop-trigger-* hooks, no :global():

Layout

Layout: Stack · Cluster · AutoGrid

Cluster — wraps, never overflows:
alpha beta gamma delta epsilon zeta
Cluster stackAt="md" — buttons stack full-width when the cluster is under 40rem:
AutoGrid — columns adapt to available width (resize the window):
Cell 1
Cell 2
Cell 3
Cell 4
Cell 5
Cell 6
AutoGrid — capped column width (max), left-packed instead of stretching:
Fixed 1
Fixed 2
Fixed 3

Card

Static card

An elevated surface with token-driven padding, border and radius.

Stacked card

Looks like a pile — two layers peek out bottom-right.

Stacked (info)

Back layers tinted with the info/blue hue.
tone info
tone ok
tone warn
tone danger
tone attention

Dashed card

An empty-slot / drop-target frame.

Dashed (danger)

Border style and tone compose.

Borderless card

Frame dropped; the surface fill carries the shape.

Dashed stack

The back layers pick up the dashed frame too.

Interactive card

Enter/Space activate; the nested button stays independent.

maxWidth 22rem

Fills its row up to the cap.

Divider

Horizontal tones — default, strong, faint: Above the rule. Between two rules. Below the strong rule. Faint, tighter spacing. Vertical — stretches to the row's cross size:

MasterDetail

Two columns above its own 48rem breakpoint; below it one pane at a time with a sticky back header. Drive selected from the URL to make each pane a route.
Alice

Alice

Detail pane for Alice.
Same component boxed at 22rem — the mobile regime on desktop:
Nothing selected
Pick someone on the left.
gap spaces the columns and divider="none" drops the seam — or override --md-gap / --md-divider from the consumer:

Bob

Detail pane for Bob.

Actions

Button

Polymorphic roots — as="a" / as="button" keep the same chrome and focus ring: Hit area — hitArea="compact" drops the 44px touch slab (outlined box shows the real hit box on touch):
Corner count — count hides at 0, caps at countMax (99+), and the exact number joins the accessible name:
Hotbar slots — layout="stacked" puts glyph over the label; shortcut is a corner hint and aria-keyshortcuts:
Variant × tone matrix (primary keeps a readable on-accent label):
npm i @dorsk/tsumikit

Icon & IconButton

filled forwards to Icon, so a two-state toggle can paint its "on" state with the filled glyph — and quote/reply head a quote-reply action:
Shared box scale (xs/sm/md/lg) across IconButton, SelectButton, Popover and Button square — 44px hit slab on touch:

All registry glyphs (sized at 1em — they scale with text):
back
arrow-right
arrow-up
arrow-down
chevron-up
chevron-down
chevron-left
chevron-right
menu
more
external
log-out
plus
minus
check
x
search
filter
copy
edit
trash
save
download
upload
send
share
reply
quote
retry
recycle
settings
play
pause
stop
music
tv
film
disc
file
file-text
folder
archive
image
markdown
list
grid
link
unlink
text-cursor
tag
bookmark
star
heart
fork
live
eye
eye-off
lock
unlock
bell
mail
calendar
clock
home
user
users
sun
moon
flame
zap
loader
info
warning
help
check-circle
x-circle
alert-circle
Custom (open) icon via children snippet:
Spinner (halts under prefers-reduced-motion):

Toggle · OptionButton · SelectButton



Choice cards with description and onfocuschange driving a preview pane (hover or arrow through them):
The agent writes a step-by-step plan and waits for approval.

SelectButton (native select over a glyph button): selected: a

SegmentedControl

selected: all
Review
view: grid
scope: album

SplitButton

A primary action fused with a caret menu: one outer radius, a single divider, two tab stops. ArrowDown on the primary opens the menu.
Pick a recipient first

Forms

Form atoms & Field

Helper text below the control.
This field is required.
Press Control Enter to submit.
Control K Shift Enter Control Alt Escape Up arrow
On
40%
50

Checkbox & RadioGroup

Preview: The agent writes a step-by-step plan and waits for approval.

Fieldset

Drag a card between pools (HTML5 DnD); buttons are the keyboard path. dragPayload feeds accepts while hovering, so the source pool stays unhighlighted.
Pool A
Ledger
Wallet
Pool B
Exchange

FileButton & Dropzone

native picker, keyboard-focusable
Drop files here or click to browse
overlay mode — wraps content, drop UI shows only while dragging a file over it

EmojiPicker

Search the built-in EN+FR catalogue or browse it by group; pass your own groups to replace it. Arrow keys walk the grid, Enter picks.

Combobox

Autocomplete: you render the field, the kit floats the listbox under it, keeps aria-activedescendant on the input and consumes Arrow / Enter / Tab / Escape.
Mention: the consumer detects the trigger with findTrigger and replaces the token with applyTrigger; anchor="caret" pins the listbox under the text cursor. Type @ to try it.

InputGroup

Input + trailing Button: the field spans the whole group and draws the one border and ring; the controls fuse with its edge, so a phone never zooms to a narrow field.
Textarea (autoresize, top grip) + leading FileButton + trailing SplitButton (send + schedule menu); past one row the adornments drop into a bar under the text, so every line runs full width and the scrollbar stays clear of them:
A trailing label whose width changes re-pads the field (click it):
Sizes (sm · md · lg), disabled and error:

AttachmentList

Chips when the list is wider than 30rem; square tiles below that (resize the window or the box). A tile's Popover carries the full name, the size and Remove:
  • screenshot-2026-09-25-at-14.02.11.png 1.2 MB
  • report.pdf 86.1 KB
  • notes.md 2.0 KB
  • export.csv 392.7 KB
Forced tiles; image attachments show their thumbnail:
numbered badges each position; onopen makes the chip name a button and adds a preview + Open to the tile popover:
  • 1
  • 2
  • 3
  • 4

Navigation

Tabs

Overview panel — arrow keys move between tabs.
Strip-only mode (no panel): the ✕, Delete and middle-click close a tab; the pinned tab opts out with closable: false. Active: doc-1.

Pagination

Page mode — bind:page + pageCount: Offset mode, small, with range — bind:offset + limit + total: Compact collapse under 24rem of container width:

TabBar

Phone bottom tabs. Fixed to the bottom by default with the safe-area inset and --tabbar-h on <html>; shown inline here.
selected: home

Accordion

A stack of Disclosures: a native button with aria-expanded over a region panel.
variant="plain" drops the outer border and radius and keeps the dividers; size="compact" tightens every header and panel to --fs-xs. One summary snippet heads every item, because it receives the item alongside the live open:
With multiple=false opening one item closes the others.
Standalone Disclosure with a header snippet, bindable open and a leading chevron:

Avatar

Listed user role="img"

GlyphStack

Pin · status · machine · account. Inline they cost four slots; stacked they share one 2×2 grid of 1rem cells. DOM order is unchanged, so Tab still reaches the pin first.
devbox never always
Folded cells are 1rem. expand="tap" opens them as big tiles on click; expand="slide" opens on press — drag toward a corner and release to fire that tile.
tap
slide
auto measures the nearest .cq container and folds below stackBelow (20rem here) — drag the handle.
devbox Conversation title that needs the width on a phone 2 min ago

Data display

Badge · Link · Dot

neutral ok warn danger info success sm 12
~/path/to/file removable typescript trash glyph removed API_KEY
uppercase stable
soft ok warn danger 3
accent muted violet color dot icon text · 12:34
xs 8 128 a very long account name that clips
Anchor link View all →
active stale dead hibernated custom ring

Swatch

Sizes and shapes; a label makes it an image, without one it is decorative.
A
Interactive palette: buttons with aria-pressed.
var(--accent)

Metric · StatTile

Tracks
1284
Size
42.7 GB
Quality
FLAC
lossless · 16-bit
Added
37 this week
+12% vs last week

Surface variants — tiles (via Metric) and panels (via Card) opt into the same theme-aware shade by prop, no :global override:
Base
base
Raised
raised
Sunken
sunken
surface base
surface raised
surface sunken

SectionHeader · KeyValue · LoadMore

Recent sessions

12 last 24h

4
Group rows render here while open.

Failed

2
live

sakura

7

Host

sakura
Running
3
Queued
0
Image
cctui-worker:0.7.3
pulled 2h ago
Load
1.42
Tokens
128000
Cost
$0.42
Failed to load
No more items

DataTable generic <T>

NameRoleStatus
api-gatewayservice ok
worker-02worker warn
cache-01cache danger
layout="fixed" + truncate, hideBelow on the role column, rowTone accent bar, hover-revealed rowActions, size="sm".
RoleStatusActions
api-gatewayservice ok
worker-02worker warn
cache-01cache danger
responsive="stack" in a 22rem-wide box: rows become cards via Column.role (title / detail / meta / hidden), actions stay on the title line.
NameRoleStatusIdActions
api-gatewayservice ok 1
worker-02worker warn 2
cache-01cache danger 3
stackLabel: false on the meta columns: no labels, the metas join on one · line under the title.
NameRoleStatusId
api-gatewayserviceok#1
worker-02workerwarn#2
cache-01cachedanger#3
Empty meta cells are hidden and skipped by the · separators; stackActions="end" closes every card with its actions.
NameRoleStatusIdActions
api-gateway-with-a-long-unbroken-nameok#1
worker-02workerwarn#2
headerSnippets replaces a header cell — here a select-all checkbox over a checkbox column.
NameRole
api-gatewayservice
worker-02worker
cache-01cache
Same table with stackBelow="30rem": it stays tabular in this 34rem box and stacks once the box drops under 30rem.
NameRoleStatusId
api-gatewayservice ok 1
worker-02worker warn 2
cache-01cache danger 3

SortableList

Drag the grip (long-press anywhere on a row with handle="row"), use the arrows, or focus a grip and press Space, then ↑/↓/Home/End and Space again. Alt+↑/↓ moves a row in one step. The first row is locked and canMove keeps it on top.
Selected: r7 · last move: none

SwipeRow

Swipe a row sideways to reveal an action; past 35% of the width it fires on release. Vertical drags scroll as usual. Tab into a row to reach the same actions as buttons.
  • Behemoth Odin — 2 min ago
  • Fafnir Hyperion — 9 min ago
  • Ixion Zalera — 14 min ago

PullToRefresh

Touch or pen: pull down from the top of the list to prepend a row.
Row 1
Row 2
Row 3
Row 4
Row 5
Row 6
Row 7
Row 8
Row 9
Row 10
Row 11
Row 12
Row 13
Row 14
Row 15
Row 16
Row 17
Row 18
Row 19
Row 20

SeekSlider · VolumeSlider · TimeDisplay

oninput previews while dragging; onchange fires once per release or key step, so the player seeks only there. The bottom slider is a live stream (no duration).
seeks: 0

Timestamp subdued · click for UTC / epoch / zone

Renders an instant in one of six modes (date, time, datetime, relative, iso, short-iso), subdued by default. The date/time/datetime modes follow the viewer's zone unless you pass utc. Click it for a read-only popover with the same instant as UTC, relative, your time zone and the unix epoch. Opt into selectable to let viewers switch the inline mode.
datetime: date: date (utc): iso: short-iso: time: relative: mono: selectable: no popover:

GitRef branch · PR · ±diff

Branch chip, pull-request link and +/− diff stats in one inline row; every part is optional. The PR state picks its tone (open ok, merged accent, closed danger, draft muted). collapse="auto" keeps only the glyphs when the nearest .cq container is narrower than 18rem; glyph forces it.
branch: feat/wave-1-new-components-with-a-long-name pr: DorskFR/tsumikit#77 all three: fix/resizable-panel #78 +128 −42 glyph: fix/resizable-panel DorskFR/tsumikit#79 +3 −1

WorkingDir fish-style · degrades with width

Drag the handle: ancestors abbreviate one at a time left to right, then only the leaf stays, then the leaf ellipsises down to minLeaf chars. The full path is always in the tooltip.
/home/dorsk/Documents/tsumikit

Truncate char-count · hover reveal

Character-count truncation (the counterpart to <Text truncate>'s CSS ellipsis). Hover or focus a truncated value to reveal the full text.
middle: 0x71C765…6d8976F end: a-very-long-resourc… start: …/14/request-trace.log fits (no tooltip): short

CodeBlock BYO highlighter

typescript
// install and use
import { Button, theme } from '@dorsk/tsumikit';

function greet(name) {
  return `Hello, ${name}!`;
}
Plain (no highlighter, no line numbers):
setup.sh
git clone …
npm install
npm run dev

ConversationPanel · ConversationFeed · ChatBubble · StatusBar

A full chat column: header, a feed that sticks to the bottom (scroll up, then append → "Jump to latest"), a "Load older" pill that keeps the viewport still, a live status row and a Composer with a toolbar row.
Session · tsumikit live
user

Can you summarise the release notes for 0.62?

assistant

Sure. Highlights:

  • renderMarkdown helper, zero deps
  • ConversationFeed + ConversationPanel organisms
  • Composer toolbar row
AreaChange
ChatBubbletimestamp, footer, markdown
StatusBarnew
1.3s↓ 233 ↑ 42
tool

Read src/lib/index.ts (220 lines)

assistant

Links are scheme-checked: docs works, [x](javascript:alert(1)) stays text.

const html = renderMarkdown(text);
1.3s↓ 160 ↑ 42
system

Context compacted

Listening 5 msgs
Ctrl/⌘+Enter to send
StatusBar tones and a standalone ChatBubble with a footer and a snippet body:
Idle
Running tests… 12s
Connected
Reconnecting
Disconnected
assistant sending…

Snippet body via Prose + renderMarkdown: bold, em, gone

0.4sgpt · 128 tokens

Feedback

EmptyState

No results

No albums match your filters. Try broadening the search or clearing filters.

Your library is empty

Add an artist or album to start building your collection.

Nothing here yet

This section hasn't been set up.

Loading sessions…

No results

Try a different query.

Skeleton:

Callout

Auto-search runs in the background and links matches as it goes.

Linked

12 releases matched and were linked to this collection.

Some matches need review

3 releases had several candidates and were left unlinked.
Searching MusicBrainz for 48 releases…

Narrow container

Wraps its body and actions instead of overflowing.

Toasts

Progress

  • conforming 412
  • nonconforming 12
  • blocked 0
  • done 300
  • skipped 120

Gauge

🍃

CapBar

Anthropic
62% · resets 3h
▲ using 62% · cap 80%
OpenAI
81% · resets 12h
Gemini
44% · uncapped
Narrow
58% · auto-stacks
Locked
95% · over cap

Overlays

Popover & Menu native top layer

Open a menu and navigate with ↑/↓, Enter to select. The mixed menu's checkable rows keep their icon (check trails) or use the leading check slot, and keepOpen leaves the menu open while toggling. The last trigger carries a count badge, a panelStyle that caps and scrolls the panel, and a data-journey rest prop forwarded to the trigger element. The Sessions menu mixes plain items with control rows: a ghost native <select> and a nested Popover, laid out as rows and reached with ↑/↓.

RadialMenu

Tap the FAB, or press it and slide onto a petal and release. From the keyboard, Enter opens it on the first petal, ←/→ walk the arc, Escape closes. The fan clamps into the viewport, so the corner FAB keeps its petals on screen.
HUD Map
Camera Logs

Tooltip

ConfirmModal

Drawer

Side panel on the native <dialog>: Escape, scrim click and the close button close it; focus is trapped; body scroll is locked. A nav snippet becomes a 150px page column beside the content on wide viewports and a horizontal strip above it under 48rem, where the panel goes full-screen.
Edit src/lib/styles/variables.css to retheme everything · add a [data-theme] block for a new theme · components never hard-code a color or pixel.
Delete library?
This removes the library and all of its items. This cannot be undone.
Provider General
2 changes