Skip to content
AyoKoding

Advanced

This advanced tutorial covers expert Tailwind CSS patterns through 25 heavily annotated examples. Each example maintains 1-2.25 comment lines per code line and addresses production-scale engineering: plugin authoring, design system architecture, Tailwind v4, and migration strategies.

Prerequisites

Before starting, ensure you understand:

  • All beginner and intermediate concepts
  • Node.js and npm module system
  • CSS custom properties and cascade layers
  • JavaScript functions and closures

Group 1: Custom Plugins

Example 56: Writing a Custom Plugin

Tailwind plugins use the plugin function to add utilities, components, and base styles programmatically. This enables sharing reusable custom utilities across projects.

// tailwind.config.js
const plugin = require("tailwindcss/plugin");
// => plugin: Tailwind's plugin factory function
 
module.exports = {
  content: ["./src/**/*.{html,js,ts}"],
  plugins: [
    // => Custom plugin using plugin() factory
    plugin(function ({ addUtilities, addComponents, theme, e }) {
      // => addUtilities: registers new utility classes
      // => addComponents: registers new component classes (lower specificity)
      // => theme: reads values from tailwind config theme
      // => e: CSS identifier escaping function
 
      // => Add custom utilities
      addUtilities({
        ".text-shadow-sm": {
          textShadow: "1px 1px 2px rgb(0 0 0 / 0.1)",
          // => Adds text-shadow-sm utility: subtle text shadow
        },
        ".text-shadow": {
          textShadow: "2px 2px 4px rgb(0 0 0 / 0.15)",
          // => Adds text-shadow utility: moderate text shadow
        },
        ".text-shadow-lg": {
          textShadow: "4px 4px 8px rgb(0 0 0 / 0.2)",
          // => Adds text-shadow-lg utility: pronounced text shadow
        },
        ".text-shadow-none": {
          textShadow: "none",
          // => Adds text-shadow-none utility: removes text shadow
        },
      });
 
      // => Add component class using theme values
      addComponents({
        ".input-field": {
          display: "block",
          width: "100%",
          padding: `${theme("spacing.2")} ${theme("spacing.3")}`,
          // => theme('spacing.2'): reads p-2 value (0.5rem) from config
          border: `1px solid ${theme("colors.gray.300")}`,
          // => theme('colors.gray.300'): reads gray-300 color (#d1d5db)
          borderRadius: theme("borderRadius.lg"),
          // => theme('borderRadius.lg'): reads rounded-lg value (0.5rem)
          fontSize: theme("fontSize.sm")[0],
          // => theme('fontSize.sm')[0]: sm size value (0.875rem)
          "&:focus": {
            outline: "none",
            borderColor: theme("colors.blue.500"),
            // => Focus state built into component
          },
        },
      });
    }),
  ],
};

Using custom plugin utilities in HTML:

<!-- => Custom plugin utilities work exactly like built-in Tailwind utilities -->
<h1 class="text-4xl font-bold text-gray-900 text-shadow-sm">
  <!-- => text-shadow-sm: applies textShadow: '1px 1px 2px rgb(0 0 0 / 0.1)' -->
  Heading with Text Shadow
</h1>
 
<input type="text" class="input-field" placeholder="Uses plugin component class" />
<!-- => input-field: applies all block/width/padding/border styles from plugin -->

Key Takeaway: Use addUtilities for single-purpose CSS properties and addComponents for multi-property patterns. Access design tokens via theme() to keep plugins synchronized with config values.

Why It Matters: Custom plugins are how teams extend Tailwind beyond its built-in utilities without resorting to arbitrary values or custom CSS files. Text shadow, scrollbar styling, animation presets, and layout helpers are common plugin candidates. The theme() function inside plugins keeps custom components synchronized with the design system - if spacing.2 changes, the input-field component updates automatically. Plugin authoring is the gateway to publishing shared utilities on npm, enabling cross-project design system consistency. Major open-source plugins (@tailwindcss/forms, @tailwindcss/typography) follow exactly this pattern.

Example 57: Plugin with Dynamic Values from Theme

Advanced plugins generate utilities dynamically from theme values, creating utility families that respond to configuration just like built-in utilities.

// tailwind.config.js
const plugin = require("tailwindcss/plugin");
 
module.exports = {
  theme: {
    extend: {
      // => Define theme values that the plugin will consume
      textShadow: {
        sm: "0 1px 2px var(--tw-shadow-color)",
        // => sm variant: subtle shadow
        DEFAULT: "0 2px 4px var(--tw-shadow-color)",
        // => DEFAULT: used when no variant specified (text-shadow)
        lg: "0 8px 16px var(--tw-shadow-color)",
        // => lg variant: pronounced shadow
      },
    },
  },
  plugins: [
    plugin(function ({ matchUtilities, theme }) {
      // => matchUtilities: generates utilities matching pattern with values
      // => Replaces addUtilities for dynamic value generation
 
      matchUtilities(
        {
          "text-shadow": (value) => ({
            // => 'text-shadow': the utility prefix class name
            "--tw-shadow-color": "rgb(0 0 0 / 0.15)",
            // => CSS variable for shadow color (overridable per-use)
            textShadow: value,
            // => value: from theme.textShadow object (sm, DEFAULT, lg)
          }),
        },
        { values: theme("textShadow") },
        // => values: reads from theme.textShadow to generate text-shadow-sm, text-shadow-lg
      );
    }),
  ],
};

Using dynamic plugin utilities:

<!-- => Dynamic plugin utilities with all theme variants -->
<h1 class="text-4xl text-shadow">Default shadow (from textShadow.DEFAULT)</h1>
<!-- => text-shadow: textShadow: '0 2px 4px var(--tw-shadow-color)' -->
 
<h2 class="text-2xl text-shadow-sm">Small shadow</h2>
<!-- => text-shadow-sm: textShadow: '0 1px 2px var(--tw-shadow-color)' -->
 
<h3 class="text-xl text-shadow-lg">Large shadow</h3>
<!-- => text-shadow-lg: textShadow: '0 8px 16px var(--tw-shadow-color)' -->
 
<!-- => Arbitrary values work with matchUtilities too -->
<p class="text-shadow-[0_4px_6px_rgb(0_0_0_/0.3)]">Custom shadow</p>
<!-- => text-shadow-[value]: arbitrary value syntax works with matchUtilities -->

Key Takeaway: Use matchUtilities instead of addUtilities when the utility should support arbitrary values [] and theme-based variants. Define the values in theme.extend to keep them configurable.

Why It Matters: matchUtilities generates the complete JIT experience for custom utilities. With addUtilities, only the explicitly listed values work. With matchUtilities, users get text-shadow-sm, text-shadow-lg, arbitrary text-shadow-[value], responsive md:text-shadow-lg, and dark mode dark:text-shadow-sm - all automatically. This is how @tailwindcss/typography, @tailwindcss/forms, and the Tailwind team's own internal plugins work. Plugin authors publishing to npm should always use matchUtilities for theme-driven utilities to ensure full compatibility with Tailwind's variant system and JIT engine.

Group 2: Tailwind v4 CSS-First Configuration

Example 58: Tailwind v4 @import and CSS-First Setup

Tailwind v4 introduces CSS-first configuration via @import "tailwindcss" and @theme directive, eliminating the need for tailwind.config.js in most cases.

/* app.css - Tailwind v4 configuration via CSS */
 
/* => @import "tailwindcss": loads all Tailwind CSS functionality */
/* => Replaces @tailwind base; @tailwind components; @tailwind utilities; */
@import "tailwindcss";
 
/* => @theme: defines design tokens directly in CSS */
/* => Replaces tailwind.config.js theme section */
@theme {
  /* => Custom colors as CSS variables */
  --color-brand-50: #eff6ff;
  /* => bg-brand-50, text-brand-50, border-brand-50 all work */
  --color-brand-500: #3b82f6;
  /* => bg-brand-500: becomes primary brand blue */
  --color-brand-900: #1e3a8a;
  /* => text-brand-900: dark brand blue */
 
  /* => Custom spacing */
  --spacing-18: 4.5rem;
  /* => p-18, m-18, w-18, h-18: all work (72px) */
 
  /* => Custom font families */
  --font-sans: "Inter", system-ui, sans-serif;
  /* => font-sans: overrides default sans-serif */
  --font-display: "Playfair Display", Georgia, serif;
  /* => font-display: custom display font */
 
  /* => Custom border radius */
  --radius-card: 0.75rem;
  /* => rounded-card: border-radius: 0.75rem */
}
 
/* => @source: points to files to scan for class detection */
/* => Replaces content[] in tailwind.config.js */
@source "./src/**/*.{html,js,ts,jsx,tsx}";
 
/* => Custom utilities still work in v4 */
@utility text-balance {
  /* => @utility: defines single-purpose utility class */
  text-wrap: balance;
  /* => text-balance: text-wrap: balance (experimental text balancing) */
}
 
/* => Custom component classes */
@layer components {
  .card {
    @apply bg-white rounded-card shadow-sm border border-gray-100 p-6;
    /* => rounded-card: uses --radius-card custom token */
  }
}

Using v4 CSS-first configuration in HTML:

<!-- => All custom tokens work as utilities automatically -->
<div class="bg-brand-50 border border-brand-500/30 rounded-card p-6">
  <!-- => bg-brand-50: from --color-brand-50 in @theme -->
  <!-- => border-brand-500/30: 30% opacity brand border -->
  <!-- => rounded-card: from --radius-card in @theme -->
 
  <h2 class="font-display text-brand-900 text-2xl font-bold">
    <!-- => font-display: from --font-display in @theme -->
    <!-- => text-brand-900: from --color-brand-900 in @theme -->
    Tailwind v4 Configuration
  </h2>
 
  <p class="text-balance text-gray-600 mt-2">
    <!-- => text-balance: from @utility text-balance -->
    Balanced text rendering for headings and short paragraphs.
  </p>
</div>

Key Takeaway: Tailwind v4 replaces tailwind.config.js with @theme in CSS files. Define tokens as --color-{name}, --spacing-{name}, --font-{name} CSS variables. Use @source for file scanning and @utility for custom utilities.

Why It Matters: Tailwind v4 represents a fundamental architectural shift: configuration moves from JavaScript to CSS. This eliminates the Node.js build step requirement for configuration changes, enables native CSS cascade and inheritance for theme values, and allows CSS variables to be read and modified at runtime without a rebuild. For teams with designers who write CSS but not JavaScript, v4's CSS-first approach dramatically lowers the barrier to contributing to the design system. The performance improvements in v4 (5x faster builds, 100x faster incremental builds) make it the future of Tailwind for all production projects. Understanding both v3 (config.js) and v4 (@theme) is essential for the next 2-3 years of transition.

Example 59: Tailwind v4 @theme Directive and Design System Tokens

The @theme directive in v4 creates a centralized design token registry in CSS, replacing scattered config values with a single source of truth that both CSS and JavaScript can consume.

/* design-tokens.css */
 
