DEV Community

Cover image for Alternatives to Tailwind CSS: Name the Complaint First
Qasim Parray
Qasim Parray

Posted on Originally published at abrarqasim.com

Alternatives to Tailwind CSS: Name the Complaint First

I defend Tailwind more than I probably should, which makes me a biased judge of its alternatives. So this post is me trying to write down the honest case for leaving, then checking which of the alternatives actually survive it.

Short version: most people who search for alternatives to Tailwind CSS don't need a different framework. They need to know which of four complaints they have, because each one has a different fix, and two of the fixes are plain CSS.

The complaints I'd take seriously

Tailwind complaints tend to come bundled together, and they shouldn't be. Pulled apart, I hear four of them.

The first is markup noise. A card with eleven classes on the wrapper and six on every child is hard to scan, and it's hard to review in a pull request when the real change is one class in the middle of a long string. This one is legitimate. Components help, but only if you actually extract them.

The second is the build step. Tailwind needs a toolchain, and some projects, a marketing page with one stylesheet, a server-rendered app with no bundler, don't have one and don't want one.

The third is team taste. Some teams read utility classes fluently and some never will. If half your team resents the syntax, "it's faster once you learn it" is a weak argument against a reviewer who is still not fluent two years in.

The fourth is lock-in to a vocabulary. px-4 means something in Tailwind and nothing anywhere else. Your design tokens live in Tailwind's theme, and moving them out takes real work.

Notice what's missing from that list: performance, and "it's not real CSS". Neither holds up well. The output is a stylesheet, and the theme variables in current versions are ordinary CSS custom properties you declare with @theme. I've already written about the CSS-first config I actually ship, so I won't repeat the setup here.

Native CSS has caught up with a lot of the pitch

Part of why Tailwind took over was that writing plain CSS used to hurt. No nesting without Sass, no scoping, no clean way to share values. Several of those problems got solved in the platform itself.

CSS nesting is the one that matters most for this comparison. The MDN guide to CSS nesting describes it as being parsed by the browser instead of compiled ahead of time by a preprocessor like Sass, and it lets you stop repeating the parent selector. Add custom properties for your tokens, @layer for ordering, and @container for component-level responsiveness, and a stylesheet with no toolchain starts to look reasonable again.

Here's the same card both ways. First, the Tailwind version:

<article class="rounded-xl border border-zinc-200 bg-white p-6 shadow-sm
                hover:shadow-md dark:border-zinc-800 dark:bg-zinc-900">
  <h3 class="text-lg font-semibold text-zinc-900 dark:text-zinc-50">Deploy log</h3>
  <p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">Last run finished clean.</p>
</article>
Enter fullscreen mode Exit fullscreen mode

And native CSS with nesting and custom properties:

.card {
  --card-bg: #fff;
  --card-border: #e4e4e7;
  --card-title: #18181b;
  --card-text: #52525b;

  padding: 1.5rem;
  border: 1px solid var(--card-border);
  border-radius: 0.75rem;
  background: var(--card-bg);
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.05);

  &:hover { box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); }
  & h3 { margin: 0; font-size: 1.125rem; font-weight: 600; color: var(--card-title); }
  & p  { margin: 0.5rem 0 0; font-size: 0.875rem; color: var(--card-text); }

  @media (prefers-color-scheme: dark) {
    --card-bg: #18181b;
    --card-border: #27272a;
    --card-title: #fafafa;
    --card-text: #a1a1aa;
  }
}
Enter fullscreen mode Exit fullscreen mode
<article class="card">
  <h3>Deploy log</h3>
  <p>Last run finished clean.</p>
</article>
Enter fullscreen mode Exit fullscreen mode

The markup is cleaner and the CSS is longer. That trade is the whole debate in one screen. I'd rather pay in CSS lines for a component I touch twice a year, and pay in class strings for the page I rearrange every week. Neither choice is a moral one.

One honest difference: the dark mode above follows the OS setting. A class-based toggle that a user can flip needs one more piece, a selector like :root[data-theme="dark"] that you maintain yourself. Tailwind gives you that with a variant. It's small, and you'll feel it the first time a client asks for a toggle button.

UnoCSS and Panda keep the idea and change the engine

If what you like is the speed of styling in the markup, but you dislike something about Tailwind specifically, two tools keep the approach.

