← back to gallery

tailwind-exit

Paste Tailwind soup, get semantic CSS — BEM, Modules, or vanilla scoped.

dev-toolstailwindcssrefactorbemcss-modulesjsxhtmlanthropic
Open product ↗

tailwind-exit

Paste a Tailwind-soup component, get back semantic CSS with extracted repeated patterns.

For frontend devs who read Julia Evans' "Moving away from Tailwind" and decided today is the day. Paste HTML or JSX with Tailwind classes, pick a target style (BEM / CSS Modules / vanilla scoped), get back two panes: refactored markup with semantic class names + an accompanying stylesheet. Repeated full class-strings (≥2 elements) become clusters that Claude names semantically (card-hero, button-primary, nav-item) — values are resolved from Tailwind's real source, never LLM-guessed.

How it works

  1. Parse. HTML → parse5. JSX → @babel/parser. AST is walked and every element's class / className is tokenized.
  2. Cluster. Full class-strings that appear on ≥2 elements are grouped into clusters. Singletons pass through unchanged.
  3. Resolve. Each Tailwind token is looked up in a SQLite table mirrored from tailwindlabs/tailwindcss source (theme.css + utilities.ts). Arbitrary values (p-[12px], bg-[#fff]) are parsed inline.
  4. Name. Clusters are sent to Claude with their structural context (tag, parent, children, text sample) — Claude returns kebab-case names. JSON-mode + one retry if parse fails. No fallback to hash names.
  5. Emit. CSS is rendered in the chosen style flavor; markup is rewritten with the new class names. Modules emits a styles.module.css import.
  6. Persist + permalink. Each conversion gets a 12-char slug, stored verbatim. /share/:slug renders a server-side OG-tagged page so Twitter / Bluesky unfurls show the line-saved badge.

Real data sources

| Source | URL | Refresh |
|---|---|---|
| Tailwind theme | https://raw.githubusercontent.com/tailwindlabs/tailwindcss/main/packages/tailwindcss/theme.css | Weekly cron (Mon 07:00 UTC) + cold-start sync if table is empty |
| Tailwind utilities source | https://raw.githubusercontent.com/tailwindlabs/tailwindcss/main/packages/tailwindcss/src/utilities.ts | Same cron |
| Tailwind preflight | https://raw.githubusercontent.com/tailwindlabs/tailwindcss/main/packages/tailwindcss/preflight.css | Same cron |
| Anthropic Claude (via OpenRouter) | https://openrouter.ai/api/v1/chat/completions | Per POST /api/convert |

On Tailwind sync failure: last-good rows in tailwind_classes are kept and lastSyncLog.error surfaces on /api/stats. Conversions still work against the previous snapshot.

On Claude failure: HTTP 502 {error: "naming_service_unavailable", retry_after: 30}. There is no hash-name fallback — semantic naming is the differentiator.

API

All endpoints mounted under /tailwind-exit.

| Method | Path | Description |
|---|---|---|
| GET | /health | {ok:true,service,version,at}. Used by uptime checks. |
| POST | /api/convert | Body: {input, style, inputType?, model?}. Returns refactored markup + CSS + cluster names + lines-saved math + share slug. stylebem, modules, vanilla. inputTypeauto, html, jsx. modeldefault (sonnet), fast (haiku). 429 on rate-limit; 400 on parse failure; 502 if Claude is unreachable. |
| GET | /api/share/:slug | Returns the stored conversion JSON. |
| GET | /share/:slug | Server-rendered HTML shell with OG meta tags filled from the row; client JS hydrates. |
| GET | /api/stats | {totalConversions, totalLinesSaved, topClusterNames, lastTailwindSync, classCount, lastSyncLog}. |
| GET | /api/classes/:name | Resolves a single Tailwind class to its CSS declarations (handles variants like hover:bg-blue-500 and arbitrary values like p-[12px]). 404 if unknown. |
| GET | /api/classes?q=&limit= | Search synced class names (debug). |

Rate limit

5 conversions per IP per UTC day. IP is SHA-256 hashed with a per-day salt before storage (no plaintext IPs persisted). No accounts. Limit resets at midnight UTC. 6th request returns 429 with count, limit, resetEpoch.

Local development

cp .env.example .env
# Add a real OPENROUTER_API_KEY (sk-or-v1-…) to .env or export it.

npm install
npm start # → http://localhost:4800/tailwind-exit/
```

On first start, if tailwind_classes is empty the server runs a cold-start sync from the Tailwind GitHub repo (~5 seconds, ~4300 classes). After that the weekly cron job keeps it fresh.

Smoke test:

curl localhost:4800/tailwind-exit/health
curl localhost:4800/tailwind-exit/api/classes/p-4
curl -s -X POST localhost:4800/tailwind-exit/api/convert \
  -H 'content-type: application/json' \
  -d '{"style":"bem","input":"<div class=\"rounded-lg border p-4 shadow\"><h2 class=\"text-lg font-semibold mb-2\">A</h2></div><div class=\"rounded-lg border p-4 shadow\"><h2 class=\"text-lg font-semibold mb-2\">B</h2></div>"}' | jq

Stack

Out of scope

License

MIT