/* => Central design token file imported everywhere */
@import "tailwindcss";
 
@theme {
  /* ============================================= */
  /* COLOR SYSTEM */
  /* ============================================= */
 
  /* => Brand color scale (all 11 shades) */
  --color-brand-50: oklch(97.3% 0.012 251);
  /* => oklch: perceptually uniform color space for v4 */
  /* => bg-brand-50: very light brand tint */
  --color-brand-100: oklch(93.8% 0.025 251);
  --color-brand-200: oklch(87.5% 0.049 251);
  --color-brand-300: oklch(79.6% 0.083 251);
  --color-brand-400: oklch(70.6% 0.114 251);
  --color-brand-500: oklch(61.2% 0.147 251);
  /* => bg-brand-500: primary brand color */
  --color-brand-600: oklch(53% 0.151 251);
  --color-brand-700: oklch(44.4% 0.142 251);
  --color-brand-800: oklch(36.2% 0.119 251);
  --color-brand-900: oklch(28.5% 0.092 251);
  --color-brand-950: oklch(20.3% 0.058 251);
  /* => text-brand-950: darkest brand for high-contrast text */
 
  /* => Semantic color aliases */
  --color-primary: var(--color-brand-600);
  /* => bg-primary: resolves to brand-600 */
  --color-primary-hover: var(--color-brand-700);
  /* => hover:bg-primary-hover: resolves to brand-700 */
 
  /* ============================================= */
  /* SPACING SYSTEM */
  /* ============================================= */
 
  /* => Extends default spacing scale */
  --spacing-4-5: 1.125rem;
  /* => p-4-5: between p-4 (1rem) and p-5 (1.25rem) */
  --spacing-13: 3.25rem;
  /* => h-13: 52px (not in default scale) */
  --spacing-15: 3.75rem;
  /* => mt-15: 60px for section separation */
 
  /* ============================================= */
  /* SHADOW SYSTEM */
  /* ============================================= */
 
  /* => Custom named shadows */
  --shadow-card: 0 1px 3px 0 rgb(0 0 0 / 0.07), 0 1px 2px -1px rgb(0 0 0 / 0.05);
  /* => shadow-card: refined card shadow from design spec */
  --shadow-modal: 0 25px 50px -12px rgb(0 0 0 / 0.25);
  /* => shadow-modal: deep shadow for floating dialogs */
}

Key Takeaway: Use OKLCH color space in v4 for perceptually uniform color scales. Define semantic aliases (--color-primary) pointing to scale values (--color-brand-600). Semantic aliases decouple usage from specific scale values.

Why It Matters: The shift to OKLCH in v4 is significant: OKLCH provides perceptually uniform color relationships, meaning the difference between brand-400 and brand-500 appears the same as between brand-800 and brand-900 visually. This eliminates the "dark colors look too dark / light colors look too light" problem common in traditional hex-based color scales. Semantic aliases (primary, primary-hover) enable theme switching - changing --color-primary from brand-600 to green-600 updates all primary colors application-wide. OKLCH also enables mathematically generating entire color scales from a single hue value, enabling programmatic design system generation for multi-tenant applications with per-tenant brand colors.

Group 3: JIT Engine and Performance

Example 60: Understanding Content Scanning and Purging

Tailwind's JIT (Just-In-Time) engine scans content files to determine which classes to include. Incorrect content configuration leads to missing styles in production.

// tailwind.config.js - Production-ready content configuration
module.exports = {
  content: [
    // => CRITICAL: every file type that uses Tailwind classes must be listed
    "./src/**/*.{html,js,ts,jsx,tsx,vue,svelte}",
    // => Covers React, Vue, Svelte, vanilla HTML/JS templates
 
    // => Third-party library components (if they use Tailwind classes)
    "./node_modules/@your-ui-library/**/*.js",
    // => Include if your component library ships with Tailwind classes in JS
 
    // => Dynamic class generation from data files
    "./src/data/**/*.json",
    // => If classes are stored in JSON config files (common in CMS setups)
 
    // => Markdown content files
    "./content/**/*.{md,mdx}",
    // => Blog posts and documentation that include HTML with classes
  ],
 
  // => safelist: force-include classes even if not found in content
  safelist: [
    // => Static strings: always included
    "bg-red-500",
    // => Never purged even if not used in content files
 
    // => Pattern-based safelisting
    { pattern: /^bg-(red|green|blue)-(100|200|300)$/ },
    // => Includes bg-red-100, bg-red-200, ..., bg-blue-300 matching the pattern
    // => Use for dynamically constructed classes in runtime (from API data)
 
    { pattern: /^text-(sm|base|lg|xl)$/, variants: ["hover", "md"] },
    // => Includes responsive and hover variants for these text sizes
    // => variants: ['hover', 'md'] adds hover:text-sm, md:text-base, etc.
  ],
};

What gets purged and why:

// => PROBLEM: Dynamic class construction prevents detection
const color = "red";
const element = `<div class="bg-${color}-500">`;
// => bg-red-500 is NOT a static string - JIT scanner misses it
// => Solution: use safelist or full class strings
 
// => SOLUTION 1: Use complete class strings
const colorMap = {
  red: "bg-red-500",
  // => 'bg-red-500': complete string, scanner finds it
  blue: "bg-blue-500",
  green: "bg-green-500",
};
const element = `<div class="${colorMap[color]}">`;
 
// => SOLUTION 2: Safelist the pattern
// => In config: safelist: [{ pattern: /^bg-(red|blue|green)-500$/ }]

Key Takeaway: The JIT scanner uses static string analysis - it finds class names as literal strings in source files. Never construct class names with string concatenation; always use complete class strings or safelist dynamic patterns.

Why It Matters: The "my styles work locally but disappear in production" bug is almost always a content scanning issue. Developers debug for hours not realizing that bg-${color}-500 is invisible to the scanner. This is the #1 Tailwind gotcha in production. The solution - mapping dynamic values to complete class strings - also has a design benefit: it makes your entire color/variant set explicit and reviewable in one place. Safelisting is the escape hatch for external data sources (CMS, API) that define styles - a blog post with category colors set in a database needs those colors safelisted. Understanding purging is essential for every production Tailwind deployment.

Example 61: Bundle Size Analysis and Optimization

Understanding the relationship between content files, class usage, and CSS bundle size enables data-driven optimization decisions.

# => Analyze Tailwind CSS output size
npx tailwindcss -i ./src/input.css -o ./dist/output.css
# => Generates output CSS file; check file size with:
ls -lh ./dist/output.css
# => Typical production build: 10-30KB (with purging) vs 4MB (without purging)

Optimization strategies in configuration:

// tailwind.config.js
module.exports = {
  content: ["./src/**/*.{html,ts,tsx}"],
 
  // => corePlugins: disable unused utility categories
  corePlugins: {
    // => Disable animation utilities if not using them
    animation: false,
    // => Removes all animate-* utilities from output (~2KB)
 
    // => Disable if not using text columns
    columns: false,
    // => Removes columns-* utilities
 
    // => Disable float if using modern flexbox/grid
    float: false,
    // => Removes float-* utilities (~1KB)
  },
 
  // => Theme: remove unused values from scales
  theme: {
    // => Override (not extend) to constrain the spacing scale
    spacing: {
      0: "0",
      1: "0.25rem",
      2: "0.5rem",
      3: "0.75rem",
      4: "1rem",
      5: "1.25rem",
      6: "1.5rem",
      8: "2rem",
      10: "2.5rem",
      12: "3rem",
      16: "4rem",
      20: "5rem",
      24: "6rem",
      32: "8rem",
      40: "10rem",
      48: "12rem",
      64: "16rem",
      96: "24rem",
      // => Only the spacing values your team actually uses
    },
  },
};

Key Takeaway: Production optimization sequence: (1) verify content paths are minimal and precise, (2) disable unused core plugins, (3) constrain default scales to values actually used, (4) measure before and after each change.

Why It Matters: CSS bundle size directly impacts Lighthouse performance scores, Core Web Vitals, and user experience on slow connections. The render-blocking nature of CSS makes even 10KB differences significant. Most production apps use only 20-30% of Tailwind's default utility set. Disabling unused core plugins (animation, float, columns, aspect-ratio on v3) and constraining spacing/color scales yields 30-50% CSS size reduction beyond purging. This matters most for performance-critical pages: landing pages, checkout flows, and mobile experiences where every kilobyte is felt by users on 3G connections. Measure with Lighthouse before and after optimization to quantify the impact.

Example 62: Production Build Pipeline Integration

Integrating Tailwind into production build pipelines requires understanding PostCSS, CSS minification, and sourcemap generation for debugging.

// postcss.config.js
// => PostCSS processes CSS through a plugin pipeline
 
module.exports = {
  plugins: {
    // => tailwindcss: generates utility CSS from classes found in content
    tailwindcss: {},
    // => Uses tailwind.config.js automatically
 
    // => autoprefixer: adds vendor prefixes for browser compatibility
    autoprefixer: {},
    // => Adds -webkit-, -moz- prefixes where needed
    // => Required for production (browser support)
 
    // => cssnano: minifies CSS for production (optional, framework may handle)
    ...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {}),
    // => Only minifies in production (preserves readable CSS in development)
    // => cssnano removes whitespace, comments, and optimizes values
  },
};

Vite integration:

// vite.config.ts
import { defineConfig } from "vite";
// => Vite's built-in PostCSS support handles Tailwind automatically
 
export default defineConfig({
  css: {
    // => postcssOptions: passed to PostCSS
    postcss: {
      plugins: [
        require("tailwindcss"),
        // => Include Tailwind in Vite's PostCSS pipeline
        require("autoprefixer"),
        // => Browser compatibility prefixes
      ],
    },
  },
  build: {
    cssCodeSplit: true,
    // => Splits CSS per chunk for better caching
    // => Route-level CSS splitting reduces initial bundle size
  },
});

Key Takeaway: Use PostCSS pipeline with tailwindcss + autoprefixer in all environments. Add cssnano for production minification. Leverage CSS code splitting in Vite/Next.js/Nuxt for per-route CSS loading.

Why It Matters: Build pipeline correctness is what separates development-only Tailwind from production-ready Tailwind. Missing autoprefixer breaks layout on Safari (still requires some prefixes). Missing cssnano sends 25KB of whitespace to production. CSS code splitting prevents loading all application CSS on the landing page - Next.js does this automatically, Vite requires explicit configuration. Understanding the PostCSS plugin execution order (tailwindcss first, then autoprefixer) prevents CSS that references non-existent utility-generated classes. Production deployments at scale (10K+ monthly active users) treat build pipeline correctness as a reliability requirement, not an optimization.

Group 4: Accessibility with Tailwind

Example 63: WCAG AA Compliance Patterns

Accessible UI requires proper color contrast, focus indicators, and semantic HTML. Tailwind utilities enforce these requirements when used with discipline.

