@birdycom/omniacss-migrate
Switch a Laravel, Vue or React project from Tailwind CSS to OmniaCSS and keep your class names.
flex items-center gap-4 p-4 md:p-8 bg-white dark:bg-gray-900 hover:bg-red-500/50 w-[37px] keeps working: the classes are resolved on demand and emitted as CSS that only uses OmniaCSS tokens (Omnia palette in OKLCH, rem scales, Omnia shadows and breakpoints).
Coming from Bootstrap 3, 4 or 5? See From Bootstrap.
This is the only package that names Tailwind and Bootstrap, because it is a migration tool.
Laravel in 3 steps
# 1. see what changes (dry run, nothing is written) and how much of your markup is covered
npx @birdycom/omniacss-migrate scan .
npx @birdycom/omniacss-migrate setup .
# 2. apply the switch: removes the Tailwind packages from package.json, swaps the Vite/PostCSS plugin, carries your theme into CSS
npx @birdycom/omniacss-migrate setup . --write
npm uninstall tailwindcss @tailwindcss/vite @tailwindcss/postcss && npm install -D @birdycom/omniacss-migrate
# 3. build as usual
npm run dev
Your Blade, Vue and React files stay exactly as they are. By hand it is two edits:
// vite.config.js
import omniaMigrate from "@birdycom/omniacss-migrate/vite"; // was: import tailwindcss from "@tailwindcss/vite"
export default defineConfig({ plugins: [laravel({ input: ["resources/css/app.css", "resources/js/app.js"] }), omniaMigrate()] });
/* resources/css/app.css: the entry, under our own name (setup --write renames @import "tailwindcss" to it): */
@import "@birdycom/omniacss-migrate";
/* the Tailwind forms keep working too: */
@import "tailwindcss";
@tailwind base; @tailwind components; @tailwind utilities;
Not using Vite? Use the PostCSS plugin instead:
// postcss.config.js
import omniaMigrate from "@birdycom/omniacss-migrate/postcss";
export default { plugins: [omniaMigrate({ content: ["resources/views/**/*.blade.php", "resources/js/**/*.{vue,tsx,jsx,js}"] })] };
What the plugin does
- Replaces
@import "@birdycom/omniacss-migrate"/@import "tailwindcss"/@tailwind base|components|utilitieswith Omnia core (tokens + the Omnia classes you use), a small base reset and the compat CSS for the classes it finds in your content. - Expands
@apply(with variants,!important, and your own classes),theme(colors.red.500),--spacing(4),@theme,@custom-variant,@variant,@source,@source inline("…"); ignores@pluginand@config. - Reads
tailwind.config.{js,cjs,mjs}(darkMode,theme.colors/extend.colors,fontFamily,borderRadius,spacing,boxShadow,screens,container,prefix,important) and your@themeblocks. - Output is wrapped in cascade layers
omnia, theme, base, components, utilities: Omnia's reset and tokens lowest; the Omnia classes (for example what the codemod wrote) and the Tailwind-style names together inutilities, sorted the Tailwind way (variants after the base class, shorthands before longhands, the class with more properties first). So an Omnia class beats every reset and your own@layer components, and wins or loses against a Tailwind-style class in the sameclassexactly as before the codemod.
Class coverage: layout, flex/grid, spacing, sizing, typography, colors (text, bg, border, ring, outline, fill, stroke, divide, accent, caret, decoration, gradients from-/via-/to-), borders, radius, shadow, ring, opacity, filters and backdrop filters, transitions, animations, transforms, display, position, z-index, overflow, cursor, scroll snap, tailwindcss-animate classes, arbitrary values (w-[37px], bg-(--brand), [mask-type:luminance]), opacity modifiers (bg-red-500/50), negatives (-mt-2), !important, and variants hover: focus: active: disabled: first: odd: before: after: placeholder: file: marker: selection:, sm: md: lg: xl: 2xl: max-md: min-[…]:, @md: container queries, dark:, group-*, peer-*, data-[…]:, aria-*:, has-[…]:, not-*, in-*, supports-[…]:, *:, arbitrary variants [&>svg]:, motion-safe:, print:, rtl:.
Colors
Tailwind palette names map to the nearest Omnia palette token (src/data/colors.json, generated by scripts/build-color-map.mjs with nearestOmnia()): bg-red-500 becomes var(--color-red-50). Arbitrary colors (bg-[#0041a7]) are mapped to the nearest token too (arbitraryColors: "keep" keeps the literal). Opacity modifiers use color-mix(in oklab, <token> 50%, transparent).
Colors from tailwind.config are mapped to Omnia tokens as well (--keep-brand-colors / keepBrandColors keeps them); values that are var(...)/hsl(var(...)) (shadcn style) stay as you wrote them.
Scales
Spacing, sizing, font sizes, radius, blur and containers use the familiar numbers (p-4 = 1rem). Shadows reuse Omnia's own shadow values. Breakpoints follow OmniaCSS (sm 36rem, md 50rem, lg 64rem; xl 80rem and 2xl 96rem exist for compatibility). Use breakpoints: "tailwind" for 40/48/64/80/96rem.
Names that exist in both systems (p-4, flex, shadow-md …) never collide: classes the compat layer resolves are removed from the Omnia core output.
CLI
omniacss-migrate scan <dir> coverage: % of class occurrences supported + the unsupported list
omniacss-migrate setup <project> [--write] plan (dry run) or apply: package.json, vite/postcss config, CSS @theme + dark mode
omniacss-migrate codemod <dir> [--write] rewrite to native OmniaCSS class names where the CSS is identical
omniacss-migrate build [-c glob] [-o file] write the CSS once (no Vite or PostCSS)
omniacss-migrate css "md:hover:bg-red-500/50" print the CSS for one class
omniacss migrate scan . does the same through the main CLI (npm i -D @birdycom/omniacss-migrate).
setup and codemod are dry runs unless you pass --write. Commit first: the files are changed in place.
Codemod (optional)
codemod rewrites class/className/cn()/cva() strings to native OmniaCSS names when an Omnia class has exactly the same declarations (for example items-center becomes align-items-center, p-4 becomes p-1). Everything else is left alone and listed. After --write the file omniacss-migrate.manifest.json tells the compat layer which names are now native, so p-1 (Omnia, 1rem) is never confused with the old p-1 (0.25rem). Run it over the whole project, not a single folder. Tests and scripts (tests/, scripts/, *.test.*, *.spec.* …) are skipped and listed, because they often assert class names as data; add --include-tests to rewrite them too.
From Bootstrap
npx @birdycom/omniacss-migrate bootstrap scan . # report only
npx @birdycom/omniacss-migrate bootstrap codemod . --write # apply (commit first)
npx omniacss build -o css/omnia.css # OmniaCSS for exactly the classes you use
Works on HTML, PHP, Blade, Twig, ERB, Vue and JSX templates; template code inside a class attribute (<?php … ?>, {{ … }}) is left untouched.
- Utilities become the OmniaCSS class with the same declarations, looked up in the installed OmniaCSS build:
mt-3(1rem) becomesmt-1,d-md-nonebecomesmd:d-none,justify-content-betweenbecomesjustify-between. Theme colors (text-primary,bg-dark,border-danger) map to the nearest Omnia token. - Components become OmniaUI classes:
btn btn-primary→oc-btn,alert alert-danger→oc-alert oc-alert--danger,form-control→oc-input(oc-selecton a<select>),card-body/panel-body→oc-card-content,table table-striped→oc-table oc-table--striped. - Grid classes (
container,row,col-*,offset-*,g-*,row-cols-*) stay, served by a generatedbootstrap-compat.csswith only the classes in use, at OmniaCSS breakpoints. Utilities without an OmniaCSS twin (border1px,z-1,mt-n1) keep their exact CSS in the same file. - JavaScript components (modal, dropdown, collapse, carousel, navbar, tabs, offcanvas, toast) and icon fonts are reported with the native replacement (
<dialog>+commandfor, popover,<details>, scroll snap). - The Bootstrap
<link>is replaced byomnia.css, the OmniaUI CSS andbootstrap-compat.cssin the same asset folder. - The report lists classes your JavaScript toggles (
classList.add('btn-primary')) and Bootstrap 3 classes in a newer site, split into "still styled by your CSS" and "safe to delete". --writerecords the classes it produced inomniacss-migrate.bootstrap.json, so running it again never reads the newmt-1(OmniaCSS, 1rem) as Bootstrap'smt-1(0.25rem).
Options: --bs 3|4|5 (default: detected from package.json, the <link>, a local bootstrap.css or the markup), --breakpoints exact (keep Bootstrap's pixel breakpoints as min-[48rem]:), --compat <file>, --omnia-href, --omniaui-href, --compat-href.
Options (PostCSS / Vite)
| Option | Default | Meaning |
|---|---|---|
content | detected (Laravel, Astro, Nuxt, Vite) | globs with class names; @source is honoured too |
core | true | true Omnia core on demand, "tokens" only the color tokens and base, false you load Omnia yourself |
preflight | true | include the base reset |
layers | true | cascade layers |
tailwind | detected | 3 or 4 naming (shadow-sm, rounded-sm, ring, blur-sm differ) |
darkMode | detected | "class" (.dark) or "media" |
breakpoints | "omnia" | or "tailwind" |
arbitraryColors | "nearest" | or "keep" |
safelist | [] | extra classes |
Known gaps
- Plugin classes other than
tailwindcss-animate(typographyprose, forms, Iconifyicon-[…], container-queries plugin utilities) are not generated. theme.extend.keyframes/animationfromtailwind.configare not carried over.bg-opacity-*/text-opacity-*(Tailwind 2 style) andmask-*utilities are not generated.- Classes assembled from fragments at runtime (
"bg-" + color + "-500") cannot be found by any scanner: add them tosafelist. - Breakpoints, shadows and nearest-palette colors differ slightly from Tailwind by design: check the result visually.
- The first build parses the full Omnia CSS (a few seconds); later builds are cached.
core: "tokens"is instant.
Node.js 22+. MIT