Dublin Library

The Publishing project

Tailwind CSS: Utility-first styling in the modern web

In this post

Over the last few years, the way we style web applications has shifted dramatically. One of the most significant forces behind this change is Tailwind CSS. Often described as a utility-first CSS framework, Tailwind has sparked intense debates. Some developers swear by its speed and consistency, while others criticize it for cluttering HTML and moving away from semantic stylesheets.

I’ve resisted using and writing about Tailwind for a long time, but for specific types of projects, it’s a powerful tool that can significantly boost development speed and maintain design consistency.

In this tutorial, you’ll learn how Tailwind CSS works, how to set up a basic project, and how to construct a responsive user interface (UI) component. You’ll also customize your design token system, explore the changes in Tailwind CSS v4 (including its CSS-first configuration), and weigh its benefits and drawbacks against modern native CSS features.

What is utility-first CSS?

Traditionally, when styling a button, you would write a semantic class name in HTML and define its rules in a separate CSS file:

<!-- HTML -->
<button class="btn-primary">Click me</button>
/* CSS */
.btn-primary {
  background-color: #3b82f6;
  color: #ffffff;
  padding: 0.5rem 1rem;
  border-radius: 0.25rem;
  font-weight: 600;
}
.btn-primary:hover {
  background-color: #2563eb;
}

With a utility-first approach like Tailwind, you don’t write custom CSS. Instead, you apply predefined, single-purpose helper classes directly to the elements:

<button class="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded font-semibold transition">
  Click me
</button>

At first glance, this looks like inline styles (style="background-color: ..."). However, there are crucial differences:

  1. Design system constraints: You are limited to predefined options (e.g., specific colors, spacing units, and font sizes) rather than arbitrary values, which maintains visual consistency.
  2. Responsive and state variants: Tailwind lets you style hover states (hover:bg-blue-600), focus states (focus:ring), active states, and media queries (md:py-4) directly in your class attributes.

Setting up Tailwind CSS

Now that you understand the basic premise of utility-first CSS, let’s look at how to set it up in a real project. There are several ways to integrate Tailwind, but the most common modern approaches are using the Tailwind command-line interface (CLI) or integrating it into a build tool like Vite.

Option 1: Tailwind CLI

The CLI is the quickest way to compile Tailwind standalone.

In Tailwind CSS v4

  1. Install dependencies: Tailwind v4 CLI is distributed as a separate package:

    npm install -D tailwindcss @tailwindcss/cli
    
  2. Add Tailwind import to your input CSS: In your main stylesheet (e.g., src/input.css), import Tailwind directly (v4 uses standard CSS @import instead of @tailwind directives):

    @import "tailwindcss";
    
  3. Run the build script: Use the new CLI package command to build your styles:

    npx @tailwindcss/cli -i ./src/input.css -o ./dist/output.css --watch
    
  4. Link the output stylesheet in your HTML: Reference the compiled CSS file inside your HTML files (e.g., index.html):

    <link href="./dist/output.css" rel="stylesheet">
    

    Tailwind CSS v4's CLI is designed to be a drop-in replacement for the previous version, but it has been optimized for speed and simplicity. It no longer requires a separate PostCSS setup, and it compiles your styles directly from the CSS imports, making it faster and easier to use.

In Tailwind CSS v3

  1. Install dependencies:

    npm install -D tailwindcss
    

    (Note: Unlike full PostCSS setups, the standalone CLI does not require installing postcss or autoprefixer directly.)

  2. Initialize configuration:

    npx tailwindcss init
    

    This generates tailwind.config.js.

  3. Configure template paths (in tailwind.config.js):

    /** @type {import('tailwindcss').Config} */
    module.exports = {
      content: ["./src/**/*.{html,js,ts,jsx,tsx}"],
      theme: {
        extend: {},
      },
      plugins: [],
    }
    
  4. Add Tailwind directives to your input CSS: In your main stylesheet (e.g., src/input.css), load Tailwind’s layers:

    @tailwind base;
    @tailwind components;
    @tailwind utilities;
    
  5. Run the build script:

    npx tailwindcss -i ./src/input.css -o ./dist/output.css --watch
    
  6. Link the output stylesheet in your HTML: Reference the compiled CSS file inside your HTML files (e.g., index.html):

    <link href="./dist/output.css" rel="stylesheet">
    