<!-- => Accessibility-first component patterns -->
<div class="space-y-6 p-6">
  <!-- === Color contrast: WCAG AA requires 4.5:1 for normal text === -->
 
  <!-- FAIL: Insufficient contrast -->
  <!-- <p class="text-gray-400 bg-white"> gray-400 on white = 2.77:1 ratio (FAIL) -->
 
  <!-- PASS: Sufficient contrast -->
  <p class="text-gray-700 bg-white">
    <!-- => text-gray-700 on white: contrast ratio 5.74:1 (PASS AA + AAA) -->
    Body text with accessible contrast ratio
  </p>
 
  <!-- === Focus indicators: WCAG 2.1 SC 2.4.7 === -->
  <div class="space-y-3">
    <!-- FAIL pattern: removed focus without replacement -->
    <!-- <button class="focus:outline-none"> ...missing focus ring = WCAG FAIL -->
 
    <!-- PASS pattern: custom focus ring replacing default -->
    <button
      class="bg-blue-600 text-white px-4 py-2 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white"
    >
      <!-- => focus:outline-none: removes browser default (acceptable only with replacement) -->
      <!-- => focus-visible:ring-2: 2px ring appears for keyboard users -->
      <!-- => focus-visible:ring-offset-2: 2px gap separates ring from button -->
      <!-- => focus-visible:ring-offset-white: gap color = page background -->
      Accessible Button
    </button>
  </div>
 
  <!-- === Screen reader utilities === -->
  <div class="flex items-center gap-3">
    <button class="bg-red-600 hover:bg-red-700 text-white p-2 rounded-lg" aria-label="Delete item">
      <!-- => aria-label: provides text description for screen readers -->
      <!-- => Without aria-label, icon-only button is inaccessible -->
      🗑
    </button>
 
    <!-- => sr-only: visually hidden but accessible to screen readers -->
    <label class="sr-only" for="search-input">Search</label>
    <!-- => sr-only: position: absolute; width: 1px; height: 1px; overflow: hidden -->
    <!-- => Screen readers announce "Search" before the input -->
    <input
      id="search-input"
      type="search"
      placeholder="Search..."
      class="border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
    />
 
    <!-- => not-sr-only: reverses sr-only (shows element again) -->
    <!-- <span class="sr-only md:not-sr-only">Show on desktop</span> -->
  </div>
 
  <!-- === Color not as sole indicator === -->
  <div class="space-y-2">
    <!-- FAIL: Color-only error indication -->
    <!-- <input class="border-red-500"> (no text, no icon - fails for color-blind users) -->
 
    <!-- PASS: Color + text + icon -->
    <div>
      <input
        class="border-red-500 border-2 rounded-lg px-3 py-2 w-full focus:outline-none"
        type="email"
        value="invalid"
      />
      <!-- => border-red-500: red border (visual color cue) -->
      <p class="text-red-600 text-sm mt-1 flex items-center gap-1">
        <!-- => text-red-600: text color (redundant color cue) -->
        <span aria-hidden="true">⚠</span>
        <!-- => aria-hidden="true": icon is decorative, screen readers skip it -->
        <span>Please enter a valid email address</span>
        <!-- => Descriptive text: the actual accessible error message -->
      </p>
    </div>
  </div>
</div>

Key Takeaway: WCAG AA requires: 4.5:1 text contrast (use gray-700+ on white), visible focus indicators (never focus:outline-none without replacement), semantic HTML + ARIA, and non-color-only indicators for status.

Why It Matters: Web accessibility is both a legal requirement and ethical responsibility. In many jurisdictions, inaccessible websites violate disability discrimination laws (ADA in the US, WCAG in the EU Web Accessibility Directive). Beyond legal risk, 15% of the global population has some form of disability. Keyboard-only users (motor disabilities, power users), screen reader users (visual impairments), and color-blind users (8% of males) all need accessible interfaces. Tailwind utilities make accessibility possible but not automatic - sr-only, focus-visible:ring-2, focus:outline-none with ring replacement, and aria-* attributes must be applied deliberately. Building accessibility from the start costs 20% more than baseline; retrofitting costs 10x more.

Skip links and focus management are critical for keyboard users to navigate efficiently. Tailwind's position and focus utilities implement these patterns cleanly.

<!-- => Full page layout with accessibility features -->
<body class="bg-white">
  <!-- === Skip link: allows keyboard users to bypass navigation === -->
  <a
    href="#main-content"
    class="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-50 focus:bg-blue-600 focus:text-white focus:px-4 focus:py-2 focus:rounded-lg focus:shadow-lg"
  >
    <!-- => sr-only: hidden visually (1px x 1px, off-screen) -->
    <!-- => focus:not-sr-only: reverses sr-only when focused (Tab key) -->
    <!-- => focus:fixed: fixed positioning so it appears in viewport -->
    <!-- => focus:top-4 focus:left-4: positions in top-left corner -->
    <!-- => focus:z-50: above all other content -->
    Skip to main content
  </a>
  <!-- => First Tab on page shows this link; pressing Enter skips to #main-content -->
 
  <!-- Navigation -->
  <nav class="sticky top-0 z-40 bg-white border-b border-gray-200 px-6 py-4" aria-label="Main navigation">
    <!-- => aria-label: names the nav landmark for screen readers -->
    <!-- => sticky top-0 z-40: fixed navigation below z-50 skip link -->
    <div class="flex items-center justify-between max-w-7xl mx-auto">
      <a href="/" class="font-bold text-xl">Brand</a>
      <div class="flex gap-6" role="list">
        <!-- => role="list": redundant for ul but valid for div-based nav lists -->
        <a
          href="/about"
          role="listitem"
          class="text-gray-600 hover:text-gray-900 focus-visible:ring-2 focus-visible:ring-blue-500 rounded"
          >About</a
        >
        <a
          href="/contact"
          role="listitem"
          class="text-gray-600 hover:text-gray-900 focus-visible:ring-2 focus-visible:ring-blue-500 rounded"
          >Contact</a
        >
      </div>
    </div>
  </nav>
 
  <!-- Main content receives focus on skip link activation -->
  <main id="main-content" tabindex="-1" class="max-w-7xl mx-auto px-6 py-12 focus:outline-none">
    <!-- => id="main-content": skip link target -->
    <!-- => tabindex="-1": allows programmatic focus (skip link) without Tab order -->
    <!-- => focus:outline-none: hides focus ring on main (it's not interactive) -->
    <h1 class="text-3xl font-bold text-gray-900">Page Title</h1>
    <p class="text-gray-600 mt-4">Main content starts here.</p>
  </main>
</body>

Key Takeaway: Implement skip links using sr-only focus:not-sr-only focus:fixed focus:z-50. Mark navigation with aria-label. Give <main> an id and tabindex="-1" for programmatic focus on skip link activation.

Why It Matters: Skip links are required by WCAG 2.4.1 (Bypass Blocks) for Level A compliance - the baseline. Without them, keyboard users must Tab through every navigation item on every page to reach the content. On a nav with 10 links, that's 10 keypresses before reading a single word of content. The sr-only focus:not-sr-only pattern is the elegant CSS-only solution - the link exists in the DOM for screen readers but only becomes visible when keyboard-focused. tabindex="-1" on main enables focus to be programmatically moved to the content area when the skip link is activated, but removes it from normal Tab order (users don't accidentally Tab into the page body). These are foundational accessibility patterns that every production application needs.

Group 5: Container Queries

Example 65: Container Queries with @container

Container queries apply styles based on the parent container's size rather than the viewport size, enabling truly component-based responsive design.

// tailwind.config.js (v3 - requires plugin)
module.exports = {
  plugins: [
    require("@tailwindcss/container-queries"),
    // => Adds @container and @{size}: variants
  ],
};

Install the plugin:

npm install -D @tailwindcss/container-queries
# => Official Tailwind container queries plugin

Using container queries in HTML:

<!-- => Container queries: component adapts to its container size, not viewport -->
<div class="@container bg-gray-50 p-4">
  <!-- => @container: marks this element as a container query context -->
  <!-- => Child elements can respond to THIS div's width (not viewport width) -->
 
  <!-- Card adapts based on container width, not viewport width -->
  <div class="flex flex-col @md:flex-row gap-4 bg-white rounded-xl p-4 shadow-sm">
    <!-- => @md:flex-row: changes to horizontal when CONTAINER is 768px+ wide -->
    <!-- => This works even if viewport is 1200px - only container width matters -->
 
    <img
      src="https://via.placeholder.com/200"
      alt="Product thumbnail"
      class="w-full @md:w-32 @md:h-32 @md:shrink-0 rounded-lg object-cover"
    />
    <!-- => @md:w-32 @md:h-32: fixed dimensions when container is wide enough -->
    <!-- => @md:shrink-0: prevents image shrinking in horizontal layout -->
 
    <div class="space-y-2">
      <h3 class="font-semibold text-gray-900 @md:text-lg">
        <!-- => @md:text-lg: larger heading when container is wider -->
        Product Title
      </h3>
      <p class="text-gray-500 text-sm">Product description text</p>
      <span class="text-blue-600 font-bold @md:text-xl">$99.00</span>
      <!-- => @md:text-xl: larger price display in wide container -->
    </div>
  </div>
</div>
 
<!-- === Container queries enable the same component in different contexts === -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
  <!-- Narrow sidebar context (1/3 width): component stacks vertically -->
  <aside class="@container">
    <!-- => Same component in narrow sidebar: @md: breakpoints don't trigger -->
    <div class="flex flex-col @md:flex-row gap-4 bg-white rounded-xl p-4 shadow-sm">
      <img src="https://via.placeholder.com/200" alt="" class="w-full @md:w-32 rounded-lg object-cover" />
      <div><h3 class="font-semibold">Sidebar Card</h3></div>
    </div>
  </aside>
 
  <!-- Wide main area (2/3 width): same component in wide context -->
  <main class="@container col-span-2">
    <!-- => Same component in wide main: @md: breakpoints DO trigger -->
    <div class="flex flex-col @md:flex-row gap-4 bg-white rounded-xl p-4 shadow-sm">
      <img src="https://via.placeholder.com/200" alt="" class="w-full @md:w-32 rounded-lg object-cover" />
      <div><h3 class="font-semibold text-lg">Main Area Card</h3></div>
    </div>
  </main>
</div>

Key Takeaway: Mark a parent with @container and use @sm:, @md:, @lg: on children to respond to the container's width. The same component adapts differently based on where it's placed, not the viewport size.

Why It Matters: Container queries solve the fundamental problem with responsive design: viewport-based media queries can't know where a component will be placed. A product card in a 3-column grid needs different styling than the same component in a 2-column or 4-column grid. Before container queries, developers duplicated component code or used JavaScript to measure container widths. Container queries enable true component-level responsiveness - the component knows its own available space, not just the viewport. This is a paradigm shift for design system components: a single ProductCard component with @container adapts perfectly everywhere it's used. React/Vue/Angular component libraries will increasingly depend on container queries as they become universally supported.

Group 6: Custom Variants

