Back to blog

React Components with Tailwind CSS v4: What's New and How to Use It

Published on March 12, 2026·6 min read

Tailwind CSS v4 rewrites the configuration model entirely. There is no tailwind.config.js anymore — theme customization moves into CSS with the @theme directive, and the PostCSS setup gets dramatically simpler. For React developers, this unlocks a pattern that wasn't practical before: seamlessly mixing Tailwind utility classes with CSS custom properties for runtime theming. Here's everything you need to know to build components with v4.

What Changed from v3 to v4

The headline changes affect how you configure Tailwind and how your build pipeline works:

| Area | v3 | v4 | |------|----|----| | Configuration file | tailwind.config.js | No JS config — CSS only | | Theme customization | theme.extend in JS | @theme directive in CSS | | PostCSS plugin | tailwindcss | @tailwindcss/postcss | | Content detection | content: [...] array | Automatic (scans project) | | CSS import | @tailwind base/components/utilities | @import "tailwindcss" | | Custom properties | Manual with theme() function | First-class via @theme | | Arbitrary values | bg-[#ff0000] | Still works, unchanged |

Content detection is now automatic — v4 scans your project files without you listing globs. This alone eliminates a common source of "my class isn't applying" bugs.

PostCSS Setup

Install only one package:

npm install -D @tailwindcss/postcss

Your postcss.config.mjs:

export default {
  plugins: {
    "@tailwindcss/postcss": {},
  },
};

That's it. No autoprefixer needed — v4 handles vendor prefixes internally.

In your global CSS file (app/globals.css):

@import "tailwindcss";

One line replaces the three @tailwind directives from v3.

The @theme Directive

Custom design tokens live in a @theme block in your CSS. These tokens become both Tailwind utility classes and CSS custom properties simultaneously:

/* app/globals.css */
@import "tailwindcss";

@theme {
  --color-brand: #818cf8;
  --color-brand-hover: #6366f1;
  --color-surface: #111111;

  --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;

  --radius-sm: 0.5rem;
  --radius-md: 0.75rem;
  --radius-lg: 1rem;
  --radius-full: 9999px;

  --spacing-section: 6rem;
}

Once defined in @theme, you can use these tokens in two ways:

// As Tailwind classes
<div className="bg-brand text-surface rounded-lg p-section" />

// As CSS custom properties in inline styles
<div style={{ background: "var(--color-brand)" }} />

Both are valid. The choice depends on whether you need runtime overrides.

CSS Custom Properties for Multi-Theme Support

The real power of v4 is runtime theming. CSS custom properties can be overridden at any level in the DOM tree — change the data-theme attribute on a parent element and all its children update instantly, with zero JavaScript and zero class name changes:

/* Theme A — light */
[data-theme="light"] {
  --color-background: #ffffff;
  --color-foreground: #18181b;
  --color-accent: #a3e635;
  --color-accent-hover: #84cc16;
  --color-border: #e4e4e7;
}

/* Theme B — dark */
[data-theme="dark"] {
  --color-background: #09090b;
  --color-foreground: #fafafa;
  --color-accent: #818cf8;
  --color-accent-hover: #6366f1;
  --color-border: #27272a;
}

Switch themes in React:

"use client";

import { useState } from "react";

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<"light" | "dark">("light");

  return (
    <div data-theme={theme}>
      <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
        Toggle theme
      </button>
      {children}
    </div>
  );
}

Any component that uses var(--color-background) or var(--color-accent) automatically responds to the theme switch.

Mixing Tailwind Classes with CSS Variables — The Correct Pattern

The most common mistake when migrating to v4 is mixing Tailwind's static colors with CSS variable-based tokens in the same component. This breaks theming:

// Wrong — mixes static colors with themed tokens
<button className="bg-indigo-500 text-white rounded-full px-6 py-3">
  Click me
</button>

// Correct — uses only themed tokens
<button
  style={{
    background: "var(--color-accent)",
    color: "var(--color-background)",
    borderRadius: "var(--radius-full)",
    padding: "0.75rem 1.5rem",
    border: "none",
    cursor: "pointer",
  }}
>
  Click me
