Tailwind CSS 2.0 landed late last year with dark mode support and an expanded default color palette, and it's become our default choice for new projects across the team. This quarter we tried something harder: retrofitting it onto three existing codebases with established, non-utility CSS, ranging from a hand-rolled BEM system to an older Bootstrap-based admin panel.
The specificity conflicts we didn't expect
Running Tailwind alongside legacy BEM classes for a transition period worked in principle, since utility classes and BEM classes don't collide on naming, but specificity conflicts showed up in places we didn't anticipate, mostly around third-party widget styles that had been loaded with higher-specificity selectors or `!important` rules baked in by whatever library shipped them. A Tailwind utility class setting `padding: 1rem` would silently lose to a legacy rule with higher specificity, and the failure mode is visually subtle enough that it took real debugging time to notice rather than something that threw an obvious error.
The fix, in most cases, was isolating legacy component styles behind a scoping class and treating any new work built with Tailwind as living in its own clean namespace, rather than trying to make old and new CSS coexist inside the same component. That worked, but it meant the migration wasn't purely additive; a few existing components needed a genuine rewrite rather than a drop-in utility class swap, specifically anywhere the old CSS had gotten aggressive with global selectors.
Getting PurgeCSS configuration right
PurgeCSS configuration needed real attention on all three projects; a default configuration left far more unused CSS in the production bundle than we expected, particularly on projects with dynamically generated class names, for example a component that builds a class string like `bg-${color}-500` at runtime, which PurgeCSS's static analysis can't see and will purge as unused by default. We ended up maintaining an explicit safelist for these dynamic cases on two of the three projects, which is manual overhead but a small price for keeping the production CSS bundle from ballooning.
The timing here worked in our favor: Tailwind's JIT engine, which generates styles on demand at build time rather than pre-generating the entire utility set and purging afterward, became available as an opt-in mode partway through the year, and switching to it on the newest of the three projects made the safelist problem largely disappear, since JIT only ever generates the classes it actually sees referenced in the source rather than needing to guess what to keep. We expect to move the other two projects onto JIT mode as we touch them for other reasons.
- Running Tailwind alongside legacy BEM classes worked overall, but specificity conflicts with third-party widget styles required isolating new work behind a scoping class rather than mixing utility and legacy classes freely in the same component.
- Default PurgeCSS configuration left significant unused CSS in the bundle on projects with dynamically generated class names; an explicit safelist, or a move to Tailwind's newer JIT mode, both solved it.
- Component extraction with `@apply` was tempting but overused early on, and we've since limited it to genuinely repeated patterns rather than reaching for it as a default habit.
- Team ramp time was real but short; developers already comfortable with CSS were writing idiomatic Tailwind within about a week on each project.
The three projects, briefly
It's worth being concrete about what we actually retrofitted, since "legacy CSS" covers a lot of ground and the friction varied noticeably across the three. The first was a hand-rolled BEM system on a five-year-old client dashboard, which turned out to be the easiest of the three, since BEM's low-specificity, class-only convention meant Tailwind's utility classes rarely lost a specificity fight; the main cost there was purely the safelist work described above. The second was an older Bootstrap-based admin panel, which was the hardest of the three by a wide margin, since Bootstrap's component classes carry meaningful specificity and its grid system actively conflicts with a utility-first layout approach if the two are mixed carelessly; we ended up isolating new Tailwind-built screens behind a dedicated layout wrapper rather than trying to interleave Tailwind utilities into existing Bootstrap components at all. The third was a smaller marketing site with mostly inline styles and almost no CSS architecture to speak of, which was actually closer to a greenfield migration in disguise, since there was so little existing specificity to fight against.
Dark mode and design system consistency
Tailwind 2.0's dark mode support, driven by a `dark:` variant prefix, gave us a chance to add dark mode to the BEM-based dashboard project as part of the same retrofit, something the client had been asking for but that would have been a much larger lift under the old hand-rolled CSS, since it would have meant a parallel dark-mode stylesheet maintained by hand. Doing it through Tailwind's variant system instead meant most dark mode support was a matter of adding a `dark:` prefixed utility alongside the existing one on each component, which our team could review and ship incrementally rather than as one large, risky release. Beyond dark mode, defining the client's brand colors, spacing scale, and typography directly in the Tailwind config, rather than relying on scattered hex values across old stylesheets, gave us a single source of truth that's already caught a couple of near-miss brand color inconsistencies in design review that would have shipped unnoticed under the old approach.
Training the team on utility-first thinking, not just the syntax
The syntax itself was never the hard part of onboarding a team already comfortable with CSS; writing `flex items-center gap-4` instead of a custom class with the equivalent properties is a small adjustment. The harder adjustment was a genuine shift in habit, away from naming a component's styles and toward composing them inline in the markup, which a few developers on the team resisted initially on the grounds that it made the markup noisier to read. We didn't try to argue everyone out of that reaction; instead we set a team convention that any utility class combination repeated more than three or four times across a project should be extracted into a component or a small `@apply`-based class, which gave people who wanted named abstractions a clear, bounded way to have them without the team sliding back into writing hand-rolled CSS for everything by default.
A specificity bug worth describing in detail
One specificity conflict on the Bootstrap admin panel is worth walking through concretely, since it's a good illustration of the class of bug this retrofit kept surfacing. A Tailwind `text-gray-700` utility applied to text inside a Bootstrap `.card` component was silently overridden by Bootstrap's own `.card-text` selector, which carried higher specificity by virtue of targeting a class rather than relying on the same single-class specificity Tailwind utilities use. The text rendered in Bootstrap's default color instead of the Tailwind gray we'd specified, and because both colors were subtly similar shades of gray, nobody on the team noticed during a normal visual review; it only surfaced when a designer flagged it during a pixel-level comparison against the design file. The eventual fix was to strip the conflicting Bootstrap component classes entirely from any markup being touched during the retrofit rather than trying to out-specificity them, which is a more invasive change per component but avoids an entire category of these subtle, hard-to-spot mismatches going forward.
What we'd do differently on the next retrofit
If we were starting this again, we'd budget for the specificity archaeology explicitly rather than treating it as a risk that might not materialize; on all three projects it ended up being the single largest source of unplanned time, more than the PurgeCSS configuration or the team ramp-up combined, and the Bootstrap project in particular would have benefited from a harder up-front decision to fully replace Bootstrap's grid rather than trying to coexist with it. We'd also push for JIT mode from day one on any retrofit going forward, rather than treating it as an optimization to revisit later, since it would have saved real configuration time on two of the three projects had it been available and adopted from the start.
Greenfield Tailwind adoption is close to friction-free at this point, and every new project we've started this year has used it without hesitation. Retrofitting it onto an established codebase is worth doing, and all three legacy projects are better off for it, but budget more time than you think for the parts of the migration that only show up once real, messy, production CSS is in the mix.