Example 66: Custom Variant Creation

Custom variants extend Tailwind's variant system to apply utilities under conditions not covered by built-in variants. Use addVariant in plugins.

// tailwind.config.js
const plugin = require("tailwindcss/plugin");
 
module.exports = {
  content: ["./src/**/*.{html,js,ts}"],
  plugins: [
    plugin(function ({ addVariant }) {
      // => addVariant: registers a new variant
 
      // => Custom variant: applies when element is inside .rtl class
      addVariant("rtl", '[dir="rtl"] &');
      // => 'rtl': variant name (usage: rtl:text-right)
      // => '[dir="rtl"] &': CSS selector (&  = the utility's element)
      // => Result: [dir="rtl"] .rtl\:text-right { text-align: right; }
 
      // => Custom variant: applies when ARIA expanded is true
      addVariant("expanded", '&[aria-expanded="true"]');
      // => 'expanded': variant name (usage: expanded:rotate-180)
      // => '&[aria-expanded="true"]': self-selector with aria attribute
      // => Result: .expanded\:rotate-180[aria-expanded="true"] { transform: rotate(180deg); }
 
      // => Custom variant: applies to first 3 children
      addVariant("first-3", "&:nth-child(-n+3)");
      // => 'first-3': variant name (usage: first-3:border-t-0)
      // => Targets first 3 children: nth-child(-n+3)
 
      // => Custom variant for print media
      addVariant("print", "@media print");
      // => 'print': variant name (usage: print:hidden, print:text-black)
      // => '@media print': applies inside print media query
    }),
  ],
};

Using custom variants in HTML:

<!-- === RTL layout support === -->
<html dir="ltr">
  <!-- => dir="ltr": left-to-right (default) -->
  <div class="text-left rtl:text-right ml-4 rtl:mr-4 rtl:ml-0">
    <!-- => text-left: left-aligned in LTR -->
    <!-- => rtl:text-right: right-aligned when dir="rtl" on ancestor -->
    <!-- => rtl:mr-4 rtl:ml-0: flips horizontal margin for RTL -->
    Internationalized text direction
  </div>
</html>
 
<!-- === ARIA-based accordion toggle icon === -->
<button
  class="flex items-center justify-between w-full py-3"
  aria-expanded="false"
  onclick="this.setAttribute('aria-expanded', this.getAttribute('aria-expanded') === 'true' ? 'false' : 'true')"
>
  Accordion Item
  <span class="transition-transform duration-200 expanded:rotate-180">
    <!-- => expanded:rotate-180: rotates arrow when button has aria-expanded="true" -->
    <!-- => transition-transform: smooth rotation animation -->

  </span>
</button>
 
<!-- === Print styles === -->
<div class="bg-gray-100 p-4 rounded print:bg-white print:p-0 print:rounded-none">
  <!-- => print:bg-white: white background for printing (saves ink) -->
  <!-- => print:p-0: removes padding for print layout -->
  Content for screen and print
</div>
<nav class="print:hidden">
  <!-- => print:hidden: navigation disappears when printing -->
  Navigation (hidden when printing)
</nav>

Key Takeaway: Use addVariant for ARIA-driven state variants (expanded, selected, checked via aria attributes), RTL layout support, print media, and complex :nth-child selectors that built-in variants don't cover.

Why It Matters: Custom variants eliminate the last category of CSS patterns that Tailwind can't handle with built-in utilities. RTL support is required for Arabic, Hebrew, and Persian language markets - without the rtl: variant, supporting RTL requires duplicating all layout utilities with direction checks. ARIA-driven variants reduce JavaScript by keeping visual state synchronized with accessibility state rather than managing both separately. Print styles are frequently neglected but important for enterprise applications where users print reports and invoices. The power of custom variants is that they participate fully in Tailwind's variant system - rtl:hover:text-right, print:md:hidden all work as expected.

Group 7: Integrating with CSS Modules

Example 67: Tailwind with CSS Modules

CSS Modules provide component-scoped styles. Combining them with Tailwind allows local component styles that can't be achieved with utilities alone, while maintaining design token consistency.

/* Button.module.css */
/* => CSS Modules: styles are locally scoped to the component */
 
.button {
  /* => Use @apply for Tailwind utilities in CSS Module classes */
  @apply inline-flex items-center justify-center;
  /* => Composing Tailwind utilities into a scoped class */
  @apply px-4 py-2 rounded-lg font-medium text-sm;
  @apply transition-colors duration-200;
  @apply focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2;
 
  /* => Custom CSS not possible with Tailwind alone */
  background-image: linear-gradient(135deg, var(--tw-gradient-from), var(--tw-gradient-to));
  /* => Complex gradient using Tailwind's gradient CSS variables directly */
}
 
.buttonPrimary {
  /* => Variant class extending button */
  @apply bg-blue-600 text-white;
  /* => Inherits base button styles via composition in JSX */
  --tw-gradient-from: theme("colors.blue.500");
  /* => theme(): access Tailwind theme values in CSS Modules */
  --tw-gradient-to: theme("colors.blue.700");
}
 
.buttonPrimary:hover {
  /* => Hover state in CSS Module (complex selectors not in Tailwind utilities) */
  @apply bg-blue-700;
}
 
.shimmer {
  /* => Complex animation CSS that can't be done with utilities alone */
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
  /* => Custom animation keyframe (defined below) */
}
 
@keyframes shimmer {
  0% {
    background-position: 200% 0;
  }
  100% {
    background-position: -200% 0;
  }
}

Using in React component:

// Button.tsx
import styles from './Button.module.css';
// => CSS Modules import: styles.button gives unique hashed class name
 
interface ButtonProps {
  variant?: 'primary' | 'secondary';
  // => variant: determines which CSS Module class to apply
  children: React.ReactNode;
  className?: string;
  // => className: allows additional Tailwind utilities at call site
}
 
function Button({ variant = 'primary', children, className = '' }: ButtonProps) {
  return (
    <button
      className={`${styles.button} ${styles[`button${variant.charAt(0).toUpperCase() + variant.slice(1)}`]} ${className}`}
      // => styles.button: base button styles from CSS Module
      // => styles.buttonPrimary: variant styles from CSS Module
      // => className: caller-provided Tailwind utilities (w-full, mt-4, etc.)
    >
      {children}
    </button>
  );
}
 
// Usage:
// <Button variant="primary" className="w-full mt-4">Submit</Button>
// => w-full and mt-4 are Tailwind utilities applied alongside CSS Module classes

Key Takeaway: Use @apply inside CSS Modules to compose Tailwind utilities. Use theme() function to access design tokens. Allow additional Tailwind classes via className prop for flexibility at call sites.

Why It Matters: CSS Modules + Tailwind is the preferred pattern for complex component animations and styles that exceed what utility composition can achieve. Loading skeleton shimmer effects, complex gradients, and custom keyframe animations are CSS Module candidates. The theme() function keeps CSS Module values synchronized with Tailwind config - theme('colors.blue.600') automatically updates when config changes. This hybrid approach captures both worlds: design token consistency from Tailwind and the full power of CSS for complex scenarios. Monorepos with component libraries frequently use this pattern for their design system packages, shipping CSS Modules that consume the host app's Tailwind config values.

Group 8: shadcn/ui and Tailwind Integration

Example 68: Understanding shadcn/ui Architecture

shadcn/ui components are Tailwind-based, copied into your project (not imported from npm). Understanding how they use Tailwind enables customization and extension.

// tailwind.config.js for shadcn/ui
// => shadcn/ui requires specific Tailwind configuration
 
module.exports = {
  darkMode: ["class"],
  // => shadcn/ui uses class-based dark mode
  content: [
    "./src/**/*.{ts,tsx}",
    "./components/**/*.{ts,tsx}",
    // => Include shadcn/ui component files in content scanning
  ],
  theme: {
    extend: {
      colors: {
        // => shadcn/ui maps to CSS variables for theming
        background: "hsl(var(--background))",
        // => hsl(var(--background)): reads CSS variable --background
        // => bg-background: uses this CSS variable
        foreground: "hsl(var(--foreground))",
        // => text-foreground: the primary text color
 
        primary: {
          DEFAULT: "hsl(var(--primary))",
          // => bg-primary: primary brand color from CSS variable
          foreground: "hsl(var(--primary-foreground))",
          // => text-primary-foreground: text on primary backgrounds
        },
 
        muted: {
          DEFAULT: "hsl(var(--muted))",
          // => bg-muted: muted surface color
          foreground: "hsl(var(--muted-foreground))",
          // => text-muted-foreground: secondary text color
        },
 
        destructive: {
          DEFAULT: "hsl(var(--destructive))",
          // => bg-destructive: red/error color for delete actions
          foreground: "hsl(var(--destructive-foreground))",
        },
 
        border: "hsl(var(--border))",
        // => border-border: the default border color (shorthand naming)
        ring: "hsl(var(--ring))",
        // => ring-ring: the focus ring color
      },
      borderRadius: {
        lg: "var(--radius)",
        // => rounded-lg: maps to --radius CSS variable
        md: "calc(var(--radius) - 2px)",
        // => rounded-md: slightly smaller than --radius
        sm: "calc(var(--radius) - 4px)",
        // => rounded-sm: smallest rounded variant
      },
    },
  },
};

CSS variable definitions (globals.css):

/* globals.css */
@layer base {
  :root {
    /* => Light mode CSS variables */
    --background: 0 0% 100%;
    /* => background: white in HSL (0 hue, 0% saturation, 100% lightness) */
    --foreground: 222.2 84% 4.9%;
    /* => foreground: near black */
    --primary: 222.2 47.4% 11.2%;
    /* => primary: dark navy */
    --primary-foreground: 210 40% 98%;
    /* => primary-foreground: near white (text on primary) */
    --radius: 0.5rem;
    /* => radius: border-radius for lg (8px) */
  }
 
  .dark {
    /* => Dark mode overrides via .dark class */
    --background: 222.2 84% 4.9%;
    /* => background: near black in dark mode */
    --foreground: 210 40% 98%;
    /* => foreground: near white in dark mode */
    --primary: 210 40% 98%;
    /* => primary: near white (inverted in dark mode) */
  }
}

Key Takeaway: shadcn/ui maps Tailwind color utilities to CSS variables (hsl(var(--primary))). Changing --primary CSS variable instantly updates all bg-primary, text-primary, and ring-primary instances. Copy components into your project and customize freely.

Why It Matters: shadcn/ui has become one of the most popular React component libraries precisely because it integrates deeply with Tailwind's design token system. The CSS variable approach enables runtime theme switching, multi-tenant white-labeling, and dark mode without any rebuild. Because components are copied (not imported from npm), you own the code - you can modify button styles, change border radii, or add custom states without forking a library. Understanding the Tailwind config + CSS variable mapping is essential for customizing shadcn/ui beyond its defaults and for building your own similar design systems. This architecture pattern (Tailwind tokens pointing to CSS variables) is now a community standard and will be foundational for the next generation of component libraries.