</button>

For layout and spacing utilities that don't need to be themeable, Tailwind classes are perfectly fine:

// Layout classes — not theme-sensitive, use Tailwind
<section className="flex flex-col items-center gap-8 py-24">
  {/* Content with themed colors */}
  <h2 style={{ color: "var(--color-foreground)", fontWeight: 700 }}>
    Section Heading
  </h2>
  <p style={{ color: "var(--color-foreground-muted)" }}>
    Description text
  </p>
</section>

This hybrid approach gives you Tailwind's DX for layout and sizing, plus full runtime theming for visual properties.

Arbitrary Values Still Work

v4 keeps the [value] arbitrary value syntax from v3:

<div className="w-[calc(100%-2rem)] mt-[3.75rem] grid-cols-[1fr_2fr_1fr]" />

You can also use CSS variables directly in arbitrary values:

<div className="bg-[var(--color-accent)] text-[var(--color-background)]" />

This is useful when you need Tailwind's responsive prefixes alongside CSS variable values:

<div className="p-4 md:p-[var(--container-padding-x)]" />

Component Example: A Themed Button

Here's a complete, themed button component using v4 patterns:

// components/Button.tsx
interface ButtonProps {
  children: React.ReactNode;
  variant?: "primary" | "secondary" | "ghost";
  size?: "sm" | "md" | "lg";
  href?: string;
  onClick?: () => void;
  disabled?: boolean;
}

const sizeStyles = {
  sm: { padding: "0.5rem 1rem", fontSize: "0.8125rem" },
  md: { padding: "0.75rem 1.5rem", fontSize: "0.875rem" },
  lg: { padding: "0.875rem 2rem", fontSize: "0.9375rem" },
};

const variantStyles = {
  primary: {
    background: "var(--color-accent)",
    color: "var(--color-foreground)",
    border: "none",
  },
  secondary: {
    background: "transparent",
    color: "var(--color-foreground)",
    border: "1px solid var(--color-border)",
  },
  ghost: {
    background: "transparent",
    color: "var(--color-foreground-muted)",
    border: "none",
  },
};

export function Button({ children, variant = "primary", size = "md", href, onClick, disabled }: ButtonProps) {
  const style = {
    ...sizeStyles[size],
    ...variantStyles[variant],
    borderRadius: "var(--radius-full)",
    fontWeight: 600,
    cursor: disabled ? "not-allowed" : "pointer",
    opacity: disabled ? 0.5 : 1,
    transition: `all var(--duration-normal) var(--ease-out)`,
    textDecoration: "none",
    display: "inline-flex",
    alignItems: "center",
    gap: "0.5rem",
  };

  if (href) {
    return <a href={href} style={style}>{children}</a>;
  }

  return (
    <button onClick={onClick} disabled={disabled} style={style}>
      {children}
    </button>
  );
}

This button works correctly on all 7 Incubator themes because it reads from CSS variables, not hardcoded color values.

Migration from v3 to v4

The main steps when migrating an existing project:

  1. Replace the PostCSS plugin: tailwindcss@tailwindcss/postcss, remove autoprefixer
  2. Replace the CSS imports: Three @tailwind directives → @import "tailwindcss"
  3. Move custom theme to @theme: Copy values from tailwind.config.js@theme {} in CSS
  4. Update deprecated utilities: bg-opacity-*bg-black/50, ring-offset-* → removed
  5. Delete tailwind.config.js: Or keep it temporarily if you need backward compat

The v4 migration guide at tailwindcss.com covers edge cases, but for most projects the upgrade is under an hour.

See v4 in Action

All 449 sections in the Incubator catalog are built with Tailwind CSS v4, using the hybrid approach described above — Tailwind utilities for layout, CSS custom properties for theming. Browse the catalog to see v4 patterns applied to real-world components across every section type.

VA

Victor Aubague

Developer & creator of Incubator

Full-stack developer specialized in React, Next.js, and TypeScript. I built Incubator to help developers ship beautiful interfaces faster — all components are crafted from real client projects and production code.

LinkedIn
React Components with Tailwind CSS v4: What's New and How to Use It — Incubator