This episode covers migrating large projects to Tailwind: incremental techniques from traditional CSS, monorepo considerations for sharing config and design tokens, and safe rollback and compatibility testing strategies.

Migrating a large codebase to Tailwind is a multi-year project, not a weekend task. Episode 21 covers realistic strategies: incremental component-by-component conversion, monorepo architecture for sharing config and tokens, and rollback and compatibility testing to keep the journey safe.
The right mindset: migration isn't "replace all CSS today", it's "continuously reduce dependence on old CSS until the point where removing it becomes cheap". Every step must be reversible, measurable, and non-disruptive to users.
The key to safety: all new components use Tailwind immediately. This stops the old CSS from growing from day one.
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -pAfter that, Tailwind's output.css is imported alongside the old stylesheets. Nothing is removed — only added.
Convert one component at a time. The recommended pattern: move utilities directly into markup, then delete the old CSS rule after the visuals are verified:
<!-- Sebelum -->
<button class="btn btn-primary">Simpan</button>
<!-- Sesudah -->
<button class="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
Simpan
</button>For components with complex CSS, first translate into @apply in the components layer for a gradual transition — then set it aside later once the markup is clean.
Run two versions of the UI simultaneously using a feature flag. Some users get the Tailwind version, the rest get the old one — compare metrics and bug reports before switching fully:
const useTailwind = featureFlags.isEnabled(user.id, "ui-tailwind");
const variant = useTailwind ? "tailwind" : "legacy";featureFlags.isEnabled(user.id, "ui-tailwind") gives you a path to test changes on a subset of users, while keeping the rest of the traffic on the stable version.
In a monorepo, config and tokens are shared across packages without duplication. Put the preset in its own package:
packages/
ui/ -> komponen bersama
tailwind-preset/ -> token + plugin bersama
apps/
web/ -> aplikasi utama
admin/ -> aplikasi adminThe preset is consumed by all applications:
module.exports = {
presets: [require("@org/tailwind-preset")],
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"../../packages/ui/src/**/*.{js,ts,jsx,tsx}",
],
};Notice content includes ../../packages/ui/src/**/* — without this, classes in shared components are never generated. The pattern content: ["../../packages/ui/src/**/*"] is the most commonly missed detail in monorepo setups.
A big migration needs a way home. A few safety nets:
A simple compatibility test example:
npx playwright test --project=chromium --project=firefox --project=webkitnpx playwright test --project=chromium runs the whole suite across three browser engines at once. If the new look passes all projects and metrics don't decline, the component is considered ready.
Warning
Never delete old CSS wholesale in a single commit. Only remove it per component after its Tailwind version passes visual tests and runs stably in production for a while. Frequent small changes are safer than one big leap.
Episode 21 made large migrations feel safe: new components use Tailwind immediately, per-component conversion with flags, monorepo architecture for sharing presets, and continuous rollback and compatibility testing.
Key takeaways:
content must include the path to the shared component package.Next, in episode 22 — the final episode — we'll cover production hardening & best practices: a final build-size checklist, CSP, SRI, caching, and accessibility audits, avoiding runtime Tailwind injection, and documenting conventions for your team.