Option 2: Vite integration (Tailwind CSS v4)

For modern web applications, integrating Tailwind directly into Vite provides a fast and seamless build experience. (Note: The following instructions focus on the modern Tailwind CSS v4 setup.)

  1. Install dependencies: Install Tailwind and its official Vite plugin as development dependencies:

    npm install -D tailwindcss @tailwindcss/vite
    
  2. Configure Vite: Add the Tailwind plugin to your vite.config.js or vite.config.ts:

    import { defineConfig } from 'vite'
    import tailwindcss from '@tailwindcss/vite'
    
    export default defineConfig({
      plugins: [
        tailwindcss(),
      ],
    })
    
  3. Add Tailwind import to your CSS: Import Tailwind in your main CSS entrypoint (e.g., src/index.css):

    @import "tailwindcss";
    
  4. Import the CSS file in your JavaScript/TypeScript entrypoint: Import the stylesheet at the top of your main script file (e.g., src/main.js or src/index.js):

    import './index.css';
    
  5. Link your script entrypoint in your HTML: Reference your main JavaScript or TypeScript script file inside your HTML file (e.g., index.html), ensuring you use type="module" so Vite compiles and includes the imported stylesheet:

    <script type="module" src="/src/main.js"></script>
    

Tailwind CSS utility reference and memory guide

With Tailwind successfully installed, you might be overwhelmed by the sheer number of available utility classes. Fortunately, you don’t need to memorize them all. Tailwind CSS uses a predictable grammar and naming structure. Understanding the system’s underlying rules lets you construct and decipher utility classes on the fly without constantly checking the official documentation.

The grammar of Tailwind

Every Tailwind class follows a consistent anatomy:

[modifier]:[utility]-[property]-[value]/[opacity]

Examples

  • hover:bg-blue-500/80 * hover: → State modifier (when to apply) * bg → Utility (what property category: background) * blue → Property (which color) * 500 → Value (shade/intensity) * /80 → Opacity (80% opacity) * md:dark:hover:translate-x-2 * md: → Screen size modifier (medium viewport and up) * dark: → Theme modifier (dark mode active) * hover: → Interaction modifier (hover state) * translate-x → Utility (translate on X-axis) * 2 → Value on spacing scale (0.5rem)

Spacing and sizing (the “divide by 4” rule)

Tailwind’s spacing scale (p-*, m-*, w-*, h-*, gap-*) uses numeric tokens. To calculate the native CSS value:

CSS value in rems = Tailwind Number / 4
CSS value in pixels = Tailwind Number * 4

Spacing quick reference table

Tailwind tokenRem valuePixel value (at 16px base)Mnemonic or visual size
00rem0pxNo spacing
0.50.125rem2pxMicro spacing
10.25rem4pxTiny (thin line gap)
20.5rem8pxSmall gap / padding
41rem16pxStandard body font size / base gap
61.5rem24pxStandard page margin padding
82rem32pxLarge padding / header spacing
123rem48pxSection margins
164rem64pxLarge hero margins