Group 9: Migration Strategies

Example 69: Migrating from Traditional CSS to Tailwind

Migration from semantic CSS to Tailwind is a gradual process. Understanding the mapping from CSS declarations to utility classes enables confident incremental migration.

Before (traditional CSS):

/* styles.css - Traditional semantic CSS */
.card {
  background-color: #ffffff;
  /* => bg-white */
  border-radius: 0.75rem;
  /* => rounded-xl */
  padding: 1.5rem;
  /* => p-6 */
  box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
  /* => shadow-md */
  border: 1px solid #e5e7eb;
  /* => border border-gray-200 */
}
 
.card__title {
  font-size: 1.125rem;
  /* => text-lg */
  font-weight: 600;
  /* => font-semibold */
  color: #111827;
  /* => text-gray-900 */
  margin-bottom: 0.5rem;
  /* => mb-2 */
}
 
.card__body {
  font-size: 0.875rem;
  /* => text-sm */
  color: #6b7280;
  /* => text-gray-500 */
  line-height: 1.625;
  /* => leading-relaxed */
}
 
.card__button {
  display: inline-flex;
  /* => inline-flex */
  align-items: center;
  /* => items-center */
  background-color: #2563eb;
  /* => bg-blue-600 */
  color: #ffffff;
  /* => text-white */
  padding: 0.5rem 1rem;
  /* => py-2 px-4 */
  border-radius: 0.5rem;
  /* => rounded-lg */
  font-weight: 500;
  /* => font-medium */
  margin-top: 1rem;
  /* => mt-4 */
  transition: background-color 150ms;
  /* => transition-colors */
}
 
.card__button:hover {
  background-color: #1d4ed8;
  /* => hover:bg-blue-700 */
}

After (Tailwind utilities):

<!-- => Tailwind utility equivalent of the CSS above -->
<div class="bg-white rounded-xl p-6 shadow-md border border-gray-200">
  <!-- => Every CSS property mapped to an equivalent utility class -->
 
  <h3 class="text-lg font-semibold text-gray-900 mb-2">Card Title</h3>
 
  <p class="text-sm text-gray-500 leading-relaxed">Card body text content with appropriate typography.</p>
 
  <button
    class="inline-flex items-center bg-blue-600 text-white py-2 px-4 rounded-lg font-medium mt-4 transition-colors hover:bg-blue-700"
  >
    <!-- => Each CSS rule becomes one or two utility classes -->
    Action Button
  </button>
</div>

Key Takeaway: Map CSS declarations directly to utilities: padding: 1.5remp-6, color: #6b7280text-gray-500, border-radius: 0.75remrounded-xl. Use browser DevTools to inspect computed styles and find Tailwind equivalents.

Why It Matters: Migration is the reality for most teams - existing codebases have traditional CSS that needs to coexist with Tailwind. The direct mapping approach enables incremental migration without rewriting everything at once. Identify the most frequently modified components first (often buttons, cards, inputs) and migrate those, leaving rare-change components until later. The key discipline: resist the temptation to use style="" attributes as a migration shortcut. Each style="" usage is a future maintenance burden. Converting to Tailwind utilities at the component level, even if the component still imports a CSS file, creates the hybrid pattern that most mature Tailwind migrations use.

Example 70: Migrating from Bootstrap to Tailwind

Bootstrap and Tailwind represent fundamentally different philosophies. Understanding the conceptual and practical translation between them enables smooth migration for Bootstrap-experienced teams.

Bootstrap approach vs Tailwind approach:

<!-- === Bootstrap component (semantic component classes) === -->
<div class="card" style="width: 18rem;">
  <!-- => Bootstrap .card: pre-styled component with fixed design -->
  <div class="card-body">
    <!-- => .card-body: Bootstrap's padding/styling for card content -->
    <h5 class="card-title">Card Title</h5>
    <!-- => .card-title: Bootstrap's heading margin -->
    <p class="card-text">Some quick example text.</p>
    <!-- => .card-text: Bootstrap's paragraph margin -->
    <a href="#" class="btn btn-primary">Go somewhere</a>
    <!-- => .btn .btn-primary: Bootstrap's pre-styled blue button -->
  </div>
</div>
 
<!-- === Tailwind equivalent (utility composition) === -->
<div class="bg-white border border-gray-200 rounded-lg shadow-sm w-72">
  <!-- => No pre-built .card class: utilities describe the card directly -->
  <!-- => More verbose but fully explicit: every property visible in HTML -->
  <div class="p-4">
    <!-- => p-4: explicit 1rem padding (Bootstrap uses more padding by default) -->
    <h5 class="text-xl font-semibold text-gray-900 mb-2">Card Title</h5>
    <!-- => Each typography property explicitly named -->
    <p class="text-gray-600 text-sm mb-4">Some quick example text.</p>
    <!-- => text-gray-600 text-sm: explicit color and size (Bootstrap inherits) -->
    <a
      href="#"
      class="inline-flex items-center bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium px-4 py-2 rounded-lg transition-colors"
    >
      <!-- => Every button property declared explicitly -->
      <!-- => Much more verbose than .btn .btn-primary -->
      <!-- => Advantage: fully customizable without overriding Bootstrap specificity -->
      Go somewhere
    </a>
  </div>
</div>

Bootstrap utilities that map directly to Tailwind:

<!-- => Bootstrap utility → Tailwind utility mappings -->
 
<div class="d-flex">
  <!-- Bootstrap d-flex = Tailwind flex -->
  <div class="flex-column">
    <!-- Bootstrap flex-column = Tailwind flex-col -->
    <div class="align-items-center">
      <!-- Bootstrap = Tailwind items-center -->
      <div class="justify-content-between">
        <!-- Bootstrap = Tailwind justify-between -->
        <div class="mt-3">
          <!-- Bootstrap mt-3 (16px) ≈ Tailwind mt-4 (16px) -->
          <div class="p-3">
            <!-- Bootstrap p-3 (16px) ≈ Tailwind p-4 (16px) -->
            <div class="text-center">
              <!-- Bootstrap = Tailwind text-center -->
              <div class="text-muted">
                <!-- Bootstrap = Tailwind text-gray-500 -->
                <div class="fw-bold">
                  <!-- Bootstrap = Tailwind font-bold -->
                  <div class="small">
                    <!-- Bootstrap = Tailwind text-sm -->
                    <div class="d-none d-md-block"><!-- Bootstrap = Tailwind hidden md:block --></div>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Key Takeaway: Bootstrap maps component classes (.card, .btn) to pre-built designs. Tailwind maps CSS properties to utilities. Migration means replacing Bootstrap component classes with equivalent utility combinations - more verbose but fully visible and customizable.

Why It Matters: Bootstrap is the world's most widely used CSS framework, and many teams migrate to Tailwind for customizability and modern tooling. The conceptual shift from "apply Bootstrap's card design" to "compose my own card design with utilities" requires active mindset change. The migration is not just a class renaming exercise - it's architectural: removing dependency on Bootstrap's global styles, specificity, and component designs. Most successful migrations are incremental: add Tailwind to the project, use it for new components, gradually migrate existing ones. A critical step is removing Bootstrap before going fully live with Tailwind - having both active creates specificity conflicts and double CSS bundle size.

Group 10: Advanced Composition Patterns

Example 71: Design Token System with Tailwind

Building a complete design token system on top of Tailwind creates a scalable foundation for large-scale, multi-brand applications.

// design-system/tokens.config.js
// => Centralized design token configuration
// => This file is imported into tailwind.config.js
 
const tokens = {
  // === COLOR TOKENS ===
  colors: {
    // => Primitive colors (raw values, not semantic)
    neutral: {
      0: "#ffffff",
      50: "#f9fafb",
      100: "#f3f4f6",
      200: "#e5e7eb",
      300: "#d1d5db",
      400: "#9ca3af",
      500: "#6b7280",
      600: "#4b5563",
      700: "#374151",
      800: "#1f2937",
      900: "#111827",
      1000: "#000000",
    },
 
    // => Brand colors (one hue, full scale)
    brand: {
      50: "#eff6ff",
      100: "#dbeafe",
      200: "#bfdbfe",
      300: "#93c5fd",
      400: "#60a5fa",
      500: "#3b82f6",
      600: "#2563eb",
      700: "#1d4ed8",
      800: "#1e40af",
      900: "#1e3a8a",
    },
  },
 
  // === SEMANTIC TOKENS ===
  // => Semantic tokens reference primitives - decouple usage from implementation
  semantic: {
    // => Background tokens
    "bg-primary": "neutral.0",
    // => bg-primary: white (neutral-0), meaning "main page background"
    "bg-secondary": "neutral.50",
    // => bg-secondary: lightest gray for subtle section backgrounds
    "bg-interactive": "brand.600",
    // => bg-interactive: primary brand blue for buttons and CTAs
 
    // => Text tokens
    "text-primary": "neutral.900",
    // => text-primary: near-black for main readable content
    "text-secondary": "neutral.500",
    // => text-secondary: medium gray for supporting content
    "text-on-interactive": "neutral.0",
    // => text-on-interactive: white text on blue interactive elements
  },
};
 
module.exports = tokens;

Using tokens in tailwind.config.js:

// tailwind.config.js
const tokens = require("./design-system/tokens.config.js");
 
module.exports = {
  content: ["./src/**/*.{html,ts,tsx}"],
  theme: {
    extend: {
      colors: {
        // => Spread primitive colors into Tailwind palette
        neutral: tokens.colors.neutral,
        // => neutral-0 through neutral-1000 become available as Tailwind utilities
        brand: tokens.colors.brand,
        // => brand-50 through brand-900 become available
 
        // => Map semantic tokens to their primitive values
        "bg-interactive": tokens.colors.brand["600"],
        // => bg-bg-interactive (class): uses brand-600 value
        "text-on-interactive": tokens.colors.neutral["0"],
        // => text-text-on-interactive (class): uses neutral-0 (white)
      },
    },
  },
};

Key Takeaway: Two-layer token architecture: primitive tokens (raw color values) + semantic tokens (role-based references). Semantic tokens enable theme switching by remapping references without changing utility names in HTML.

Why It Matters: Design token systems are the foundation of scalable design engineering. At Airbnb, IBM, Adobe, and Atlassian, design token systems allow: (1) designers to change brand colors from a single source, (2) developers to switch between light/dark/high-contrast themes by swapping token mappings, (3) multi-brand products to share component code while having distinct visual identities. The two-layer approach is the industry standard - primitives give you the full palette, semantics give you purpose. When the semantic token bg-interactive changes from brand-600 to green-600, every button in the entire application updates without touching component code. This is the difference between a CSS framework and a design system.

Example 72: Complex Responsive Dashboard Layout

Building a complete responsive dashboard demonstrates how multiple Tailwind patterns combine to handle real-world layout complexity.

