← Tools

Shortwind

Tags

TailwindLLMTokensCLI

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.

html
<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:

MetricShortwindExpandedSavings
Class-string tokens12565881.0%
Whole-file LLM tokens1,2553,00158.2%

Per-file savings range from 46.6% on layout-heavy markup to 77.9% on recipe-dense components.

#Installation

npx @shortwind/cli@beta init

init 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

  1. The model writes shorthand@-prefixed recipes inside class="..."
  2. The expander resolves them — each @name expands to its definition, nested references resolve transitively, shortwind lint rejects cycles
  3. 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.js walks the DOM before first paint. For HTML artifacts an LLM emits straight to a .html file.

#Recipe format

Recipes live in .css files, which buys comments, multi-line definitions, universal syntax highlighting, and free Tailwind IntelliSense hover via classRegex.

css
/* 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 kindAllowed values
Size suffixxs, sm, md, lg, xl
Intent suffixprimary, secondary, ghost, danger, warning, success, info
OrderIntent 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.

FamilyExamples
cardcard, card-elevated, card-flat, card-interactive, card-header, card-body
buttonbtn-primary, btn-secondary, btn-ghost, btn-destructive, btn-icon, btn-lg
badgebadge, badge-success, badge-warning, badge-danger, badge-outline
layoutstack-xsstack-lg, row, row-between, grid-2, grid-3, center, full
textheading-xlheading-sm, body, muted, label, caption, link
forminput, textarea, select, checkbox, field, field-error, fieldset
surfacesurface, surface-muted, wrapper, wrapper-tight, divider-h, divider-v
feedbackalert, alert-success, alert-danger, callout, toast, banner
navigationnav, nav-link, nav-link-active, breadcrumb, tab, tab-active
tabletable, th, td, tr-hover, table-zebra
mediaavatar, avatar-sm, thumb, aspect-square, aspect-video
dialogdialog, dialog-overlay, dialog-content, dialog-header, dialog-footer
skeletonskeleton, skeleton-text, skeleton-circle

Plus list, code, empty, progress, tooltip, and icon.

#CLI

bash
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

PresetFamiliesCatalog size
startercard, button, layout, text, form~25 recipes
app (default)starter + badge, table, dialog, list, navigation, feedback, tooltip~65 recipes
contentstarter + badge, code, list, media, empty~50 recipes
allevery 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

RuleTriggerSeverity
recipe/unknown@name with no matching recipeError
recipe/cycleRecipe references itself transitivelyError
recipe/no-sibling-overlapTwo recipes from one family on one elementWarning
recipe/conflicting-intentConflicting intent suffixesWarning
recipe/bad-suffix-order@btn-lg-primary, size before intentWarning, auto-fixable
recipe/dynamic-classLikely-computed name, e.g. @${variant}Warning
recipe/no-redundant-utilityRaw utility already present in the recipeInfo

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:

css
/* 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

PackageRoleInstall it?
@shortwind/cliAll commandsTooling, or run it with npx
@shortwind/vite / next / astroBundler plugin, transforms before Tailwind's scanPick one; init adds it
@shortwind/tailwindDetects v3 vs v4, registers via the right APIinit adds it
@shortwind/coreParser and resolver; no Tailwind dependencyNo, transitive
@shortwind/runtimeBrowser/CDN expander, ~6KB with catalog inlineOnly 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.

View on GitHub·raymond-UI/shortwind
Actions
Open