Back to Home

Tailwind CSS: limits of design systems for developers

The article analyzes Tailwind CSS issues when building scalable design systems. CVA, CSS variables and @utility approaches with code examples are considered. Recommendations for middle/senior developers on avoiding tech debt.

Tailwind CSS destroys design systems: why and how to fix
Advertisement 728x90

Why Tailwind CSS Limits Scalable Design Systems: Approach Analysis

Tailwind CSS speeds up prototyping, but its utility-first approach creates scaling headaches. In design systems, components like buttons need combos of variants, sizes, and states. Adding a new variation like btn-focus must play nice with btn-sm, btn-lg, btn-outline. States like hover (15% darker) or active (10% brighter) should apply consistently across the board. Traditional frameworks handle this with mixins, like:

.button-variant-other(@color; @background; @border);

Tailwind lacks built-in tools for this kind of seamless integration without compromises.

CVA: Fast but Limited

Shadcn uses CVA for buttons with fixed variants and sizes:

Google AdInline article slot
variant "default" | "outline" | "ghost" | "destructive" | "secondary" | "link"
size "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg"

Advantages:

  • Rapid development without jumping between files.
  • Clean API for basic use cases.

Drawbacks show up in long-term projects:

  • Can't create primary outlined small without hacks.
  • Fixed combos pile up tech debt after 6+ months.

Fine for simple apps, but in enterprise dev, it drives up costs.

Google AdInline article slot

CSS Variables: The Flexible Fix

CSS variables separate state logic from variants:

.btn {
  @apply inline-flex items-center justify-center rounded px-4 py-2 font-body;
  background-color: var(--btn-color);
  color: var(--btn-text, white);
}

.btn:hover {
  background-color: color-mix(in srgb, var(--btn-color), black 15%);
}

.btn:active {
  filter: brightness(1.1);
}

.btn-primary {
  --btn-color: theme('colors.blue.600');
}
.btn-whatever {
  --btn-color: …;
}

Key wins:

  • Auto-states for any color.
  • Easy variant additions via variables.
  • Works with any UI library.

Downside for Tailwind fans: loses some speed since styles move to separate files.

Google AdInline article slot

@utility Directive: A Utility Hack

The directive dynamically generates classes:

@utility btn-* {
  font-size: --value(--text-ui-*, [length]);
  background-color: --value(--color-ui-*, [color]);
  color: --value(--color-ui-*-text, [color]);
}

This pairs with Tailwind modifiers for hover or responsive. But:

  • btn-focus doesn't scale to sm or lg—functional classes fall short.
  • Building primary small outline needs brittle component tweaks.
  • Breaks down with complex combos.

Approach Comparison

| Approach | Speed | Scalability | State Integration | Tailwind Dependency |

|-------------|---------|-------------|-------------------|---------------------|

| CVA | High | Low | Limited | Full |

| CSS vars | Medium | High | Full | Low |

| @utility | Medium | Medium | Partial | High |

CVA shines for MVPs, CSS variables for production, @utility as a middle ground.

Key Takeaways

  • Tailwind doesn't dictate structure: Utility-first demands a DIY system.
  • Components as classes: Locks complex design systems into rigid options.
  • Tech debt alert: Early simplicity means refactoring in six months.
  • Alternatives deliver: CSS variables offer flexibility without ditching Tailwind.

Tailwind rocks for quick starts, but mature design systems need hybrid setups or loosening the utility reins.

— Editorial Team

Advertisement 728x90

Read Next