<!-- => Production dashboard layout -->
<div class="min-h-screen bg-gray-50 dark:bg-gray-950">
  <!-- => min-h-screen: fills viewport height -->
  <!-- => dark:bg-gray-950: near-black in dark mode -->
 
  <!-- === Sidebar (hidden on mobile, fixed on desktop) === -->
  <aside
    class="fixed inset-y-0 left-0 w-64 bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 z-30 hidden lg:block"
  >
    <!-- => fixed inset-y-0 left-0: sticks to left side, full height -->
    <!-- => w-64: sidebar width: 256px -->
    <!-- => hidden lg:block: only shows at 1024px+ -->
    <!-- => z-30: above content but below modals (z-50) -->
 
    <div class="p-6">
      <a href="/" class="flex items-center gap-2 text-lg font-bold text-gray-900 dark:text-white">
        <!-- => flex items-center gap-2: logo + text alignment -->
        <span class="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center text-white text-sm font-bold"
          >A</span
        >
        AppName
      </a>
    </div>
 
    <nav class="px-4 space-y-1">
      <!-- => space-y-1: 4px gap between nav links -->
      <a
        href="#"
        class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium bg-blue-50 text-blue-700 dark:bg-blue-950 dark:text-blue-300"
      >
        <!-- => bg-blue-50 text-blue-700: active nav item styling -->
        <!-- => dark:bg-blue-950: dark mode active state -->
        Dashboard
      </a>
      <a
        href="#"
        class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium text-gray-600 hover:bg-gray-50 dark:text-gray-400 dark:hover:bg-gray-800 transition-colors"
      >
        <!-- => text-gray-600 hover:bg-gray-50: inactive nav item -->
        Analytics
      </a>
    </nav>
  </aside>
 
  <!-- === Main content area (offset by sidebar width on desktop) === -->
  <div class="lg:ml-64">
    <!-- => lg:ml-64: pushes content right by sidebar width at 1024px+ -->
 
    <!-- Top header -->
    <header class="sticky top-0 z-20 bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 px-6 py-4">
      <!-- => sticky top-0 z-20: sticks to top when scrolling (below sidebar z-30) -->
      <div class="flex items-center justify-between">
        <h1 class="text-xl font-semibold text-gray-900 dark:text-white">Dashboard</h1>
        <div class="flex items-center gap-3">
          <button
            class="relative p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
          >
            <!-- => Notification bell with badge -->
            🔔
            <span class="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full"></span>
            <!-- => absolute: positions badge relative to button -->
            <!-- => top-1 right-1: 4px from top-right corner -->
          </button>
          <div class="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-white text-sm font-medium">
            <!-- => Avatar: circular with initials -->
            A
          </div>
        </div>
      </div>
    </header>
 
    <!-- Dashboard content -->
    <main class="p-6 space-y-6">
      <!-- Metric cards grid -->
      <div class="grid grid-cols-2 lg:grid-cols-4 gap-4">
        <!-- => grid-cols-2: 2 columns on mobile -->
        <!-- => lg:grid-cols-4: 4 columns on desktop -->
 
        <div class="bg-white dark:bg-gray-900 rounded-xl p-4 border border-gray-200 dark:border-gray-800 shadow-sm">
          <p class="text-sm text-gray-500 dark:text-gray-400">Total Users</p>
          <p class="text-2xl font-bold text-gray-900 dark:text-white mt-1">12,345</p>
          <p class="text-xs text-green-600 dark:text-green-400 mt-1">+12% from last month</p>
          <!-- => text-green-600: positive change in green -->
        </div>
 
        <div class="bg-white dark:bg-gray-900 rounded-xl p-4 border border-gray-200 dark:border-gray-800 shadow-sm">
          <p class="text-sm text-gray-500 dark:text-gray-400">Revenue</p>
          <p class="text-2xl font-bold text-gray-900 dark:text-white mt-1">$89,400</p>
          <p class="text-xs text-red-600 dark:text-red-400 mt-1">-3% from last month</p>
          <!-- => text-red-600: negative change in red -->
        </div>
      </div>
    </main>
  </div>
</div>

Key Takeaway: Dashboard layout pattern: fixed sidebar + lg:ml-{sidebar-width} main content + sticky top header. Each layer uses explicit z-index values (z-20, z-30, z-50) for correct stacking.

Why It Matters: Dashboard layout is the most complex and common application layout pattern. Every SaaS product, admin panel, and analytics tool uses this exact structure. The z-index hierarchy (header z-20, sidebar z-30, modal z-50) prevents classic bugs: dropdowns hidden behind the sidebar, modals appearing under the header, tooltips clipped by overflow-hidden containers. The responsive pattern (sidebar hidden on mobile, shown on desktop with hidden lg:block) avoids the need for a separate mobile layout. Understanding how lg:ml-64 offsets the content to prevent sidebar overlap is essential knowledge - forget it and the sidebar covers the first 256px of content on desktop.

Example 73: Tailwind v4 Migration Guide

Migrating from Tailwind v3 to v4 requires understanding breaking changes, new features, and the migration path for configuration.

/* v4 migration: Before (v3) → After (v4) */
 
/* === BEFORE (v3): tailwind directives in CSS === */
/*
@tailwind base;
@tailwind components;
@tailwind utilities;
*/
 
/* === AFTER (v4): single @import === */
@import "tailwindcss";
/* => Replaces all three @tailwind directives */
 
/* === BEFORE (v3): Configuration in tailwind.config.js === */
/*
module.exports = {
  theme: {
    extend: {
      colors: { brand: { 500: '#3b82f6' } },
      fontFamily: { display: ['Playfair Display', 'serif'] },
    },
  },
};
*/
 
/* === AFTER (v4): Configuration in CSS via @theme === */
@theme {
  --color-brand-500: #3b82f6;
  /* => Replaces theme.extend.colors.brand[500] */
  --font-display: "Playfair Display", serif;
  /* => Replaces theme.extend.fontFamily.display */
}
 
/* === BREAKING CHANGES FROM v3 TO v4 === */
 
/* 1. Removed: @tailwind directives */
/* @tailwind base; → removed */
/* @tailwind components; → removed */
/* @tailwind utilities; → removed */
/* Solution: use @import "tailwindcss" */
 
/* 2. Changed: prefix syntax */
/* v3: tw-bg-blue-500 → v4: bg-blue-500 (prefix in @import) */
/* @import "tailwindcss" prefix(tw); */
 
/* 3. Changed: dark mode configuration */
/* v3: darkMode: 'class' in config.js */
/* v4: @variant dark (&:where(.dark, .dark *)); */
 
/* 4. Changed: content configuration */
/* v3: content: ['./src/**\/*.{html,ts}'] in config.js */
/* v4: @source './src/**\/*.{html,ts}'; in CSS */

v4 update instructions:

# => Official Tailwind v4 upgrade tool
npx @tailwindcss/upgrade@next
# => Automatically migrates tailwind.config.js to @theme in CSS
# => Updates @tailwind directives to @import
# => Flags breaking changes for manual review
 
# => Install Tailwind v4
npm install -D tailwindcss@next @tailwindcss/vite@next
# => @tailwindcss/vite: new Vite plugin replacing PostCSS plugin for v4

Key Takeaway: v4 migration: replace @tailwind directives with @import "tailwindcss", move theme values from tailwind.config.js to @theme {} in CSS, and use @source for content paths. Run the official upgrade tool first.

Why It Matters: Tailwind v4 is the most significant version change in the framework's history. The CSS-first approach is faster (no Node.js config parsing), more flexible (native CSS cascade for tokens), and integrates better with the browser's native CSS custom property system. Understanding the migration path is essential for teams maintaining v3 codebases - major frameworks (Next.js, Nuxt, Remix) will require v4 for their latest features over time. The official upgrade tool handles most of the mechanical changes, but understanding what's changing prepares you to handle edge cases the tool misses and to write idiomatic v4 CSS from the start on new projects.

Example 74: Testing Tailwind-Styled Components

Testing components that use Tailwind classes requires different strategies depending on whether you're testing behavior, visual appearance, or accessibility.

// Button.test.tsx (React Testing Library)
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
 
// === STRATEGY 1: Test behavior, not classes ===
// => Best practice: query by semantic attributes, not CSS classes
// => Tailwind classes are implementation details, not user-facing behavior
 
describe("Button component", () => {
  it("calls onClick when clicked", async () => {
    const user = userEvent.setup();
    const onClick = jest.fn();
 
    render(
      <button onClick={onClick} className="bg-blue-600 text-white px-4 py-2 rounded">
        Click me
      </button>,
    );
    // => render: renders component into JSDOM (CSS classes not evaluated)
    // => Note: JSDOM doesn't compute CSS, so Tailwind classes have no visual effect in unit tests
 
    await user.click(screen.getByRole("button", { name: "Click me" }));
    // => getByRole: queries by ARIA role (semantic) not class (implementation)
    // => This test remains valid even if all Tailwind classes change
 
    expect(onClick).toHaveBeenCalledTimes(1);
    // => Tests behavior (was onClick called), not appearance
  });
 
  it("is disabled when loading", () => {
    render(
      <button disabled className="disabled:opacity-50 cursor-not-allowed">
        Loading...
      </button>,
    );
 
    const button = screen.getByRole("button");
    expect(button).toBeDisabled();
    // => toBeDisabled(): checks HTML disabled attribute, not CSS class
    // => The disabled:opacity-50 visual effect is not testable in unit tests
  });
 
  // === STRATEGY 2: Test class application for design system verification ===
  // => Only needed for component library packages where class correctness matters
 
  it("applies correct classes for variant", () => {
    const { container } = render(<Button variant="primary">Primary</Button>);
    // => container: the DOM element wrapping the rendered output
 
    expect(container.firstChild).toHaveClass("bg-blue-600");
    // => toHaveClass: checks if element has the specified CSS class
    // => Use sparingly - couples tests to implementation (Tailwind classes)
    // => Valid for design system regression tests where class = design contract
  });
});

Storybook for visual testing:

// Button.stories.tsx
export default {
  title: "Components/Button",
  component: Button,
};
// => Storybook: visual testing environment where Tailwind CSS is evaluated
 
export const Primary = {
  args: { variant: "primary", children: "Primary Button" },
  // => Renders with actual CSS evaluation - visual regressions are visible
};
 
export const AllVariants = () => (
  <div className="flex gap-4 p-8">
    <Button variant="primary">Primary</Button>
    <Button variant="secondary">Secondary</Button>
    <Button variant="destructive">Destructive</Button>
  </div>
  // => Storybook: visual catalog of all variants with actual Tailwind rendering
);

Key Takeaway: Unit tests with React Testing Library should test behavior and accessibility (roles, disabled state, callbacks), not Tailwind classes. Use Storybook or Chromatic for visual regression testing where CSS is actually evaluated.