Because Tailwind uses rem (Root EM) units for spacing, sizing, and typography by default, these values are mathematically tied to the root font size of your document. If you alter the font-size on the or :root element (or if a user changes their browser's default font size for accessibility), all of your Tailwind utility classes will dynamically scale up or down to match it.

Directional shortcuts

Tailwind simplifies layout directions by matching physical axes and margins.

Coordinates and sides

  • tTop (e.g., mt-4margin-top: 1rem;)
  • bBottom (e.g., pb-2padding-bottom: 0.5rem;)
  • lLeft (e.g., pl-8padding-left: 2rem;)
  • rRight (e.g., mr-1margin-right: 0.25rem;)
  • xX-axis (horizontal) (left and right) (e.g., px-4padding-left: 1rem; padding-right: 1rem;)
  • yY-axis (vertical) (top and bottom) (e.g., my-6margin-top: 1.5rem; margin-bottom: 1.5rem;)
  • sStart (Logical left in LTR layouts) (e.g., ms-2margin-inline-start: 0.5rem;)
  • eEnd (Logical right in LTR layouts) (e.g., pe-3padding-inline-end: 0.75rem;)

Remember X is left and right (horizontal timeline) and Y is up and down (vertical elevator).

Primary property categories

Layout and positioning

Tailwind shortcuts mirror the actual CSS properties closely:

Tailwind classNative CSS equivalentMemory anchor
blockdisplay: block;Plain English
flexdisplay: flex;Plain English
griddisplay: grid;Plain English
hiddendisplay: none;”Hide” the element
relativeposition: relative;Plain English
absoluteposition: absolute;Plain English
fixedposition: fixed;Plain English
z-[val]z-index: [val];z index coordinates

Typography

Tailwind classNative CSS equivalentMemory anchor
text-centertext-align: center;Align text
text-[size]font-size: [size];Font size controls size of text
font-[weight]font-weight: [weight];Font weight (e.g., font-bold700)
leading-[val]line-height: [val];Typography term: leading (vertical spacing between lines)
tracking-[val]letter-spacing: [val];Typography term: tracking (horizontal spacing between letters)

Borders and effects

  • roundedborder-radius. * Scale: rounded-sm (2px), rounded (4px), rounded-md (6px), rounded-lg (8px), rounded-xl (12px), rounded-2xl (16px), rounded-full (9999px).
  • shadowbox-shadow. * Scale: shadow-sm, shadow (base), shadow-md, shadow-lg, shadow-xl, shadow-2xl, shadow-inner.
  • opacityopacity. * Scale: Percentage values (opacity-0 to opacity-100).

Flexbox and grid controls

Flexbox and grid layout classes combine container setups with child alignment controls.

Flex container

  • flex-row → Align items horizontally (default).
  • flex-col → Align items vertically (column format).
  • flex-wrap → Allow items to wrap to a new line.

Alignment (cross-axis versus main-axis)

To align elements, Tailwind uses standard CSS terms:

  • justify-[alignment]main-axis (horizontal by default). * justify-start → Align left. * justify-end → Align right. * justify-center → Center items. * justify-between → Spaced out evenly (first item at start, last item at end).
  • items-[alignment]cross-axis (vertical). * items-start → Align top. * items-end → Align bottom. * items-center → Align vertically centered. * items-stretch → Stretch to fill height (default).

Grid configuration

  • grid-cols-[n] → Define the number of grid columns (e.g., grid-cols-3 → 3 columns).
  • col-span-[n] → Set how many columns a child item spans (e.g., col-span-2 → spans two columns).

Mental shortcuts to code without docs

  1. Don’t Guess Colors, Guess Hue and Value:
    • Tailwind color shades range from 50 (lightest) → 950 (darkest), with 500 being the exact pure hue.
    • If you need a text color that is readable on a white background, aim for 700 to 950 (e.g., text-slate-800).
    • If you need a border color, aim for 100 or 200 (e.g., border-slate-200).
  2. Use Arbitrary Values [*] for One-Offs:
    • If you need a very specific styling value that is not on the default scale, wrap it in square brackets. Tailwind compiles it on the fly:
      • w-[347px]width: 347px;
      • bg-[#ff00ff]background-color: #ff00ff;
      • top-[12.5rem]top: 12.5rem;
  3. Compound Modifiers Stack Orderly:
    • Order modifiers from broad constraints to specific interactions:
      • Correct: lg:dark:hover:bg-slate-700 (Screen size → Color scheme → Interaction → Utility).

Building a component: The card

Once you grasp this grammar, you can start combining simple classes to build complex user interfaces rapidly. Let’s put Tailwind to work by building a modern, responsive card component with a static glassmorphism badge and a smooth hover lift-and-shadow effect.

<div class="max-w-sm sm:max-w-md mx-auto bg-white dark:bg-slate-800 rounded-2xl shadow-xl overflow-hidden border border-slate-100 dark:border-slate-700/50 transition-all duration-300 hover:shadow-2xl hover:-translate-y-1">
  <!-- Card Image -->
  <div class="relative h-48 bg-slate-200">
    <img
      src="https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&w=600&q=80"
      alt="Ocean Beach"
      class="w-full h-full object-cover"
    />
    <span class="absolute top-4 right-4 bg-white/80 backdrop-blur-md text-xs font-semibold text-slate-800 px-2.5 py-1 rounded-full uppercase tracking-wider shadow-sm">
      Travel
    </span>
  </div>

  <!-- Card Body -->
  <div class="p-6 sm:p-8">
    <div class="flex items-center space-x-2 text-sm text-blue-500 font-medium mb-2">
      <span>June 12, 2026</span>
      <span>•</span>
      <span>5 min read</span>
    </div>

    <h3 class="text-xl sm:text-2xl font-bold text-slate-900 dark:text-white mb-2 leading-tight">
      Exploring the Hidden Beaches of the Pacific Coast
    </h3>

    <p class="text-slate-600 dark:text-slate-400 text-sm sm:text-base mb-6 line-clamp-3">
      From rugged sea stacks to hidden coves, the Pacific Coast holds some of the country's most spectacular coastal treasures. Here is how to find them.
    </p>

    <!-- Author and action -->
    <div class="flex items-center justify-between">
      <div class="flex items-center space-x-3">
        <div class="w-10 h-10 rounded-full bg-slate-300 overflow-hidden">
          <img src="https://i.pravatar.cc/100?img=33" alt="" />
        </div>
        <div>
          <p class="text-sm font-semibold text-slate-800 dark:text-slate-200">Carlos A.</p>
          <p class="text-xs text-slate-500">Content Creator</p>
        </div>
      </div>

      <a
        href="#"
        class="inline-flex items-center justify-center p-2 rounded-full bg-slate-50 dark:bg-slate-700 text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/40 transition-colors"
        aria-label="Read article"
      >
        <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
          <path stroke-linecap="round" stroke-linejoin="round" d="M14 5l7 7m0 0l-7 7m7-7H3" />
        </svg>
      </a>
    </div>
  </div>
</div>

Breakdown of utilities used

  • Layout and sizing: Classes like max-w-sm (with sm:max-w-md for larger viewports) control the maximum width, mx-auto centers the container, and flex with space-x-3 handles alignment and spacing.
  • Colors and theming: The bg-white dark:bg-slate-800 utility switches the background color based on system light or dark preferences.
  • Borders and corners: The rounded-2xl class creates large rounded corners, and border-slate-100 adds a subtle boundary line.
  • Transitions: Utilities like transition-all duration-300 hover:shadow-2xl hover:-translate-y-1 create a smooth lift-and-shadow effect when hovering over the card.
  • Glassmorphism: The bg-white/80 backdrop-blur-md classes apply a translucent background with background blur.

Customizing the design system

While Tailwind’s default design system covers most use cases, you will eventually need to adapt it to your specific brand. One of Tailwind’s strongest selling points is its customizability. Instead of being stuck with default design tokens, you can easily define your own color palettes, typography, and spacing scales.

In Tailwind CSS v4 (CSS-first configuration)

In Tailwind CSS v4, the architecture has been streamlined significantly. The framework moved away from the JavaScript configuration file (tailwind.config.js) in favor of a CSS-first configuration.

Instead of writing JavaScript files to extend your theme, you configure Tailwind directly in your CSS using standard CSS custom properties under the new @theme directive:

/* src/input.css */
@import "tailwindcss";

@theme {
  --color-brand-50: #f0f7ff;
  --color-brand-100: #e0effe;
  --color-brand-500: #3b82f6;
  --color-brand-900: #1e3a8a;

  --font-sans: "Outfit", sans-serif;
}

Advantages of v4 configuration

  1. No config file: Tailwind automatically builds your classes and configuration directly from imports and standard CSS syntax.
  2. CSS variables out of the box: Under the hood, Tailwind generates real CSS variables (like --color-brand-500) for every theme token, letting you use them in vanilla CSS code seamlessly.
  3. Faster compiling: By using Rust-based compilation, v4 is incredibly fast compared to JavaScript-reliant preprocessors.

In Tailwind CSS v3 (JavaScript configuration)

For legacy projects, you customize tokens in tailwind.config.js under the theme key. If you want to add colors while keeping the default ones, you put them under extend:

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        brand: {
          50: '#f0f7ff',
          100: '#e0effe',
          500: '#3b82f6',
          900: '#1e3a8a',
        },
      },
      fontFamily: {
        sans: ['Outfit', 'sans-serif'],
      },
    },
  },
}

