Skip to content
Strynal, Digital Agency

Engineering 7 min read

Tailwind CSS in Production: Lessons Learned

Hard-won lessons from shipping Tailwind CSS at scale: design tokens, component patterns, readable markup, team conventions, and when to reach for something else.

By Strynal Team

Tailwind CSS divides engineering teams like almost nothing else in the frontend stack. After shipping dozens of production projects with it (from marketing sites to complex web apps), the heated debates largely dissolve into something simpler: it is a powerful tool with real constraints, and ignoring either side of that statement leads to trouble.

Why Tailwind CSS Earns Its Place in Production

The case for Tailwind is not that it writes CSS faster. The case is that it eliminates an entire class of long-term maintenance problem: the stylesheet that grows in one direction.

In a traditional CSS architecture, dead styles accumulate. A class gets added during development, the component it styled gets removed six months later, and the rule stays in the bundle forever because nobody is confident enough to delete it. Tailwind sidesteps this by making styles co-located with the markup they affect. When a component is deleted, its utility classes go with it.

The second real benefit is the constraint system. Tailwind’s default config ships with an opinionated scale for spacing, type sizes, colors, and breakpoints. Those constraints force a kind of visual consistency that even experienced teams struggle to maintain with raw CSS. You cannot accidentally write padding: 17px when the nearest option is p-4.

Tailwind does not make bad CSS decisions for you. It just makes the good ones easier to reach first.

Tokens First: Using the Theme Correctly

The most common mistake on Tailwind projects is treating the default config as the final config. It is not. The default config is a starting point. Before writing a single component, the theme should be extended with the project’s actual design tokens.

This means opening tailwind.config.ts and mapping brand colors, type scales, spacing steps, and border radii to semantic names your team will actually use. A brand color should live in the theme as brand-primary, not scattered as #1A2E4A across fifty files.

// tailwind.config.ts
theme: {
  extend: {
    colors: {
      brand: {
        primary: '#1A2E4A',
        accent: '#F5A623',
      },
    },
    fontFamily: {
      sans: ['Inter', 'system-ui', 'sans-serif'],
      display: ['Fraunces', 'Georgia', 'serif'],
    },
  },
}

Done this way, a brand color change is a one-line edit in the config. Without it, a brand color change is a grep-and-replace operation with a good chance of missing something.

If your project also uses a dedicated design token pipeline, this connects naturally to design tokens as a single source of truth. The Tailwind config becomes the CSS layer of that pipeline, not a parallel system.

Semantic vs. Literal Class Names

A practical pattern: reserve semantic aliases (text-brand-primary, bg-surface) for values that carry meaning across the whole design system, and let Tailwind’s literal utilities (mt-6, flex, rounded-lg) handle the layout and structural styling. Do not try to alias every utility: that defeats the point of the framework.

Component Patterns That Stay Maintainable

Tailwind markup gets messy fast if you do not establish component conventions early. A button with twelve utility classes is readable. A card with thirty is not, and that card will likely gain more classes over time.

The three patterns that hold up best in production:

1. Component abstraction at the right altitude. Extract reusable UI pieces into framework components (React, Astro, Vue, or whatever you are using) and keep the Tailwind classes inside the component, not at the call site. The goal is that the call site reads semantically (<Button variant="primary">) while the implementation reads in utilities.

2. clsx or cva for variant logic. When a component has states or variants, manage class composition with a utility like cva (class-variance-authority) or clsx. Conditional string concatenation becomes unreadable and fragile within a few variants. A cva definition documents the available variants explicitly and composes safely.

3. Avoid @apply except for third-party overrides. @apply exists but should be used sparingly. It reintroduces the abstraction layer Tailwind was meant to eliminate. The one legitimate use is overriding styles in third-party components where you cannot touch the markup.

For teams that care about the handoff between design and engineering, these patterns connect directly to the broader question of closing the Figma-to-production gap. Good component structure on both sides makes that handoff tractable.

