Shortwind
A token-efficient class layer for LLM-generated HTML. Collapses Tailwind utility chains into 1–3 token recipes, cutting class-string tokens by 50–80% with zero runtime cost.
LLMs spend 35–50% of their HTML output tokens on Tailwind class strings. Shortwind collapses common class clusters into @-prefixed recipes a model memorizes on the first turn, then expands them back to plain Tailwind before the browser sees anything.
<article class="@card-elevated @stack-md">
<header class="@row-between">
<h3 class="@heading-md">Quarterly review</h3>
<span class="@badge-success">On track</span>
</header>
<p class="@muted">3 of 5 milestones complete.</p>
<div class="@row">
<button class="@btn-primary">Open</button>
<button class="@btn-ghost">Skip</button>
</div>
</article>Same rendered output, roughly half the tokens.
#Measured impact
Benchmarked on representative components with the cl100k_base tokenizer:
| Metric | Shortwind | Expanded | Savings |
|---|---|---|---|
| Class-string tokens | 125 | 658 | 81.0% |
| Whole-file LLM tokens | 1,255 | 3,001 | 58.2% |
Per-file savings range from 46.6% on layout-heavy markup to 77.9% on recipe-dense components.
#Installation
npx @shortwind/cli@beta initinit detects your bundler (Vite, Next, Astro) and installs the matching adapter, copies the recipe catalog into ./recipes/ under your ownership, generates skills/shortwind/SKILL.md for agent discovery, and installs a pre-commit hook to keep that skill file in sync.
You never install all @shortwind/* packages, only one adapter plus the Tailwind integration.
#How it works
- The model writes shorthand —
@-prefixed recipes insideclass="..." - The expander resolves them — each
@nameexpands to its definition, nested references resolve transitively,shortwind lintrejects cycles - Tailwind sees only utilities — no runtime cost, no semantic change; purge, JIT, and dev servers behave normally
Two expansion paths share the same @shortwind/core parser and produce byte-identical output:
- Build-time — a Vite/Next/Astro plugin transforms source before Tailwind's content scan. Production ships zero
@tokens and no runtime expander. - Runtime — a standalone ~6KB
shortwind.dev/expand.jswalks the DOM before first paint. For HTML artifacts an LLM emits straight to a.htmlfile.
#Recipe format
Recipes live in .css files, which buys comments, multi-line definitions, universal syntax highlighting, and free Tailwind IntelliSense hover via classRegex.
/* recipes/card.css */
/* shortwind: card@0.4.2 sha:b0a1c3 — DO NOT EDIT THIS LINE */
/* Default content card. */
@recipe card {
rounded-lg border border-zinc-200 bg-white p-4 shadow-sm
}
/* Raised card for primary content. Built on @card. */
@recipe card-elevated {
@card rounded-xl p-6 hover:shadow-md transition-shadow
}
/* Interactive card with full hover + focus states baked in. */
@recipe card-interactive {
@card hover:shadow-md hover:border-zinc-300
focus-visible:ring-2 focus-visible:ring-zinc-900
cursor-pointer transition-all
}Recipes bake in canonical hover/focus/active/dark states, so the model picks @card or @card-interactive and never writes hover:@card. One-off states get appended as raw Tailwind, and tailwind-merge resolves conflicts last-position-wins.
#Naming convention
Names follow @<family>[-<intent>][-<size>] so models generalize instead of memorizing every combination.
| Token kind | Allowed values |
|---|---|
| Size suffix | xs, sm, md, lg, xl |
| Intent suffix | primary, secondary, ghost, danger, warning, success, info |
| Order | Intent first, size last: @btn-primary-lg, never @btn-lg-primary |
Not every family uses both axes: @stack-md is size-only, @badge-success is intent-only, @btn-primary-lg is both.
There is deliberately no parameter syntax like @btn(primary, lg). Parameters introduce grammar, and grammar is a new way for models to be wrong. Flat names with predictable suffixes give regularity at zero parsing cost.
#Default catalog
~19 families and ~150–200 recipes, shipped broad so models rarely need to reach for raw Tailwind.
| Family | Examples |
|---|---|
card | card, card-elevated, card-flat, card-interactive, card-header, card-body |
button | btn-primary, btn-secondary, btn-ghost, btn-destructive, btn-icon, btn-lg |
badge | badge, badge-success, badge-warning, badge-danger, badge-outline |
layout | stack-xs…stack-lg, row, row-between, grid-2, grid-3, center, full |
text | heading-xl…heading-sm, body, muted, label, caption, link |
form | input, textarea, select, checkbox, field, field-error, fieldset |
surface | surface, surface-muted, wrapper, wrapper-tight, divider-h, divider-v |
feedback | alert, alert-success, alert-danger, callout, toast, banner |
navigation | nav, nav-link, nav-link-active, breadcrumb, tab, tab-active |
table | table, th, td, tr-hover, table-zebra |
media | avatar, avatar-sm, thumb, aspect-square, aspect-video |
dialog | dialog, dialog-overlay, dialog-content, dialog-header, dialog-footer |
skeleton | skeleton, skeleton-text, skeleton-circle |
Plus list, code, empty, progress, tooltip, and icon.
#CLI
shortwind init # interactive setup
shortwind init --preset=app # starter | app | content | all | none
shortwind add card button badge layout # install families
shortwind add card --as marketing-card # rename on install to avoid collision
shortwind new marketing # scaffold a custom family
shortwind remove table # delete family + skills section
shortwind upgrade # walk families, show changelogs, prompt on touched files
shortwind upgrade --check # CI gate: non-zero exit on drift
shortwind dev # watch ./recipes/, regenerate SKILL.md on save
shortwind ls # list installed families + recipes
shortwind lint --fix # validate usage, naming, cycles, conflicts#Presets
| Preset | Families | Catalog size |
|---|---|---|
starter | card, button, layout, text, form | ~25 recipes |
app (default) | starter + badge, table, dialog, list, navigation, feedback, tooltip | ~65 recipes |
content | starter + badge, code, list, media, empty | ~50 recipes |
all | every family | ~100 recipes |
#Agent discovery
Shortwind generates skills/shortwind/SKILL.md in skills.sh format, a path every major harness (Claude Code, Cursor, Copilot, Aider, Continue) auto-discovers. That saves per-agent symlinks and file fan-out.
The file regenerates whenever ./recipes/ changes, through any of three sync surfaces: the shortwind dev watcher, the bundler plugin hooking your dev server's watcher, or the pre-commit hook that guarantees the committed copy is fresh.
Comments in your .css recipe files become the inline explanations in SKILL.md, so editing recipes/card.css updates what the model reads. The full skill file for the default catalog is ~150 lines, small enough for any system message.
#Lint rules
| Rule | Trigger | Severity |
|---|---|---|
recipe/unknown | @name with no matching recipe | Error |
recipe/cycle | Recipe references itself transitively | Error |
recipe/no-sibling-overlap | Two recipes from one family on one element | Warning |
recipe/conflicting-intent | Conflicting intent suffixes | Warning |
recipe/bad-suffix-order | @btn-lg-primary, size before intent | Warning, auto-fixable |
recipe/dynamic-class | Likely-computed name, e.g. @${variant} | Warning |
recipe/no-redundant-utility | Raw utility already present in the recipe | Info |
Unknown recipes pass through unchanged rather than throwing, so a failure is visible at view-source and the rest of the document is unaffected.
#Upgrades
Every recipe file carries a fingerprint header:
/* shortwind: card@0.4.2 sha:b0a1c3 — DO NOT EDIT THIS LINE */shortwind upgrade compares the recorded sha against current content. Pristine files apply cleanly with no prompt; files you've edited show a three-way diff and require confirmation. Your edits are never silently clobbered. recipes/.shortwind-lock.json records { family: version } and commits with your code, and shortwind upgrade --check is a read-only CI gate.
#Packages
| Package | Role | Install it? |
|---|---|---|
@shortwind/cli | All commands | Tooling, or run it with npx |
@shortwind/vite / next / astro | Bundler plugin, transforms before Tailwind's scan | Pick one; init adds it |
@shortwind/tailwind | Detects v3 vs v4, registers via the right API | init adds it |
@shortwind/core | Parser and resolver; no Tailwind dependency | No, transitive |
@shortwind/runtime | Browser/CDN expander, ~6KB with catalog inline | Only for the CDN path |
Tailwind v3 and v4 are both supported. The expander is version-agnostic; only the registration glue differs. On v4, .css recipe files sit naturally alongside @theme and @source.
#Registry
shortwind.dev hosts the catalog, docs, playground, registry, and CDN expander from a single Cloudflare Worker. Browse families visually, see expansions and example rendering, and copy the npx command to add one.
The registry origin is overridable. shortwind.config.json accepts registry: "https://corp-internal.example.com", so enterprise mirrors and forks work without code changes.