Skip to content
AgileLogics
7 min read

Utility-First CSS and Tailwind

Prerequisites: CSS architecture and BEM

Utility-first CSS builds interfaces from many small, single-purpose classes applied directly in the HTML. Tailwind CSS is the most popular utility framework.

<a href="/rye" class="rounded-lg bg-brand px-4 py-2 text-white hover:bg-brand/90 md:px-6">View rye loaf</a>

Tailwind scans your files for class names and generates CSS only for the classes you use. With a custom --color-brand theme color, Tailwind v4.3 generated exactly this for the link above:

@layer theme {
  :root, :host {
    --color-white: #fff;
    --spacing: 0.25rem;
    --radius-lg: 0.5rem;
    --color-brand: oklch(55% 0.18 260);
  }
}
@layer utilities {
  .rounded-lg {
    border-radius: var(--radius-lg);
  }
  .bg-brand {
    background-color: var(--color-brand);
  }
  .px-4 {
    padding-inline: calc(var(--spacing) * 4);
  }
  .py-2 {
    padding-block: calc(var(--spacing) * 2);
  }
  .text-white {
    color: var(--color-white);
  }
  @media (hover: hover) {
    .hover\:bg-brand\/90:hover {
      background-color: color-mix(in srgb, oklch(55% 0.18 260) 90%, transparent);
    }
    @supports (color: color-mix(in lab, red, red)) {
      .hover\:bg-brand\/90:hover {
        background-color: color-mix(in oklab, var(--color-brand) 90%, transparent);
      }
    }
  }
  @media (width >= 48rem) {
    .md\:px-6 {
      padding-inline: calc(var(--spacing) * 6);
    }
  }
}

Things to notice:

  • Every utility reads a theme variable (--spacing, --radius-lg, --color-brand), so the design system lives in custom properties.
  • hover: became a :hover rule wrapped in @media (hover: hover) so touch screens don't get stuck hover states; md: became a width >= 48rem media query.
  • bg-brand/90 (90% opacity) uses color-mix(), with an srgb fallback for older browsers.
  • Utilities live in @layer utilities — cascade layers, from this level's earlier lessons.

Setting it up (v4)

/* illustrative: Tailwind-specific CSS, processed by the Tailwind build */
@import "tailwindcss";

@theme {
  --color-brand: oklch(55% 0.18 260);
  --font-display: "Inter", sans-serif;
}

Tailwind v4 is configured in CSS: @import "tailwindcss" brings in the reset (Preflight), theme, and utilities; @theme adds or overrides tokens, and each token creates matching utilities (bg-brand, text-brand, font-display). It runs through a Vite plugin (@tailwindcss/vite), a PostCSS plugin, or its CLI.

Trade-offs

Pros Cons
No naming, no dead CSS, small output Long class lists in HTML
Constrained design tokens keep the UI consistent Learning the utility vocabulary
Changes are local — no fear of breaking other pages Repetition across templates unless you extract components
Works well with component frameworks Harder to read the CSS in DevTools

Keeping it maintainable

  • Extract repetition into components in your framework or template language (a Blade component, a React component) rather than into CSS.
  • Use @apply sparingly — it recreates the problems utility classes were meant to avoid.
  • Class names must appear whole in your source files. Tailwind scans text; it can't see a class built at run time from pieces like "bg-" + color. Write the full names, for example in a lookup table.
  • Plain CSS is still fine next to Tailwind for complex things (animations, :has() logic, third-party widgets).

Utility classes without Tailwind

Even non-utility projects keep a few helpers: .visually-hidden, .flow > * + * { margin-block-start: 1em; }, .text-center.