Keeping Markup Readable at Scale

The honest tension in Tailwind is that utility-first markup is harder to scan than semantic class names. class="flex items-center gap-4 p-6 bg-white rounded-xl shadow-sm" requires more cognitive effort to parse than class="card".

A few practices that reduce that friction:

  • Sort classes consistently. The official Prettier plugin for Tailwind enforces a canonical class order. Enable it from day one. Unsorted Tailwind classes are significantly harder to review and diff.
  • Group by concern inside a class attribute. Some teams prefix with a line break or comment before layout, typography, and color groups. This is not universal, but on complex components it pays off.
  • Name things at the component boundary, not the class level. The component name (<FeatureCard>) carries the semantic meaning. The classes inside it are implementation details.

Projects that invest in type systems and component libraries benefit here: when your design system is properly structured, even verbose Tailwind markup stays navigable because the structural decisions are made once and repeated consistently.

Performance: What You Get for Free and What You Don’t

Tailwind’s JIT compiler scans your files and generates only the CSS classes you actually use. The output is typically a few kilobytes, often the smallest CSS bundle on the network waterfall. This is genuine and significant.

What Tailwind does not solve is runtime performance. If you are rendering hundreds of components with dozens of classes each, the DOM may carry more class attributes than a CSS-first approach would, but that is rarely the actual bottleneck. The real cost of a slow website is almost always image weight, JavaScript parse time, and render-blocking resources, not CSS payload.

The edge case worth knowing: very large projects with dynamic class generation (classes constructed from runtime variables) can defeat the static scanner. If you are generating class names from data, you need to safelist those patterns in the config or use CSS custom properties for the variable parts.

When Tailwind Is the Wrong Choice

Tailwind works well when teams are building products or sites with a controlled component set, strong design-token discipline, and engineers comfortable in the framework. It is not always the right tool.

Long-form content sites (documentation, editorial, blogs) often benefit from a single scoped stylesheet rather than utility classes on every paragraph. Tailwind ships a @tailwindcss/typography plugin (the prose class) that handles this well, but if the bulk of your surface is prose, the framework overhead may not earn its keep.

Teams that do not enforce conventions. Tailwind amplifies both good and bad CSS habits. A team with strong design-system discipline ships faster and more consistently with Tailwind. A team without that discipline can produce markup that is impossible to maintain inside a year.

Projects with complex animation or highly custom visual effects. Tailwind covers the common case well. When you are building elaborate keyframe sequences, WebGL overlays, or deeply custom interactions, you will reach for raw CSS or a dedicated animation system anyway. That is fine. Tailwind and vanilla CSS can coexist. But if most of the CSS work on a project falls outside what Tailwind handles well, the framework is adding overhead without adding value.

Tailwind and Accessibility

Utility classes have no opinion on semantic HTML or ARIA. That is the engineer’s responsibility. Two common mistakes: using cursor-pointer on a div instead of a button, and styling focus states with focus:outline-none without a visible alternative.

Tailwind 3 introduced focus-visible: variants that target keyboard focus without affecting mouse clicks. Use them. The pattern focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-primary is a reasonable baseline for interactive elements.

Accessibility is a design decision, not a checklist. The implementation layer matters, and Tailwind gives you the utilities to do it right or wrong with equal ease.

How Strynal Approaches Tailwind

At Strynal, Tailwind is a standard part of the stack for the websites and apps we build. It earns its place because we pair it with disciplined token setup from the first day of a project, not retrofitted when the codebase is already large.

Every engagement starts on a blank page. That means the config is set up for the specific project: brand tokens wired in, semantic aliases established, class sorting enforced, component extraction patterns agreed on before the first component is built. When those decisions are made early and consistently, Tailwind does what it promises.

If you are evaluating how to structure the CSS layer of a new product or site, or if an existing Tailwind project has become harder to work with over time, get in touch. Not to pitch a framework, but to think through the actual constraints of your project and what will hold up over the next few years.