Tailwind CSS

Six months of Tailwind CSS in production: what we would tell ourselves

TW

Our first real client project on Tailwind CSS launched back in the spring, following the pilot we planned at the end of last year. Half a year of maintaining it in production has given us enough experience to say what has held up and what we would change.

What held up well

  • Encoding the client's design system directly into the Tailwind config made design consistency almost automatic — a developer reaching for `text-brand-600` cannot accidentally use an off-brand color the way a hand-picked hex value invites
  • PurgeCSS integration kept the production CSS bundle genuinely small, under 10kb gzipped for the whole site
  • Onboarding a new developer onto the project took less time than our previous component-class-based projects, since the utility classes are largely self-documenting once you know the naming conventions

What we would do differently

  • We under-invested early in extracting repeated utility combinations into actual components, which led to some real duplication across templates before we cleaned it up
  • A few developers new to utility-first CSS took longer to get comfortable than we expected, and we would build a short internal onboarding guide from day one next time rather than leaving people to figure out the conventions themselves

Overall the pilot was a success, and Tailwind is now our default choice for new client CSS work going forward, alongside our existing component-based approach for projects that are a better fit for it.

A concrete duplication problem we found and fixed

Partway through the project, we noticed the same eleven-utility-class combination for a "primary call-to-action button" repeated across nine different templates, each copy having drifted slightly from the others as different developers made small tweaks over time without anyone owning the canonical version. We extracted it into an actual Vue component wrapping those utilities once, and grep-replaced all nine call sites to use it instead, which took an afternoon but removed a source of visual inconsistency that had been slowly accumulating without anyone noticing it as a pattern until we went looking specifically for repeated utility strings.

Measuring the onboarding-time difference more concretely

We tracked how long it took the two developers who joined this project partway through to make their first meaningful pull request, and compared it informally against our historical average for developers joining an established component-class-based project of similar size. Both new developers on this Tailwind project shipped a real change faster than our historical average, though we hold that comparison loosely given the small sample size — two developers is not a rigorous study — but it matches our subjective sense that reading a utility class list in the markup, once you know the naming conventions, requires less codebase-specific tribal knowledge than tracing a semantic class name back to wherever its actual styles are defined.

Adding dark mode partway through, without planning for it upfront

The client requested a dark mode roughly four months into the project, which we had not originally scoped or designed for. Tailwind's `dark:` variant, which lets any utility have a dark-mode-specific counterpart declared inline right next to its light-mode value, made this a considerably smaller undertaking than we braced for — most components needed only a handful of added `dark:` variants directly alongside their existing classes, rather than a parallel dark-mode stylesheet or a significant CSS refactor. It is not something we would recommend skipping in initial design work if you know dark mode is coming, but as an unplanned mid-project addition, Tailwind's variant system handled it about as gracefully as we could have hoped for.

What we would tell a team about to start their first real Tailwind project

Set up the shared design tokens in the Tailwind config before writing a single component, even if the client's design system is not fully finalized yet — a config file that already encodes the client's actual brand colors and spacing scale, even a provisional version of it, saves the far more painful exercise of retrofitting dozens of components from hardcoded values once the design system does solidify. Also budget real time early for identifying and extracting repeated utility combinations into components, rather than treating that as cleanup work to get to eventually, since our own under-investment here is the single thing we would most want to redo differently on a second attempt at this project.

Build-time impact of the Tailwind pipeline itself

One thing we did not anticipate going in: the initial uncached Tailwind build, scanning every template file for used classes, added a noticeable amount of time to a cold CI build compared to our previous component-class stylesheet, which compiled in a fraction of the time. Caching the build step between CI runs brought this back down to a non-issue for day-to-day development, but it is a real, if minor, operational detail that a team evaluating Tailwind for the first time should budget CI configuration time for rather than discovering only after their build times have already crept upward.

An informal survey of how the team feels about it now

At the six-month mark we ran a short, informal internal survey asking the team building on this project whether they would choose Tailwind again for a similar project. The response was strongly positive, with the most common praise being the reduced context-switching between markup and stylesheet, and the most common lingering complaint being that a genuinely complex, deeply nested component's markup can still look visually noisy with a long utility class list attached to a single element, even after extracting the most obviously repeated patterns into components.

Results

Overall the pilot was a success, and Tailwind is now our default choice for new client CSS work going forward, alongside our existing component-based approach for projects that are a better fit for it. Six months of real production maintenance, including an unplanned scope addition, gave us considerably more confidence in that recommendation than the weekend experiment alone ever could have.

Performance impact on actual page load, not just bundle size

A small production CSS bundle is not automatically the same thing as a fast-feeling page, so we also measured real page load metrics — First Contentful Paint and Largest Contentful Paint — on this project against a comparable client site still running our older component-class approach. The Tailwind-based site's metrics were modestly better, though the difference was smaller than the bundle-size gap alone would suggest, which is a useful reminder that CSS bundle size is only one of several factors determining a page's actual perceived speed, alongside image optimization, font loading strategy, and server response time, none of which Tailwind itself has any direct bearing on.

A client stakeholder's reaction to seeing the utility-class markup

One client stakeholder with some technical background asked, during a code walkthrough, why the HTML looked so much more cluttered than a previous project's cleaner semantic class names, a completely fair reaction given how different utility-first markup looks from what most people picture when they think of clean code. We walked through the actual tradeoff honestly — markup verbosity in exchange for eliminating an entire category of CSS specificity and override problems — rather than dismissing the concern, and the stakeholder came away satisfied once they saw how quickly the team could make a visual change during the same walkthrough, live, by just editing classes directly rather than hunting through a separate stylesheet. We now explicitly tell new client stakeholders about this expected tradeoff during the project kickoff conversation, before they ever see a line of the actual markup, which has made the reaction described above less common on subsequent projects than it was the first time we ran into it here.

How we handled one-off custom styles that do not fit the utility model

A handful of highly specific visual effects on this project — a particular animated gradient background, for instance — genuinely did not fit cleanly into Tailwind's utility model no matter how we tried to compose them from existing utilities. Rather than forcing an awkward utility-only solution, we used Tailwind's own supported escape hatch, a small amount of custom CSS defined alongside the utility classes for exactly these specific cases, which we think is a healthier approach than either avoiding custom CSS on principle or overusing it out of unfamiliarity with what the utility approach can actually cleanly express.

One more onboarding aid we built after this project

Following this project, we recorded a short internal screencast walking through the Tailwind config file and its relationship to the client's actual brand guidelines, specifically for future developers joining a Tailwind project without the benefit of having lived through this project's early ramp-up conversations in person.

← Back to the journal

Have a project in mind?
Let’s talk.

Tell us where you are and where you want to go. We'll map the fastest route between the two.

Currently accepting new clients