UnoCSS describes itself as an instant atomic CSS engine with no opinions in the core, where all the utilities come from presets. You can define your own rule in a config file and it only generates CSS for classes it finds in your code. That flexibility is the pitch. If your team has strong views about utility names, you can write the vocabulary yourself. The cost is that you now own a vocabulary, and the next developer has to learn yours, because they didn't learn it anywhere else.

Panda CSS goes the other direction. You write styles in TypeScript, and it reads your source at build time to generate atomic CSS, so you get something like CSS-in-JS authoring without shipping a style engine to the browser. The same card looks like this:

import { css } from '../styled-system/css'

export function Card() {
  return (
    <article
      className={css({
        p: '6',
        rounded: 'xl',
        borderWidth: '1px',
        shadow: 'sm',
        _hover: { shadow: 'md' },
      })}
    >
      <h3 className={css({ fontSize: 'lg', fontWeight: 'semibold' })}>Deploy log</h3>
    </article>
  )
}
Enter fullscreen mode Exit fullscreen mode

I'd pick Panda when type safety on style props is the thing the team wants, because a typo in a token name becomes a compile error instead of a card that quietly has no padding. I wouldn't pick it for a content site, because a code generation step and a generated styled-system folder are a lot of machinery for a blog.

I haven't shipped either one in production, so my opinion here is a reading of their docs, not a war story. If you've run UnoCSS on a large codebase, your experience is worth more than mine.

CSS Modules and vanilla-extract are the boring choice

When the complaint is markup noise and the team is already in a component framework, I'd look at the dullest option first. CSS Modules is a bundler feature: you write a normal stylesheet, import it, and class names become locally scoped. You get nesting from the browser, scoping from the bundler, and no new vocabulary.

/* card.module.css */
.card { padding: 1.5rem; border-radius: 0.75rem; }
.card:hover { box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); }
Enter fullscreen mode Exit fullscreen mode
import styles from './card.module.css'

export const Card = () => <article className={styles.card}>Deploy log</article>
Enter fullscreen mode Exit fullscreen mode

vanilla-extract takes that idea and puts it in TypeScript. You write styles in a .css.ts file, they compile to a static stylesheet, and you import class names like any other value:

// card.css.ts
import { style } from '@vanilla-extract/css'

export const card = style({
  padding: '1.5rem',
  borderRadius: '0.75rem',
  ':hover': { boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' },
})
Enter fullscreen mode Exit fullscreen mode

Both give you scoping, which is the thing Tailwind avoids needing in the first place, since utilities don't collide. If your pain is that global CSS leaks, scoping solves it directly. If your pain is that class strings are ugly, scoping does nothing for you and you've swapped one kind of ceremony for another.

When I'd stay put

Switching has a cost that the alternatives' docs never mention. Every project already on Tailwind has people who can read it, a design system that's expressed in it, and a pile of snippets, templates and component libraries that assume it. Leaving means you re-express all of that, and the savings only show up if the complaint was real.

I'd stay with Tailwind when the team is fast in it, when the project has a design system with many contributors, and when the markup noise is something extracted components could fix. I'd also stay when the real problem is that the design itself is inconsistent. A different styling tool doesn't make a team agree on spacing. Writing the spacing scale down as theme values does, and that's a one-afternoon job whichever tool you're using.

I'd leave, or at least not start, for a small content site with no bundler, for a team that's openly unhappy with the syntax, and for a library whose users shouldn't need your build setup to consume it. Those three have something in common: the cost of the toolchain or the vocabulary lands on someone who didn't choose it.

What I'd decide this week

Don't start by picking a tool. Start by naming the complaint, because the cheapest fix for each one is different.

If the complaint is markup noise, extract components before you change tools. Take the five most repeated class strings in your codebase and turn each into a component. If that isn't enough, the native CSS version above is the next thing to try.

If the complaint is the build step, use plain CSS with nesting and custom properties. For a small site, it's less code to maintain than a config you never touch.

If the complaint is team taste, ask the team what they'd write instead, and take that answer seriously. A styling system half the team resents will decay no matter how well it benchmarks.

If the complaint is token lock-in, move your tokens into CSS custom properties first. That's useful whichever tool you end up with, and it's the first step Tailwind's own theme variables already point toward.

This week, take one real component from your project, the card or the button, and rewrite it in native CSS next to the Tailwind version. Time how long it takes and look at which one you'd rather review. If you want to see how I structure client frontends before choosing a stack, my portfolio has examples.


Originally published at abrarqasim.com. I write there about React, PHP, Rust, Go and the AI tooling around them.

Top comments (0)