Design

Mastering Tailwind CSS for Modern Web Design

Discover advanced Tailwind CSS techniques to create beautiful and responsive web designs.

Plake
Feb 20, 20266 min read1,156 words
Mastering Tailwind CSS for Modern Web Design

Tailwind CSS flips the traditional CSS mental model on its head. Instead of writing semantic class names that map to custom styles, you compose small, single-purpose utility classes directly in your HTML. The result is a design system with almost no custom CSS — and a codebase that a new developer can read and change without fear of unexpected side effects. This guide covers the fundamentals and the techniques that will make you genuinely fast with Tailwind.

Why Utility-First?

The core promise: you never leave your markup to style it.

Traditional approach:

<!-- HTML -->
<button class="btn btn-primary">Save</button>

/* CSS — somewhere else */ .btn { padding: 0.5rem 1rem; border-radius: 0.375rem;
font-weight: 600; } .btn-primary { background: #6366f1; color: white; }
.btn-primary:hover { background: #4f46e5; }

Tailwind approach:

<button
  class="rounded-md bg-indigo-500 px-4 py-2 font-semibold text-white hover:bg-indigo-600"
>
  Save
</button>

The style lives next to the HTML. No context switching, no naming gymnastics, no cascading side effects. The trade-off is verbose class strings — a cost most teams find manageable once they learn the naming system.

Installation

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Configure content sources in tailwind.config.ts:

import type { Config } from "tailwindcss";

export default {
  content: [
    "./app/**/*.{ts,tsx}",
    "./components/**/*.{ts,tsx}",
    "./content/**/*.mdx",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
} satisfies Config;

Add to your global CSS:

@tailwind base;
@tailwind components;
@tailwind utilities;

Tailwind scans the files in content and only ships the classes you actually use. A production bundle is typically 5–15 kB.

Core Concepts

Spacing

Tailwind’s spacing scale is based on a 4px unit. p-1 = 4px, p-4 = 16px, p-8 = 32px.

<div class="m-4 space-y-4 p-6">
  <!-- m-4 = margin: 16px -->
  <!-- p-6 = padding: 24px -->
  <!-- space-y-4 = 16px gap between children -->
</div>

Typography

<h1 class="text-4xl font-bold tracking-tight text-gray-900">Heading One</h1>
<p class="text-base leading-7 text-gray-600">
  Body text with comfortable line height.
</p>
<span class="text-sm font-medium tracking-widest text-indigo-600 uppercase">
  Label
</span>

Flexbox and Grid

<!-- Horizontal navbar -->
<nav class="flex items-center justify-between px-6 py-4">
  <Logo />
  <div class="flex items-center gap-4">
    <a href="/blog">Blog</a>
    <a href="/about">About</a>
  </div>
</nav>

<!-- 3-column grid, 1-column on mobile -->
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
  {cards.map(card => <Card key="{card.id}" {...card} />)}
</div>

Responsive Design

Tailwind uses mobile-first breakpoint prefixes. A class without a prefix applies at all sizes; a prefixed class overrides at that breakpoint and above.

Prefix Breakpoint
(none) All sizes
sm: ≥ 640px
md: ≥ 768px
lg: ≥ 1024px
xl: ≥ 1280px
2xl: ≥ 1536px
<!-- Stack on mobile, side-by-side on desktop -->
<div class="flex flex-col gap-8 lg:flex-row lg:items-start">
  <aside class="w-full lg:w-64 lg:shrink-0">…</aside>
  <main class="min-w-0 flex-1">…</main>
</div>

<!-- Font size scales up with viewport -->
<h1 class="text-2xl font-bold sm:text-3xl lg:text-5xl">Responsive Heading</h1>

State Variants

Apply styles conditionally based on element state:

<!-- Hover, focus, active -->
<button
  class="bg-indigo-500 hover:bg-indigo-600 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none active:scale-95"
>
  Click me
</button>

<!-- Form state -->
<input
  class="border border-gray-300 invalid:border-red-500 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500"
/>

<!-- Disabled -->
<button class="bg-indigo-500 disabled:cursor-not-allowed disabled:opacity-50">
  Submit
</button>

<!-- Parent state with group -->
<div class="group rounded-lg border p-4 hover:border-indigo-500">
  <h3 class="font-semibold group-hover:text-indigo-600">Card Title</h3>
  <p class="text-gray-500 group-hover:text-gray-700">Card body</p>
</div>

Dark Mode

Enable dark mode in your config:

export default {
  darkMode: "class", // toggles via a "dark" class on <html>
  // or "media" to follow the OS preference automatically
} satisfies Config;

Use dark: variants in markup:

<body class="bg-white text-gray-900 dark:bg-gray-950 dark:text-gray-100">
  <div class="rounded-lg bg-white shadow dark:bg-gray-800">
    <p class="text-gray-600 dark:text-gray-300">Card content</p>
  </div>
</body>

Toggle in JavaScript:

document.documentElement.classList.toggle("dark");

Custom Configuration

Extend Tailwind’s defaults with your brand colours, fonts, and spacing:

// tailwind.config.ts
export default {
  theme: {
    extend: {
      colors: {
        brand: {
          50: "#eff6ff",
          500: "#3b82f6",
          900: "#1e3a8a",
        },
      },
      fontFamily: {
        sans: ["Inter", "ui-sans-serif", "system-ui"],
        mono: ["JetBrains Mono", "ui-monospace"],
      },
      spacing: {
        18: "4.5rem",
        112: "28rem",
      },
      borderRadius: {
        "4xl": "2rem",
      },
    },
  },
} satisfies Config;

Custom values are available as first-class utilities: bg-brand-500, font-mono, p-18.

Arbitrary Values

Need a one-off value not in the scale? Use square brackets:

<div class="top-[117px] w-[calc(100%-2rem)] bg-[#1a1a2e]">
  Arbitrary positioning
</div>

<p class="text-[13px] leading-[1.6]">Fine-tuned typography</p>

Lean on these sparingly — overuse signals the design token is missing from your config.

Component Patterns with @apply

For repeated patterns across many files, extract them with @apply in CSS:

/* globals.css */
@layer components {
  .btn {
    @apply inline-flex items-center rounded-md px-4 py-2 text-sm font-medium transition-colors focus:ring-2 focus:ring-offset-2 focus:outline-none;
  }
  .btn-primary {
    @apply btn bg-indigo-600 text-white hover:bg-indigo-700 focus:ring-indigo-500;
  }
  .btn-ghost {
    @apply btn bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-500;
  }
}

Use sparingly. Heavy @apply use recreates the problem Tailwind solves. Prefer React components for reuse over CSS abstractions.

Plugins

The official Tailwind plugins add common patterns:

npm install -D @tailwindcss/typography @tailwindcss/forms @tailwindcss/aspect-ratio
import typography from "@tailwindcss/typography";
import forms from "@tailwindcss/forms";

export default {
  plugins: [typography, forms],
} satisfies Config;

The typography plugin provides prose classes for rendering rich markdown/HTML content beautifully:

<article class="prose prose-lg prose-indigo dark:prose-invert max-w-none">
  {/* markdown rendered to HTML */}
</article>

Tailwind CSS design system illustration

Useful VS Code Workflow

Install the Tailwind CSS IntelliSense extension. You get:

  • Autocomplete for class names.
  • Live CSS preview on hover.
  • Lint warnings for unknown classes.
  • Sorting with the prettier-plugin-tailwindcss (sorts classes in a consistent, canonical order):
npm install -D prettier prettier-plugin-tailwindcss
// .prettierrc
{ "plugins": ["prettier-plugin-tailwindcss"] }

Sorted classes are easier to scan for duplicates and easier to diff in pull requests.

Tips for Real Projects

Consistent sizing — decide early whether you use size-* (sets both width and height) or separate w-*/h-*. Tailwind 3.3+ has size-* — use it for icons and avatars.

Avoid !important overrides — if you find yourself reaching for !, something is wrong in the design token structure.

Component variants without a library — use cva (class-variance-authority) to express variants cleanly:

import { cva } from "class-variance-authority";

const button = cva("rounded-md font-semibold transition-colors", {
  variants: {
    intent: {
      primary: "bg-indigo-600 text-white hover:bg-indigo-700",
      ghost: "bg-transparent text-gray-700 hover:bg-gray-100",
    },
    size: {
      sm: "px-3 py-1.5 text-sm",
      md: "px-4 py-2 text-base",
      lg: "px-6 py-3 text-lg",
    },
  },
  defaultVariants: { intent: "primary", size: "md" },
});

// <Button intent="ghost" size="sm">Cancel</Button>

Closing Thoughts

Tailwind’s learning curve is front-loaded — the class names feel foreign for the first few hours, then click into place. Once they do, the speed is real: you style without leaving your component, redesigns happen in one file, and dead CSS is impossible because styles live with the markup. Invest a day in the config and the utility naming convention; the return comes back quickly.

Share:

About the author

The Plake Team

Writes long-form essays on craft, software, and the slow web. Curates the Sunday digest read by 88,000+ readers.

Read more

Keep reading

Related Articles

Back to AllBack to All
Building Accessible Web Applications

Mar 25, 2026

8 min

Building Accessible Web Applications

State Management with Redux Toolkit

Mar 22, 2026

8 min

State Management with Redux Toolkit