Now, you can use classes like bg-brand-500 or text-brand-900 natively.

Critical evaluation: Tailwind CSS versus vanilla CSS

At this point, you have seen how to set up, use, and customize Tailwind CSS. However, while Tailwind is exceptionally popular, it is not a silver bullet. Let’s take a step back and look at a balanced evaluation of utility-first CSS compared to modern native CSS features.

Advantages

  • Development velocity: You build layouts without writing custom class names or switching files.
  • Built-in styling constraints: It stops team members from using arbitrary sizes, creating a cohesive interface.
  • Unused code purging: Tailwind analyzes your source files and removes all classes you didn’t use, resulting in tiny CSS bundle sizes (often under 20KB).
  • Consistent scoping: It avoids cascading issues and style leakages between different team members’ code.

Limitations

  • HTML bloat: The class strings can become incredibly long and hard to read, making the document hierarchy difficult to scan.
  • Dependency and build lock-in: You cannot use Tailwind without a compilation step (such as the CLI or Vite plugin) in production, which adds complexity.
  • Separation of concerns: Styling rules are intermingled with markup.
  • Steep learning curve and mental context switching: You must learn Tailwind’s shorthand dialect (pl-4, md:max-h-screen, col-span-2) instead of writing plain CSS. While you don’t switch files, you do switch mental contexts, constantly having to translate standard CSS properties into Tailwind’s specific utility names.

