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:
- 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.
- 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
-
Install dependencies: Tailwind v4 CLI is distributed as a separate package:
npm install -D tailwindcss @tailwindcss/cli -
Add Tailwind import to your input CSS: In your main stylesheet (e.g.,
src/input.css), import Tailwind directly (v4 uses standard CSS@importinstead of@tailwinddirectives):@import "tailwindcss"; -
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 -
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
-
Install dependencies:
npm install -D tailwindcss(Note: Unlike full PostCSS setups, the standalone CLI does not require installing
postcssorautoprefixerdirectly.) -
Initialize configuration:
npx tailwindcss initThis generates
tailwind.config.js. -
Configure template paths (in
tailwind.config.js):/** @type {import('tailwindcss').Config} */ module.exports = { content: ["./src/**/*.{html,js,ts,jsx,tsx}"], theme: { extend: {}, }, plugins: [], } -
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; -
Run the build script:
npx tailwindcss -i ./src/input.css -o ./dist/output.css --watch -
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.)
-
Install dependencies: Install Tailwind and its official Vite plugin as development dependencies:
npm install -D tailwindcss @tailwindcss/vite -
Configure Vite: Add the Tailwind plugin to your
vite.config.jsorvite.config.ts:import { defineConfig } from 'vite' import tailwindcss from '@tailwindcss/vite' export default defineConfig({ plugins: [ tailwindcss(), ], }) -
Add Tailwind import to your CSS: Import Tailwind in your main CSS entrypoint (e.g.,
src/index.css):@import "tailwindcss"; -
Import the CSS file in your JavaScript/TypeScript entrypoint: Import the stylesheet at the top of your main script file (e.g.,
src/main.jsorsrc/index.js):import './index.css'; -
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 usetype="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 token | Rem value | Pixel value (at 16px base) | Mnemonic or visual size |
|---|---|---|---|
0 | 0rem | 0px | No spacing |
0.5 | 0.125rem | 2px | Micro spacing |
1 | 0.25rem | 4px | Tiny (thin line gap) |
2 | 0.5rem | 8px | Small gap / padding |
4 | 1rem | 16px | Standard body font size / base gap |
6 | 1.5rem | 24px | Standard page margin padding |
8 | 2rem | 32px | Large padding / header spacing |
12 | 3rem | 48px | Section margins |
16 | 4rem | 64px | Large 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
t→ Top (e.g.,mt-4→margin-top: 1rem;)b→ Bottom (e.g.,pb-2→padding-bottom: 0.5rem;)l→ Left (e.g.,pl-8→padding-left: 2rem;)r→ Right (e.g.,mr-1→margin-right: 0.25rem;)x→ X-axis (horizontal) (left and right) (e.g.,px-4→padding-left: 1rem; padding-right: 1rem;)y→ Y-axis (vertical) (top and bottom) (e.g.,my-6→margin-top: 1.5rem; margin-bottom: 1.5rem;)s→ Start (Logical left in LTR layouts) (e.g.,ms-2→margin-inline-start: 0.5rem;)e→ End (Logical right in LTR layouts) (e.g.,pe-3→padding-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 class | Native CSS equivalent | Memory anchor |
|---|---|---|
block | display: block; | Plain English |
flex | display: flex; | Plain English |
grid | display: grid; | Plain English |
hidden | display: none; | ”Hide” the element |
relative | position: relative; | Plain English |
absolute | position: absolute; | Plain English |
fixed | position: fixed; | Plain English |
z-[val] | z-index: [val]; | z index coordinates |
Typography
| Tailwind class | Native CSS equivalent | Memory anchor |
|---|---|---|
text-center | text-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-bold → 700) |
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
rounded→border-radius. * Scale:rounded-sm(2px),rounded(4px),rounded-md(6px),rounded-lg(8px),rounded-xl(12px),rounded-2xl(16px),rounded-full(9999px).shadow→box-shadow. * Scale:shadow-sm,shadow(base),shadow-md,shadow-lg,shadow-xl,shadow-2xl,shadow-inner.opacity→opacity. * Scale: Percentage values (opacity-0toopacity-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
- Don’t Guess Colors, Guess Hue and Value:
- Tailwind color shades range from
50(lightest) →950(darkest), with500being the exact pure hue. - If you need a text color that is readable on a white background, aim for
700to950(e.g.,text-slate-800). - If you need a border color, aim for
100or200(e.g.,border-slate-200).
- Tailwind color shades range from
- 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;
- 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:
- 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).
- Correct:
- Order modifiers from broad constraints to specific interactions:
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(withsm:max-w-mdfor larger viewports) control the maximum width,mx-autocenters the container, andflexwithspace-x-3handles alignment and spacing. - Colors and theming: The
bg-white dark:bg-slate-800utility switches the background color based on system light or dark preferences. - Borders and corners: The
rounded-2xlclass creates large rounded corners, andborder-slate-100adds a subtle boundary line. - Transitions: Utilities like
transition-all duration-300 hover:shadow-2xl hover:-translate-y-1create a smooth lift-and-shadow effect when hovering over the card. - Glassmorphism: The
bg-white/80 backdrop-blur-mdclasses 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
- No config file: Tailwind automatically builds your classes and configuration directly from imports and standard CSS syntax.
- 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. - 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
- Design tokens with CSS custom properties: Instead of relying on
tailwind.config.jsor Tailwind’s@themedirective, you can use vanilla CSS variables. By styling elements withvar(--color-primary), you get the same design constraint system. - 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.
- 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:
- 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-actionwhile 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. - 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. - 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.
- Utility-level encapsulation: Although modern CSS supports native scoping via the
@scopeat-rule (letting you isolate styles to specific DOM subtrees) and specificity management via@layerdirectives, 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
| Feature | Tailwind CSS | Open Props |
|---|---|---|
| Core philosophy | Utility-first (classes in HTML markup) | Design tokens (native CSS variables) |
| Styling location | Class attributes on elements | Traditional stylesheets (CSS/Sass) |
| Learning curve | High (memorizing shorthand classes) | Low (leverages native CSS knowledge) |
| Build overhead | Requires compilation step to purge unused classes | Zero build step required (can run natively via CDN/imports) |
| Customization | Configuration 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
@containerqueries 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!