Why It Matters: Testing strategy determines test suite quality and maintenance burden. Tests that assert specific Tailwind class names break every time a developer refactors styles (changing p-4 to px-4 py-4 is functionally identical but breaks class-checking tests). Behavior-based tests (toBeDisabled(), toHaveRole(), toHaveTextContent()) survive style refactoring. Visual regressions are best caught by screenshot testing tools (Chromatic, Percy, Playwright's screenshot comparisons) that evaluate actual CSS. This strategy - behavior tests in Jest/Vitest, visual tests in Storybook/Chromatic - is the industry standard for component libraries. Understanding this prevents the common mistake of writing fragile class-checking tests that provide false confidence while adding maintenance overhead.

Example 75: Tailwind in Monorepo and Shared Component Libraries

Managing Tailwind across multiple applications in a monorepo requires careful configuration sharing, version alignment, and content scanning across package boundaries.

// packages/design-system/tailwind.config.js
// => Shared Tailwind config in a monorepo design system package
 
const sharedConfig = {
  // => theme: shared design tokens across all apps in monorepo
  theme: {
    extend: {
      colors: {
        brand: {
          50: "#eff6ff",
          500: "#3b82f6",
          900: "#1e3a8a",
        },
      },
      fontFamily: {
        sans: ["Inter var", "system-ui", "sans-serif"],
      },
    },
  },
};
 
module.exports = sharedConfig;
// => Export shared config for consumption by app-level configs

App-level config extending shared config:

// apps/marketing-site/tailwind.config.js
const sharedConfig = require("@company/design-system/tailwind.config.js");
// => Import shared config from the design system package
 
module.exports = {
  // => Spread shared config
  ...sharedConfig,
 
  // => Override content for this app's file paths
  content: [
    "./src/**/*.{html,ts,tsx}",
    // => App-specific files
    "../../packages/design-system/src/**/*.{ts,tsx}",
    // => CRITICAL: include design system component files
    // => Without this, classes used in shared components get purged
  ],
 
  // => Merge theme (don't overwrite shared config)
  theme: {
    ...sharedConfig.theme,
    // => Spread shared theme first
    extend: {
      ...sharedConfig.theme.extend,
      // => Spread shared extensions
      // => Add app-specific extensions below
      colors: {
        ...sharedConfig.theme.extend.colors,
        marketing: {
          // => Marketing site-specific colors (not in shared config)
          hero: "#FF6B35",
          cta: "#4ECDC4",
        },
      },
    },
  },
};

Key Takeaway: Extract shared design tokens to a package-level config. Each app extends the shared config, adds its own content paths (including the design system package paths), and adds app-specific theme extensions. Content paths must include all packages that use Tailwind classes.

Why It Matters: Monorepo Tailwind management is one of the most common pain points for large engineering teams. The two critical failure modes are: (1) duplicate Tailwind configurations that drift out of sync, creating visual inconsistencies across apps; (2) missing content paths that cause component library classes to be purged from app builds. The shared config pattern solves both: one source of truth for tokens, and explicit content path configuration that ensures shared components' classes are preserved. Teams building internal component libraries (a design system package used by 5+ apps) should set up shared Tailwind config from day one - retrofitting it later requires auditing every app's config and coordinating releases. Large monorepos like Vercel's, Stripe's, and Shopify's use exactly this pattern.

Example 76: Performance Profiling and CSS Optimization

Measuring and optimizing Tailwind's CSS output size and rendering performance enables data-driven decisions for production deployments.

# => Measure CSS bundle size
npx tailwindcss -i src/input.css -o dist/output.css --minify
# => --minify: applies cssnano-equivalent minification
# => Check output size: should be <30KB for most apps
ls -lh dist/output.css
 
# => Count unique class usages to identify most common patterns
grep -oh 'class="[^"]*"' src/**/*.html | \
  grep -oh '\b\w[-\w:/]+\b' | \
  sort | uniq -c | sort -rn | head -20
# => Identifies top 20 most-used classes
# => High frequency = candidates for @apply extraction
# => Low frequency = keep inline (not worth abstracting)

Performance optimization checklist:

// tailwind.config.js - Performance-optimized configuration
module.exports = {
  content: [
    // => PRECISE paths (avoid over-scanning)
    "./src/**/*.{tsx,ts}",
    // => Only TypeScript React files (not all files)
    // => Avoid './src/**/*' (scans binary files unnecessarily)
  ],
 
  // => Disable unused core plugins
  corePlugins: {
    // => Disable features your app doesn't use
    preflight: true,
    // => Keep: base CSS reset (needed)
 
    animation: true,
    // => Keep if using animate-spin, animate-pulse (skeleton screens)
 
    gradientColorStops: true,
    // => Keep if using gradients
 
    // => Disable if truly unused
    // columns: false,          // ~1KB savings
    // aspectRatio: false,      // ~0.5KB (if browser support allows)
    // textIndent: false,       // ~0.5KB
    // writingMode: false,      // ~0.5KB
  },
 
  // => Theme constraints: only include values actually used
  theme: {
    // => Override colors to just what your design system uses
    colors: require("./design-tokens").primitiveColors,
    // => If design-tokens defines 10 colors, only those 10 appear in output
    // => Default Tailwind ships 22 colors × 11 shades = 242 color values
    // => Your app likely uses 5-8 colors × 6-8 shades = 30-64 values
  },
};

Key Takeaway: Optimization sequence: measure first (file size), identify unused core plugins (disable them), constrain color/spacing scales to values actually used, verify content paths are precise and minimal.

Why It Matters: CSS size has direct impact on Core Web Vitals. The Largest Contentful Paint (LCP) metric depends on when critical CSS is parsed. Every 10KB of CSS adds approximately 50-100ms of parse time on mid-range mobile devices. Lighthouse's "Reduce unused CSS" audit flags large CSS bundles. For e-commerce checkout pages, performance improvements of 100ms can increase conversion rates by 1%. The data-driven approach (measure → optimize → measure again) prevents premature optimization while ensuring meaningful improvements are made. Most Tailwind apps with proper purging are already small, but design system packages with many custom tokens and plugins can accumulate significant CSS weight without targeted measurement and optimization.

Example 77: Advanced Selectors and the JIT Engine

Tailwind v3's JIT engine and v4's Rust-based engine support a rich set of selectors via variants that go beyond the basics covered in beginner and intermediate sections.

<!-- === Advanced variant combinations === -->
<div class="space-y-4 p-4">
  <!-- === Multiple condition chaining === -->
  <button
    class="bg-blue-600 dark:bg-blue-500 hover:dark:bg-blue-400 focus-visible:dark:ring-blue-300 text-white px-4 py-2 rounded"
  >
    <!-- => dark:bg-blue-500: dark mode base style -->
    <!-- => hover:dark:bg-blue-400: hover state within dark mode -->
    <!-- => focus-visible:dark:ring-blue-300: focus ring in dark mode -->
    <!-- => Variants chain: each adds a level of specificity -->
    Chained variants: hover + dark
  </button>
 
  <!-- === has: variant (parent-based styling) === -->
  <form class="has-[:invalid]:ring-red-500 has-[:invalid]:ring-2 p-4 rounded-lg border border-gray-300">
    <!-- => has-[:invalid]: applies when any descendant matches :invalid -->
    <!-- => ring-red-500: red ring on the FORM when an input is invalid -->
    <!-- => This styles the parent based on child state (normally hard in CSS) -->
    <input
      type="email"
      required
      class="border border-gray-300 rounded px-3 py-2 w-full"
      placeholder="Email (required)"
    />
    <!-- => When email is invalid, has-[:invalid] triggers on the form wrapper -->
  </form>
 
  <!-- === supports: variant (feature detection) === -->
  <div
    class="bg-gray-100 supports-[backdrop-filter]:bg-white/50 supports-[backdrop-filter]:backdrop-blur-md rounded-xl p-4"
  >
    <!-- => supports-[backdrop-filter]: only applies if browser supports backdrop-filter -->
    <!-- => Browsers with backdrop-filter: glassmorphism (frosted glass) effect -->
    <!-- => Browsers without: falls back to solid bg-gray-100 -->
    <!-- => Progressive enhancement with CSS feature detection -->
    Glassmorphism with supports: fallback
  </div>
 
  <!-- === not: variant === -->
  <ul class="space-y-1">
    <li class="p-2 rounded [&:not(:last-child)]:border-b border-gray-200">Item 1 (has border)</li>
    <!-- => [&:not(:last-child)]: arbitrary variant with :not selector -->
    <!-- => border-b: bottom border applied to all items except last -->
    <li class="p-2 rounded [&:not(:last-child)]:border-b border-gray-200">Item 2 (has border)</li>
    <li class="p-2 rounded [&:not(:last-child)]:border-b border-gray-200">Item 3 (no border - last child)</li>
  </ul>
 
  <!-- === aria: variants === -->
  <button
    class="px-4 py-2 rounded aria-pressed:bg-blue-600 aria-pressed:text-white aria-pressed:ring-2 aria-pressed:ring-blue-400 bg-gray-100 text-gray-700 transition-colors"
    aria-pressed="false"
    onclick="this.setAttribute('aria-pressed', this.getAttribute('aria-pressed') === 'true' ? 'false' : 'true')"
  >
    <!-- => aria-pressed:bg-blue-600: active background when aria-pressed=true -->
    <!-- => aria-pressed:text-white: white text in pressed state -->
    <!-- => No JavaScript state management needed for visual styling -->
    Toggle (ARIA-driven)
  </button>
</div>

Key Takeaway: Chain variants freely (hover:dark:bg-blue-400). Use has-[selector]:utility to style parents based on child state. Use supports-[feature]:utility for progressive enhancement. Use aria-{attribute}:utility for ARIA-driven state styling.

Why It Matters: Advanced selectors eliminate entire categories of JavaScript state management. The has-[:invalid] pattern styles form wrappers based on validation state without React state or event handlers. The aria-pressed:bg-blue-600 pattern keeps visual styling synchronized with accessibility state automatically. The supports-[backdrop-filter] pattern implements progressive enhancement (graceful degradation for older browsers) in a single class. These patterns represent Tailwind's approach to moving complexity from JavaScript into CSS, reducing bundle size and improving performance. Each eliminates at least one useState or useEffect call that would otherwise be needed for pure CSS-equivalent functionality.

Example 78: Tailwind with Server Components and SSR

Using Tailwind in server-rendered environments (Next.js App Router, Astro, Nuxt) requires understanding how class scanning works with server-side rendering.

// next.config.mjs (Next.js 14+ App Router)
// => Next.js handles Tailwind via PostCSS automatically
// => No special configuration needed for App Router
 
// tailwind.config.js for Next.js
module.exports = {
  content: [
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
    // => app/: Next.js 13+ App Router directory
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",
    // => pages/: Next.js Pages Router (if used alongside App Router)
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
    // => components/: shared UI components
    "./src/**/*.{js,ts,jsx,tsx,mdx}",
    // => src/: if using src/ directory convention
  ],
  // => No theme or plugin changes needed for SSR compatibility
};

SSR-safe class construction patterns:

// components/Badge.tsx - Server Component (no 'use client')
// => This is a React Server Component - no client-side JavaScript
 
interface BadgeProps {
  status: 'active' | 'inactive' | 'pending' | 'error';
  label: string;
}
 
// => Color map: complete class strings (JIT scanner friendly)
const statusClasses: Record<BadgeProps['status'], string> = {
  active: 'bg-green-100 text-green-800 ring-green-600/20',
  // => 'bg-green-100': complete class string visible to static scanner
  inactive: 'bg-gray-100 text-gray-600 ring-gray-500/10',
  pending: 'bg-yellow-100 text-yellow-800 ring-yellow-600/20',
  error: 'bg-red-100 text-red-700 ring-red-600/20',
  // => Each value is a COMPLETE string - scanner sees all classes
};
 
export function Badge({ status, label }: BadgeProps) {
  // => Server Component: renders on server, sends HTML to client
  const classes = statusClasses[status];
  // => classes: selected from static map (all possible values pre-declared)
 
  return (
    <span className={`inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset ${classes}`}>
      {/* => Base classes: always included (scanner sees these as literal strings) */}
      {/* => Dynamic classes: from statusClasses map (scanner sees all values) */}
      {label}
    </span>
  );
}
 
// Usage in a Server Component page:
// <Badge status="active" label="Active" />
// <Badge status="error" label="Error" />

Key Takeaway: In SSR environments, Tailwind class scanning works on source files at build time. Use object maps with complete class string values for dynamic class selection. Avoid template literal class construction. Server Components don't affect Tailwind scanning behavior.

Why It Matters: Next.js App Router's Server Components are the future of React rendering, and understanding how Tailwind's build-time scanning interacts with runtime rendering is essential. The JIT scanner runs during next build on source files - it doesn't execute code, it pattern-matches strings. A Server Component is scanned exactly like a Client Component from Tailwind's perspective. The only special consideration is dynamic class construction: bg-${status} is invisible to the scanner, but statusClasses[status] where statusClasses contains complete strings works perfectly. This pattern (status map with complete class strings) appears in every production Next.js + Tailwind application that has dynamic styling from API data or database values.

Example 79: Tailwind Print Styles and Export Patterns

Print-specific styles enable polished document output for reports, invoices, and data exports that users print from web applications.

<!-- === Print-optimized report layout === -->
 
<!-- Add print: prefix to any utility for print-specific behavior -->
<div class="min-h-screen bg-gray-50 p-8 print:p-0 print:bg-white print:min-h-0">
  <!-- => print:p-0: removes screen padding for print (margin set by printer) -->
  <!-- => print:bg-white: white background (saves ink, most printers need it) -->
  <!-- => print:min-h-0: removes viewport height constraint for print -->
 
  <!-- Navigation: visible on screen, hidden when printing -->
  <nav class="bg-white shadow-sm mb-8 px-6 py-4 print:hidden">
    <!-- => print:hidden: navigation disappears entirely when printing -->
    <h1 class="text-2xl font-bold">Monthly Report</h1>
  </nav>
 
  <!-- Report header: adapts for print -->
  <div
    class="bg-white rounded-xl border border-gray-200 p-6 shadow-sm print:shadow-none print:border-0 print:rounded-none"
  >
    <!-- => print:shadow-none: removes drop shadow (doesn't print well) -->
    <!-- => print:border-0: removes borders that may not print as expected -->
    <!-- => print:rounded-none: sharp corners for print (rounding is cosmetic) -->
 
    <div class="flex justify-between items-start print:mb-4">
      <div>
        <h2 class="text-xl font-bold text-gray-900">Invoice #2024-001</h2>
        <p class="text-gray-500 text-sm">Generated March 25, 2026</p>
      </div>
      <div class="text-right">
        <p class="text-2xl font-bold text-gray-900">$1,234.00</p>
        <p class="text-green-600 text-sm font-medium">Paid</p>
      </div>
    </div>
 
    <!-- Table with print-specific styling -->
    <table class="w-full mt-6 text-sm print:mt-2">
      <thead>
        <tr class="border-b-2 border-gray-200">
          <th class="text-left py-3 font-semibold text-gray-900">Description</th>
          <th class="text-right py-3 font-semibold text-gray-900">Amount</th>
        </tr>
      </thead>
      <tbody class="divide-y divide-gray-100 print:divide-gray-300">
        <!-- => print:divide-gray-300: darker dividers are more visible when printed -->
        <tr>
          <td class="py-3 text-gray-700">Consulting Services (20 hours)</td>
          <td class="py-3 text-right text-gray-900 font-medium">$1,000.00</td>
        </tr>
        <tr>
          <td class="py-3 text-gray-700">Hosting (1 month)</td>
          <td class="py-3 text-right text-gray-900 font-medium">$234.00</td>
        </tr>
      </tbody>
      <tfoot>
        <tr class="border-t-2 border-gray-900">
          <td class="py-3 font-bold text-gray-900">Total</td>
          <td class="py-3 text-right font-bold text-gray-900">$1,234.00</td>
        </tr>
      </tfoot>
    </table>
  </div>
 
  <!-- Page break control -->
  <div class="mt-8 break-before-page print:mt-0">
    <!-- => break-before-page: page-break-before: always (forces new print page) -->
    Second page content starts here
  </div>
</div>

Key Takeaway: Use print:hidden for screen-only chrome, print:shadow-none print:border-0 to clean up decorative styles, print:bg-white for ink-saving backgrounds, and break-before-page for multi-page document pagination.

Why It Matters: Print styles are the most commonly neglected aspect of web application development, yet they're frequently required in enterprise contexts. Finance dashboards need printable P&L reports. Legal platforms need printable contracts. Invoicing systems need printable invoices that look professional out of the printer. Neglecting print styles results in screen navigation, sidebars, and interactive elements appearing in printed output - deeply unprofessional for client-facing documents. The print: variant handles all of this declaratively in HTML. Adding print styles to an existing Tailwind application requires 20-30 minutes and transforms mediocre printouts into polished documents, directly impacting enterprise sales and user satisfaction with the product.

Example 80: Building a Complete Accessible UI Component

Synthesizing all advanced concepts into a single production-quality, fully accessible, dark-mode-compatible component demonstrates the full capability of Tailwind in production.

<!-- === Production-quality accessible toggle/switch component === -->
 
<!--
  Requirements checklist:
  - WCAG AA accessible (keyboard, screen reader)
  - Dark mode support
  - Smooth animations
  - Correct ARIA attributes
  - Loading state
  - Disabled state
  - Visual + non-visual state indication
-->
 
<label class="flex items-center gap-3 cursor-pointer group" aria-label="Enable notifications">
  <!-- => label: clicking anywhere activates the input -->
  <!-- => flex items-center gap-3: aligns toggle and label text -->
  <!-- => group: enables group-* variants on children -->
  <!-- => aria-label: describes the purpose for screen readers -->
 
  <!-- Hidden native checkbox (accessible but visually replaced) -->
  <input type="checkbox" class="peer sr-only" role="switch" aria-checked="false" />
  <!-- => peer: marks as peer context for sibling styling -->
  <!-- => sr-only: visually hidden but in tab order and accessible -->
  <!-- => role="switch": announces as toggle switch to screen readers -->
  <!-- => aria-checked="false": current state (JS updates this) -->
 
  <!-- Custom toggle visual (replaces native checkbox appearance) -->
  <div
    class="
    relative w-11 h-6 rounded-full
    bg-gray-200 dark:bg-gray-700
    peer-checked:bg-blue-600 dark:peer-checked:bg-blue-500
    peer-focus-visible:ring-2 peer-focus-visible:ring-blue-500 peer-focus-visible:ring-offset-2
    peer-focus-visible:dark:ring-offset-gray-900
    peer-disabled:opacity-50 peer-disabled:cursor-not-allowed
    transition-colors duration-200
    group-hover:ring-2 group-hover:ring-blue-500/20
  "
  >
    <!-- => relative: positions the sliding knob absolutely within -->
    <!-- => w-11 h-6: toggle track: 44px × 24px -->
    <!-- => rounded-full: pill-shaped track -->
    <!-- => bg-gray-200: off state track color -->
    <!-- => dark:bg-gray-700: off state in dark mode -->
    <!-- => peer-checked:bg-blue-600: on state track color -->
    <!-- => dark:peer-checked:bg-blue-500: on state in dark mode -->
    <!-- => peer-focus-visible:ring-2: focus ring (keyboard only) -->
    <!-- => peer-focus-visible:ring-offset-2: gap between track and ring -->
    <!-- => peer-disabled:opacity-50: dims when disabled -->
    <!-- => group-hover:ring-2: subtle hover ring on parent label hover -->
 
    <!-- Sliding knob -->
    <div
      class="
      absolute top-1 left-1 w-4 h-4 rounded-full
      bg-white dark:bg-gray-200
      shadow-sm
      peer-checked:translate-x-5
      transition-transform duration-200
      peer-disabled:opacity-80
    "
    >
      <!-- => absolute top-1 left-1: 4px from top and left edges -->
      <!-- => w-4 h-4: knob: 16px × 16px -->
      <!-- => rounded-full: circular knob -->
      <!-- => bg-white: white knob on gray track -->
      <!-- => shadow-sm: subtle knob shadow for depth -->
      <!-- => peer-checked:translate-x-5: slides 20px right when checked -->
      <!-- => transition-transform duration-200: smooth slide animation -->
    </div>
  </div>
 
  <!-- Label text -->
  <div class="select-none">
    <!-- => select-none: prevents accidental text selection on click -->
    <p class="text-sm font-medium text-gray-900 dark:text-white peer-disabled:opacity-50">
      <!-- => text-gray-900 dark:text-white: readable in both modes -->
      <!-- => peer-disabled:opacity-50: dims with the toggle when disabled -->
      Enable Notifications
    </p>
    <p class="text-xs text-gray-500 dark:text-gray-400">
      <!-- => text-gray-500: secondary description text -->
      Receive updates about your account
    </p>
  </div>
</label>

Key Takeaway: Production-quality components combine: semantic HTML (role="switch", aria-checked), peer-based visual state (peer-checked:translate-x-5), dark mode support (dark:bg-gray-700), keyboard focus visibility (peer-focus-visible:ring-2), disabled states (peer-disabled:opacity-50), and smooth transitions (transition-colors, transition-transform).

Why It Matters: This toggle component synthesizes every major Tailwind concept: peer modifiers for CSS-only state management, dark mode variants for theme compatibility, focus-visible for keyboard accessibility, group modifiers for hover regions, animation for smooth interaction, and ARIA for screen reader compatibility. Real production components require all of these simultaneously - a visually beautiful toggle that fails WCAG accessibility tests is unusable for enterprise customers who procure software through accessibility compliance audits. A perfectly accessible toggle that animates poorly feels cheap and unprofessional. The convergence of all these requirements in a single component is what advanced Tailwind proficiency enables: not knowing any one of these patterns, but understanding how they compose into production-quality interfaces that work for every user, on every device, in every context.

Last updated March 24, 2026

Command Palette

Search for a command to run...