Modern vanilla CSS alternatives

Modern CSS specifications have evolved dramatically to address the primary reasons frameworks like Tailwind were created. Below is an example of how you can build the same responsive, dark-mode-aware card component using vanilla CSS and modern features like CSS custom properties, native nesting, and container queries:

The HTML structure

Instead of utility classes, the HTML uses semantic class names that describe the component’s structure rather an its styling.

<!-- HTML markup with semantic classes -->
<div class="travel-card">
  <div class="card-image-wrapper">
    <img src="https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&w=600&q=80" alt="Ocean Beach" />
    <span class="card-badge">Travel</span>
  </div>
  <div class="card-body">
    <div class="card-meta">
      <span>June 12, 2026</span>
      <span>•</span>
      <span>5 min read</span>
    </div>
    <h3 class="card-title">Exploring the Hidden Beaches of the Pacific Coast</h3>
    <p class="card-desc">From rugged sea stacks to hidden coves, the Pacific Coast holds some of the country's most spectacular coastal treasures...</p>
    <div class="card-footer">
      <div class="card-author">
        <div class="author-avatar"><img src="https://i.pravatar.cc/100?img=33" alt="" /></div>
        <div>
          <p class="author-name">Carlos A.</p>
          <p class="author-role">Content Creator</p>
        </div>
      </div>
      <a href="#" class="card-action-btn" aria-label="Read article">→</a>
    </div>
  </div>
</div>

Vanilla CSS

The CSS file is organized using native nesting to group styles under the .travel-card parent selector. It uses CSS custom properties for design tokens and container queries for responsive adjustments.

It is more verbose than Tailwind, but it’s also more explicity, doesn’t require memorization of utility classes and can be written without a build step in a plain CSS file.

