technique
Your brand comes with you
npx cia migrate reads your Tailwind config or Bootstrap variables and writes a cia theme.scss, with a confidence rating on every token it maps.
The most common reason a team doesn't try a new styling system isn't the API. It's this:
"We already have a design system. We spent eight months on those colors. We're not rewriting it."
That's the right instinct. Your tailwind.config.js or your _variables.scss isn't config — it's a record of arguments you already had and settled. Brand review, contrast fixes, the four rounds on the blue. Any system that asks you to start from a blank theme file is asking you to relitigate all of it.
So we wrote the converters. npx cia migrate tailwind and npx cia migrate bootstrap read the file you already have and write a cia theme out the other end.
What it actually looks like
npx cia migrate tailwind ./tailwind.config.js --name acme
No path? It walks up from the current directory looking for tailwind.config.ts, .mjs, .cjs, then .js, in that order. Diagnostics go to stderr, so --json output stays pipe-clean:
npx cia migrate tailwind --json | jq '.cia.report'
Flags are the same on both tools: --name (default migrated), --out (default ./cia-themes/<name>.scss), --json, --help. That's the whole surface — see bin/cia.cjs.
Here's a real run against a config with a primary palette, a couple of status palettes, five spacing steps, four font sizes, two families, five radii, and one house color called cocoa:
─── confidence report ─────────────────────────
HIGH (exact match): 15
MEDIUM (close, ≤0.125rem): 2
LOW (best guess): 1
UNMAPPED (no cia analog): 2
And the file it wrote:
// acme theme — generated by `cia migrate tailwind`
// Mapping confidence — HIGH: 15 · MEDIUM: 2 · LOW: 1 · UNMAPPED: 2
@use 'css-is-awesome/scss/mixins' as m;
@include m.theme('acme') {
/* ─── Colors ─── */
--error-default: #dc2626; /* MEDIUM */
--success-default: #16a34a; /* MEDIUM */
--action-primary-default: #2563eb;
/* ─── Spacing ─── */
--space-4: 1rem;
--space-7: 3rem; /* LOW */
/* ─── Font families ─── */
--font-sans: Inter, system-ui, sans-serif;
/* ─── Derived hover + active states ─── */
@include m.states(action-primary);
@include m.states(error);
@include m.states(success);
}
Those m.states() calls are the part that saves the most hand-work. You gave the converter one hex per group; cia derives hover and active from it with color-mix(in oklch), the same way the shipped themes do.
The confidence report is the actual feature
Any converter can produce a file. The question is whether you can trust it, and the honest answer is: not uniformly. Some mappings are certain and some are a guess, and the useful thing is to say which is which.
So every token carries a rating, and anything below HIGH is tagged inline in the output. The rules are plain arithmetic (bin/migrate-tailwind.cjs, commit 18764ac):
- HIGH — exact. A rem value that lands dead on a cia slot, or a palette literally named
primary/brand/accent. - MEDIUM — within 0.125rem (2px), or a status color inferred from a palette name.
redprobably means error. Probably. - LOW — within 0.25rem (4px), or: no palette named primary was found, so the first non-status, non-gray palette got picked. Review this one.
- UNMAPPED — no cia analog. Appended as a comment block with a suggestion, never silently dropped.
Look at what got flagged in that run. --space-7: 3rem /* LOW */ came from a Tailwind 13 step at 3.25rem — a 0.25rem gap, so you get told. cocoa went UNMAPPED with a note to add it as a brand-* token. And spacing.1 at 0.25rem went UNMAPPED for a different reason: cia's space scale starts at 0.5rem, and spacing.2 had already claimed that slot exactly. Two of your steps wanted one of ours. The report says so rather than picking a winner quietly.
That's the whole design principle: a wrong value with a warning costs you five minutes. A wrong value with no warning costs you a bug report from design three weeks later.
One caveat that follows from the same honesty: proximity matches write cia's value, not yours. If your radius was 0.3rem, the output says 0.25rem. Direct semantic mappings keep your literal value. Read the MEDIUM and LOW lines before you ship.
Bootstrap, and the thing it can't resolve
The Bootstrap path (commit 50d4042) parses $var: value !default; declarations directly — no Sass compiler, just a line parser with paren-depth tracking for multi-line maps. It leans on Bootstrap's naming convention, which is stable enough to map with confidence: $primary → action-primary-default, $danger → error-default, $body-bg → background-default, $border-radius → radius-md, $spacer → space-4.
Against a stock-shaped Bootstrap 5.3 variables file: HIGH 13, UNMAPPED 8. And in the unmapped block, this:
* $body-bg !default = $white
* '$body-bg' references another Sass variable or expression we can't
* evaluate without compiling. Resolve manually (e.g. paste the literal
* hex/rem value) then re-run.
That's a real limit, stated plainly rather than guessed around. Bootstrap variables reference each other constantly (Sass customization docs), and resolving $body-bg: $white properly means running Sass. The converter doesn't, so it tells you which values it couldn't follow.
What this does not do
It maps tokens. Color, spacing, type scale, radii, families. That's it.
It does not convert components, behavior, or markup. Nothing here turns class="flex items-center gap-2 rounded-lg" into a mixin call, and no flag ports an app. Component-level migration is what the recipes and the Tailwind / Bootstrap migration guides are for — that part is human work, and the epic (roadmap/epics/v1-0/EPIC-03-migration-on-ramp.md) puts class migration out of scope permanently.
One more gap worth naming: the Tailwind converter reads a JavaScript or TypeScript config file. Tailwind 4 moved to CSS-first configuration with @theme (theme docs) — a JS config still loads via the @config directive, and if you have one, this works. If your theme lives only in CSS, the converter has nothing to open yet. That's on the list, not in the box.
Tailwind and Bootstrap are good tools. Plenty of teams should stay on them. This isn't an argument that you're on the wrong thing — it's an attempt to make trying a different thing cost an afternoon instead of a quarter.
Run it, read the flags, keep your blue.