/* CSS using Native Nesting, Custom Properties, and Container Queries */
@layer components {
  .travel-card {
    /* Define scoping system using local CSS variables */
    --card-bg: var(--color-white, #ffffff);
    --card-text: var(--color-slate-900, #0f172a);
    --card-text-muted: var(--color-slate-600, #475569);
    --card-border: var(--color-slate-100, #f1f5f9);

    /* Container query container definition */
    container-type: inline-size;
    container-name: card-container;

    max-width: 24rem;
    margin: 0 auto;
    background-color: var(--card-bg);
    color: var(--card-text);
    border-radius: 1rem;
    border: 1px solid var(--card-border);
    overflow: hidden;
    box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1);
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);

    &:hover {
      box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
      transform: translateY(-4px);
    }

    /* Dark mode styling using CSS variables swap */
    @media (prefers-color-scheme: dark) {
      --card-bg: var(--color-slate-800, #1e293b);
      --card-text: var(--color-white, #ffffff);
      --card-text-muted: var(--color-slate-400, #94a3b8);
      --card-border: var(--color-slate-700, #334155);
    }

    /* Nesting child selectors directly */
    .card-image-wrapper {
      position: relative;
      height: 12rem;

      img {
        width: 100%;
        height: 100%;
        object-fit: cover;
      }

      .card-badge {
        position: absolute;
        top: 1rem;
        right: 1rem;
        background-color: rgb(255 255 255 / 0.8);
        backdrop-filter: blur(12px);
        font-size: 0.75rem;
        font-weight: 600;
        color: #1e293b;
        padding: 0.25rem 0.625rem;
        border-radius: 9999px;
        text-transform: uppercase;
        letter-spacing: 0.05em;
        box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
      }
    }

    .card-body {
      padding: 1.5rem;
    }

    .card-meta {
      display: flex;
      align-items: center;
      gap: 0.5rem;
      font-size: 0.875rem;
      color: var(--color-blue-500, #3b82f6);
      font-weight: 500;
      margin-bottom: 0.5rem;
    }

    .card-title {
      font-size: 1.25rem;
      font-weight: 700;
      line-height: 1.25;
      margin-block: 0.5rem;
    }

    .card-desc {
      color: var(--card-text-muted);
      font-size: 0.875rem;
      margin-bottom: 1.5rem;
      display: -webkit-box;
      -webkit-line-clamp: 3;
      line-clamp: 3;
      -webkit-box-orient: vertical;
      overflow: hidden;
    }

    .card-footer {
      display: flex;
      align-items: center;
      justify-content: space-between;
    }

    .card-author {
      display: flex;
      align-items: center;
      gap: 0.75rem;

      .author-avatar {
        width: 2.5rem;
        height: 2.5rem;
        border-radius: 9999px;
        background-color: var(--color-slate-300, #cbd5e1);
        overflow: hidden;

        img {
          width: 100%;
          height: 100%;
          object-fit: cover;
        }
      }

      .author-name {
        font-size: 0.875rem;
        font-weight: 600;
        color: var(--card-text);
        margin: 0;
      }

      .author-role {
        font-size: 0.75rem;
        color: var(--card-text-muted);
        margin: 0;
      }
    }

    .card-action-btn {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 2.25rem;
      height: 2.25rem;
      aspect-ratio: 1;
      padding: 0.5rem;
      border-radius: 9999px;
      background-color: var(--color-slate-50, #f8fafc);
      color: var(--color-blue-600, #2563eb);
      text-decoration: none;
      transition: background-color 0.2s, color 0.2s;

      &:hover {
        background-color: var(--color-blue-50, #eff6ff);
      }

      @media (prefers-color-scheme: dark) {
        background-color: var(--color-slate-700, #334155);
        color: var(--color-blue-400, #60a5fa);

        &:hover {
          background-color: rgb(30 58 138 / 0.4);
        }
      }
    }

    /* Component responsiveness using container queries */
    @container card-container (min-width: 350px) {
      .card-title {
        font-size: 1.5rem; /* Automatically increases font size if card itself is wide */
      }
    }
  }
}

Where modern CSS replaces Tailwind

  1. Design tokens with CSS custom properties: Instead of relying on tailwind.config.js or Tailwind’s @theme directive, you can use vanilla CSS variables. By styling elements with var(--color-primary), you get the same design constraint system.
  2. Native nesting: Historically, nested layouts required preprocessors like Sass. With native nesting, you can group all your component styles cleanly inside a parent selector, preventing stylesheet clutter.
  3. True component responsiveness via @container: Tailwind operates primarily using viewport-based media queries (md:, lg:). If you place a viewport-responsive card in a narrow sidebar, it looks bloated because it doesn’t know its parent container is small. Container queries (@container) let you write rules that target the component’s actual layout space, a massive improvement over traditional viewport design.

Where vanilla CSS still falls short

Despite major specification additions, vanilla CSS falls short of Tailwind in several workflows:

  1. The naming tax and cognitive load: In the vanilla example, we had to invent class names for almost every sub-element (.card-image-wrapper, .card-body, .card-title, .card-action-btn). In a large team, this leads to naming fatigue, inconsistency, and confusion (e.g., one person uses .btn-action while another uses .action-btn). While this tax can be managed using strict naming methodologies (like BEM), CSS linters (like Stylelint), or auto-scoping CSS Modules, Tailwind eliminates the burden of inventing class names completely.
  2. File and context switching: While modern component architectures allow you to collocate CSS in the same file (like Vue/Svelte <style> blocks or CSS-in-JS), you still must switch contexts between your markup and your style definitions. Tailwind lets you style the DOM directly on the elements themselves, keeping your focus completely unified.
  3. No automatic bundle optimization (purging): A custom-written CSS file retains every line of code you write, even if you delete the HTML elements that used those classes. Over time, CSS codebases suffer from “append-only CSS” bloat. Tailwind’s build tool automatically inspects your templates and strips out unused classes, keeping your production CSS bundle consistently minimal.
  4. Utility-level encapsulation: Although modern CSS supports native scoping via the @scope at-rule (letting you isolate styles to specific DOM subtrees) and specificity management via @layer directives, you still have to manually define and maintain these boundaries. Tailwind utilities bypass this by being self-contained and deterministic at the class level.

The middle ground: Tailwind CSS versus Open Props

If Tailwind feels too extreme and vanilla CSS feels too disorganized, there is an alternative. For developers who want the structure of a curated design system (like Tailwind’s design tokens) but prefer the separation of concerns offered by standard stylesheets, Open Props has emerged as a compelling middle ground.

Unlike Tailwind’s utility-first approach, which places helper classes directly in your HTML markup, Open Props is a collection of custom properties (CSS variables) that you use in standard stylesheets:

/* Styling a card header using Open Props design tokens in standard CSS */
.card-header {
  font-size: var(--font-size-4);
  font-weight: var(--font-weight-7);
  padding: var(--size-3) var(--size-4);
  box-shadow: var(--shadow-3);
  color: var(--blue-7);
}

How they compare

FeatureTailwind CSSOpen Props
Core philosophyUtility-first (classes in HTML markup)Design tokens (native CSS variables)
Styling locationClass attributes on elementsTraditional stylesheets (CSS/Sass)
Learning curveHigh (memorizing shorthand classes)Low (leverages native CSS knowledge)
Build overheadRequires compilation step to purge unused classesZero build step required (can run natively via CDN/imports)
CustomizationConfiguration file (@theme or tailwind.config.js)Overriding standard CSS variable values

Summary: When to use what

Ultimately, the choice between these styling approaches depends on your project’s specific requirements and your team’s workflow.

  • Use Tailwind CSS if you are building application-driven, component-focused projects (e.g., React, Svelte, Vue, or Astro) where styling is encapsulated, or if you need a preconfigured design system scale to move rapidly without naming fatigue.
  • Use Open Props if you want to write standard CSS stylesheets but want a pre-built, high-quality scale of design tokens (spacing, shadows, colors, and animation easing) without setting them up from scratch or requiring a build compiler.
  • Use vanilla CSS if you prefer a clean separation of concerns, want to maximize native browser APIs (such as @container queries or CSS nesting) without build-time dependency overhead, or are building simple document-centric websites.

Conclusion

The modern CSS ecosystem has never been more powerful. Whether you choose the rapid, component-driven velocity of Tailwind CSS, the tokenized structure of Open Props, or the immense flexibility of modern vanilla CSS, you now have the tools to build beautiful, responsive user interfaces faster than ever before.

Utility-first frameworks like Tailwind aren’t about discarding traditional CSS knowledge; rather, they rely heavily on your understanding of the underlying platform. By mastering the core concepts of layout, typography, and styling, you can confidently navigate this ecosystem and choose the right abstraction for your next project. Happy styling!