On a Vite project, install tailwindcss and @tailwindcss/vite with npm, then register the plugin in vite.config. After that, a single @import "tailwindcss"; in the main CSS file finishes the setup.

From there you write utility classes directly in className. Only the CSS those classes need ends up in the build.

Tailwind Labs released version 4.0 on January 22, 2025. It dropped the three @tailwind directives and the content array, which most older setup guides still teach. If a guide has you create a tailwind.config.js file or run npx tailwindcss init, it’s describing version 3.

How do you install Tailwind CSS in a React project?

Run npm to install tailwindcss and @tailwindcss/vite, register the plugin in the Vite config, and put @import "tailwindcss"; in your main CSS file. A Vite-based React app needs nothing more than that.

No wrapper components are involved. Classes go into className, and the build writes out only the CSS they use (if the framework itself is new to you, start with a primer on what the framework is).

The commands below follow the official installation guides as of Tailwind CSS v4.3.

Installing with the Vite plugin

  1. Create the project with npm create vite@latest my-react-app -- --template react, then run cd my-react-app and npm install.
  2. Add Tailwind with npm install tailwindcss @tailwindcss/vite.
  3. Register the plugin in vite.config.js, next to the React plugin.
  4. Import Tailwind in src/index.css and make sure main.jsx imports that file.
  5. Run npm run dev and put a class such as text-3xl font-bold underline on an element.

vite.config.js

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
plugins: [react(), tailwindcss()],
})

src/index.css

@import "tailwindcss";

You don’t need a tailwind.config.js or a postcss.config.js here, which makes this about as painless as a CSS setup gets. The upgrade guide prefers the Vite plugin to the PostCSS one because it performs better.

Setting up Next.js and other React frameworks

Next.js goes through the PostCSS plugin, since there is no Vite plugin to use.

Install it with npm install tailwindcss @tailwindcss/postcss postcss. Then add "@tailwindcss/postcss": {} to the plugins object in postcss.config.mjs, and put @import "tailwindcss"; in ./app/globals.css.

Any React setup without a Vite plugin can take the same PostCSS route. Tailwind also publishes guides for React Router, TanStack Start, Gatsby, Parcel and Rspack.

Is responsive design still a top priority?

Explore the latest responsive design statistics: adoption rates, performance impact, user behavior, and trends shaping modern websites.

See the Numbers →

Adding Tailwind CSS to an existing React project

An app that already has styles gets the same install, and then it may need a little protection against collisions.

If your own class names clash with Tailwind utilities, add a prefix with @import "tailwindcss" prefix(tw); and the classes read tw:flex. Legacy CSS with high-specificity rules that beat utilities is a different problem, and @import "tailwindcss" important; handles that one.

Preflight, the base layer Tailwind imports, deserves a look as well. In v4, buttons use cursor: default and dialog elements lose their default margin (Tailwind upgrade guide).

Using Create React App

A Create React App project being set up

Create React App is a poor starting point in 2026.

The React team deprecated it on February 14, 2025. It sits in maintenance mode with no active maintainers, and new React apps are meant to start with a framework. When a framework is a poor fit, its migration guidance points to a build tool such as Vite, Parcel or Rsbuild. Tailwind’s own framework guides don’t list Create React App either.

Any tutorial that has you run npx tailwindcss init -p and fill in a content array is teaching the v3 workflow, and the three @tailwind directives it adds give it away too. On a fresh project, follow the Vite steps above.

What changed between Tailwind CSS v3 and v4 for React projects?

Version 4 swaps the three @tailwind directives for a single @import. Configuration moves out of tailwind.config.js and into CSS, and the content array goes away because source files are found automatically.

The import line tells you which version a tutorial teaches. Three @tailwind directives mean v3.

AspectTailwind CSS v3Tailwind CSS v4
Import in CSSThree @tailwind directives (base, components, utilities)One @import “tailwindcss”
Configurationtailwind.config.js@theme block in CSS
Source detectioncontent array of pathsAutomatic, with @source for extras
PostCSS setuptailwindcss plugin plus Autoprefixer@tailwindcss/postcss, prefixing built in
Vite setupPostCSS route@tailwindcss/vite plugin
Browser supportOlder browsers (v3.4)Chrome 111, Safari 16.4, Firefox 128 and newer

Tailwind Labs benchmarked both versions on its Catalyst template. A full build dropped from 378ms in v3.4 to 100ms in v4.0, and an incremental rebuild with no new CSS dropped from 35ms to 192 microseconds (Tailwind CSS v4.0 announcement, January 2025).

Speed alone rarely justifies a migration, though. What matters more is the browser baseline.

Version 4 depends on modern CSS features such as @property and color-mix(), so it does not run in older browsers, per the upgrade guide. Teams with strict cross-browser support requirements should stay on v3.4 until those requirements change.

In practice, new React projects should start on v4, while a v3.4 project can stay put until browser support, not build speed, gives you a reason to move.

For an existing v3 project, run npx @tailwindcss/upgrade on a new branch. It needs Node.js 20 or higher, and in most projects it migrates the dependencies and the config file, plus the template changes.

Review the diff and test in the browser afterward. Complex projects still need a few manual fixes.

Customizing colors, fonts and breakpoints with the @theme directive

Add an @theme block to the CSS file that imports Tailwind, and every variable inside it becomes a utility class or a variant. A variable named --color-mint-500 produces bg-mint-500, text-mint-500 and fill-mint-500.

@import "tailwindcss";

@theme {
--color-mint-500: oklch(0.72 0.11 178);
--font-display: "Satoshi", sans-serif;
--breakpoint-3xl: 120rem;
}

The new tokens work in JSX right away.

<h1 className="font-display text-mint-500 3xl:text-6xl">Pricing</h1>

The prefix on a variable name decides what it generates. --color-* makes color utilities such as bg-mint-500, and --font-* makes font family utilities such as font-display. Breakpoints come from --breakpoint-*, which gives you variants like 3xl:. Border radius utilities such as rounded-lg come from --radius-*.

Redefine a default variable to override it. Setting --breakpoint-sm: 30rem; moves the sm: variant from the default 40rem down to 30rem.

To drop a whole default namespace, set it to initial, as in --color-*: initial;. Only your own colors generate utilities after that.

Every theme variable is also a plain CSS variable, so React code can read it directly with style={{ backgroundColor: "var(--color-mint-500)" }}. Tailwind’s theme documentation shows the same pattern with the Motion animation library.

Use @theme inline when a token points at another variable, such as a font variable that a framework injects. Without it, the utility can resolve in the wrong scope and fall back to sans-serif.

A v3 tailwind.config.js is no longer picked up automatically. Load it with @config "../../tailwind.config.js";, or let the upgrade tool move its contents into CSS.

Theme variables have to sit at the top level of the stylesheet, not inside a selector or media query. Breakpoints should also share one unit, since mixed units can sort the generated utilities in the wrong order.

Plain CSS variables that should not create a utility belong in :root instead.

How to style React components with utility classes in className

Put the utility classes in the className prop as one space-separated string. Each class sets a single property, so a button gets by with only a handful.

export default function SaveButton() {
return (
<button className="rounded-md bg-sky-500 px-4 py-2 font-semibold text-white hover:bg-sky-700">
Save changes
</button>
)
}

JSX uses className, not class. Markup copied from Tailwind examples goes in faster if you run it through a converter that turns HTML into JSX.

One-off values get their own syntax. Square brackets take any value, as in bg-[#316ff6] or grid-cols-[24rem_2.5rem_minmax(0,1fr)], and underscores stand in for spaces. In v4, parentheses reference a CSS variable instead, as in bg-(--brand-color), because the v3 square-bracket form changed.

The important modifier goes at the end of the class in v4, as in bg-red-500!. The leading ! still works but is deprecated.

Values that arrive at runtime, such as a color from an API, belong in an inline style. Set a CSS variable there and reference it from the class.

<button
style={{ "--bg-color": buttonColor }}
className="rounded-md bg-(--bg-color) px-3 py-1.5"
>
Buy now
</button>

Template literals work as long as each utility is written out in full somewhere in the file. Building a class name from a variable, such as text-${color}-600, generates no CSS for it.

How do responsive, hover and dark mode variants work in React?

A variant is a prefix plus a colon in front of any utility. md:flex applies from the medium breakpoint up and hover:bg-sky-700 applies on hover. dark:bg-gray-800 only kicks in for dark mode.

Responsive breakpoints

Unprefixed utilities apply at every screen size, and prefixed ones apply at that breakpoint and above. So text-center sm:text-left centers text on phones and left-aligns it from 640px.

Tailwind’s responsive design documentation sets five defaults.

  • sm: at 40rem (640px)
  • md: at 48rem (768px)
  • lg: at 64rem (1024px)
  • xl: at 80rem (1280px)
  • 2xl: at 96rem (1536px)

Don’t read sm: as “on small screens”. It means “at the small breakpoint and up”, so phone styles stay unprefixed.

Write the phone layout first and add overrides at larger breakpoints. That small-screen-first way of designing is what the breakpoint system assumes.

Each breakpoint compiles to a width condition in a CSS media query, such as @media (width >= 40rem) for sm:.

Stack a max-* variant to limit a style to a range. md:max-xl:flex applies only between 48rem and 80rem.

Reusable components often need to respond to their parent’s width instead of the viewport. Version 4 ships container queries in core, so you mark the parent with @container and use @md: on its children, with no plugin (Tailwind CSS v4.0 announcement).

Hover and focus states

Prefixes such as hover:, focus: and active: style interaction states. Variants stack, so disabled:hover:bg-sky-500 targets an element that is both disabled and hovered.

On touch devices, the v4 hover variant applies only when the primary input device supports hover, so a tap on a touch screen does not trigger it. To restore the old behavior, add @custom-variant hover (&:hover); to your CSS.

Stacked variants also apply in a different order now. v4 reads them left to right, where v3 read them right to left, so a v3 class such as first:*:pt-0 becomes *:first:pt-0 (upgrade guide).

Dark mode with a manual toggle

By default, dark: follows the operating system through the prefers-color-scheme media feature. A toggle button needs a CSS selector instead.

  1. Override the variant in CSS after the Tailwind import: @custom-variant dark (&:where(.dark, .dark *));
  2. Toggle a dark class on the html element from a React component.
  3. Save the choice in localStorage so it survives a reload.
import { useEffect, useState } from "react";

export default function ThemeToggle() {
const [dark, setDark] = useState(
() =>
localStorage.theme === "dark" ||
(!("theme" in localStorage) &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
);

useEffect(() => {
document.documentElement.classList.toggle("dark", dark);
localStorage.theme = dark ? "dark" : "light";
}, [dark]);

return (
<button
onClick={() => setDark(!dark)}
className="rounded-md bg-gray-200 px-3 py-1 dark:bg-gray-700 dark:text-white"
>
{dark ? "Light" : "Dark"}
</button>
);
}

Tailwind’s dark mode guide suggests running the first class toggle inline in the document head to avoid a flash of the wrong theme. A useEffect runs after the first paint, so this component alone can flash on load.

A data attribute works too: @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *)); with data-theme="dark" on the html element.

On v3.4.1 or later, the equivalent is darkMode: 'selector' in tailwind.config.js, which replaced the older class strategy (Tailwind v3 documentation).

How does Tailwind CSS detect classes in React files?

Tailwind scans every source file as plain text and collects anything that could be a class name. CSS gets generated only for tokens that match a known utility, and your JSX is never parsed as code.

Any file with a class written out as literal text counts, whether that’s a component or a data file, or even a Markdown file. The official guide on detecting classes in source files lists what gets skipped as of Tailwind CSS v4.3.

  • Files listed in .gitignore
  • The node_modules directory
  • Binary files such as images, videos and archives
  • CSS files
  • Common package manager lock files

A few directives override those defaults.

  • @source adds a path relative to the stylesheet, which a Tailwind-based component library inside node_modules needs, as in @source "../node_modules/@acmecorp/ui-lib";
  • @source not excludes a path, such as @source not "../src/components/legacy";
  • source() sets the base path on the import, as in @import "tailwindcss" source("../src");. Monorepos that run builds from the root rely on it.
  • source(none) turns automatic detection off, so you register every path yourself.

Classes that never appear in your source, such as ones stored in a CMS or database, need @source inline(). It replaces the v3 safelist option, which v4 does not support (Tailwind upgrade guide).

@import "tailwindcss";
@source inline("{hover:,focus:,}underline");

The argument is brace-expanded. @source inline("{hover:,}bg-red-{50,{100..900..100},950}"); generates red backgrounds at 50, 100 through 900 in steps of 100, and 950, each with a hover: variant.

@source not inline() does the reverse and blocks specific classes even when they appear in source.

How to apply conditional and dynamic classes in React components

Write out every possible class in full and pick between the whole strings at render time. Ternaries cover two states and lookup objects cover more. clsx joins the pieces together, and tailwind-merge sorts out clashes.

Ternaries and lookup objects

Use a ternary for two states, and a lookup object keyed by prop value for anything beyond that.

const colorVariants = {
blue: "bg-blue-600 hover:bg-blue-500 text-white",
red: "bg-red-500 hover:bg-red-400 text-white",
yellow: "bg-yellow-300 hover:bg-yellow-400 text-black",
};

function Button({ color, children }) {
return (
<button className={`${colorVariants[color]} rounded-md px-3 py-1.5`}>
{children}
</button>
);
}

The mapping also lets each value pick its own shade and text color, which a built-up string cannot do.

In TypeScript, derive the prop type from the object so an unknown color fails at compile time.

type ButtonColor = keyof typeof colorVariants;

Joining classes with clsx

The clsx library weighs 239 bytes gzipped, according to its repository. It takes plain strings, and objects or arrays when you need them.

import clsx from "clsx";

const classes = clsx(
"rounded-md px-3 py-1.5",
isActive && "bg-sky-600 text-white",
{ "opacity-50": disabled }
);

Falsy values get dropped, so false and null never reach the DOM. The clsx/lite build is stricter and accepts only strings, ignoring anything else.

The catch is that clsx only joins. It doesn’t resolve conflicts, so two padding classes in one string both reach the browser.

Resolving conflicts with tailwind-merge

When two classes target the same property, the one that appears later in the generated stylesheet wins, not the one later in the attribute. Tailwind’s documentation shows grid flex producing display: grid even though flex comes last.

That makes a className prop unreliable as an override. The twMerge function drops the earlier class when a later one conflicts with it.

import { twMerge } from "tailwind-merge";

twMerge("px-2 py-1 bg-red hover:bg-dark-red", "p-3 bg-[#B91C1C]");
// "hover:bg-dark-red p-3 bg-[#B91C1C]"

Most React projects pair it with clsx in one helper.

import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs) {
return twMerge(clsx(inputs));
}

function Card({ className, ...props }) {
return <div className={cn("rounded-lg bg-white p-6", className)} {...props} />;
}

On versions, tailwind-merge 3.x supports Tailwind CSS v4.0 up to v4.3, while Tailwind v3 projects need tailwind-merge 2.6.0 (project README).

It isn’t free, either. Its class-conflict config accounts for about 5 kB of a roughly 7 kB minified and gzipped bundle, according to the maintainer’s documentation.

The sources disagree on the pattern itself. The maintainer’s guide names merging a className prop as the library’s main job, while Tailwind’s styling guide advises exposing specific props instead of accepting outside classes, because those classes often conflict.

For classes defined inside one component, the maintainer recommends twJoin, which skips conflict resolution and costs about the same as clsx. Treat twMerge as an escape hatch, not the default.

Should you reuse Tailwind styles with components, class-variance-authority or @apply?

Extract a React component first. Reach for class-variance-authority once one component has several variants, and keep @apply for small patterns in CSS files.

Tailwind’s own guidance suggests doing even less at first. Markup rendered in a loop only holds its class list once, and repeats inside a single file are often faster to fix with multi-cursor editing than with a new abstraction.

Extracted components

A component is the right unit when styles repeat across files.

export function StatusBadge({ children }) {
return (
<span className="rounded-full bg-emerald-100 px-2 py-0.5 text-xs font-medium text-emerald-800">
{children}
</span>
);
}

You get one source of truth for markup and styles, no extra dependency, and props that expose only the variations you allow. It works the same in Vite, Next.js and every other setup. The downside is that variants multiply conditional branches as props grow, and the team needs a convention for callers that pass extra classes.

class-variance-authority

The cva library is 596 bytes minified and Brotli-compressed (cva documentation), and it turns variant props into class strings with a typed API.

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

const button = cva("rounded font-semibold", {
variants: {
intent: {
primary: "bg-blue-500 text-white",
secondary: "bg-white text-gray-800 border border-gray-400",
},
size: {
small: "px-2 py-1 text-sm",
medium: "px-4 py-2 text-base",
},
},
compoundVariants: [
{ intent: "primary", size: "medium", class: "uppercase" },
],
defaultVariants: { intent: "primary", size: "medium" },
});

// button({ intent: "secondary", size: "small" })
ProsCons
Variants declared once in a typed objectClass strings live away from the markup
compoundVariants cover combinationsOne more API for the team to learn
defaultVariants remove repeated propsOverkill for a component with one variant

@apply and @layer components

@apply makes sense when markup comes from a template without components, or when a third-party widget needs styles you cannot reach with classes. Tailwind’s documentation accepts plain custom CSS in those cases.

It breaks down when the stylesheet is processed apart from your main CSS file. CSS module files cannot see theme variables, so @apply needs @reference "../app.css"; at the top.

Classes defined in @layer components can still be overridden by utilities, as in <div className="card rounded-none">. Use @utility instead when a custom class must work with variants such as hover: and lg:.

The upside is short class names in markup, with utilities still able to override them. The downside is that it brings back named CSS classes, which is the habit utility classes exist to avoid.

Component sets built on Tailwind CSS

shadcn/ui keeps no hidden abstraction between you and the component code. It is one of the better-known Tailwind-based UI component libraries.

As of October 2026, its documentation lists Tailwind v4 and React 19 support. New projects start on v4, while existing v3 and React 18 apps keep working and receive v3 components until you upgrade.

Headless UI, published by Tailwind Labs, ships unstyled components for React and Vue, so you add the Tailwind classes yourself. Its focus is accessible interface components such as menus, dialogs and tabs.

For a handful of buttons and inputs, an extracted component beats adding a library. That’s my preference, anyway.

Editor tools that make Tailwind CSS classes easier to manage in React

Install Tailwind CSS IntelliSense for autocomplete and linting, and prettier-plugin-tailwindcss for automatic class sorting. Both are official, per Tailwind’s editor setup guide.

ToolJobRuns in
Tailwind CSS IntelliSenseAutocomplete, linting, hover previews, syntax highlightingVS Code, Cursor
Built-in Tailwind supportAutocomplete, linting, hover previewsZed
prettier-plugin-tailwindcssSorts classes in recommended orderAnywhere Prettier runs
Built-in Tailwind CSS supportClass completions in HTMLJetBrains IDEs such as WebStorm and PhpStorm

Version 4 adds custom at-rules such as @theme, @variant and @source. Strict editors can flag them, and the IntelliSense language mode understands all of them. In some editors you also need to switch off native CSS validation.

For Prettier, install the plugin and then register it in the config.

npm install -D prettier prettier-plugin-tailwindcss
{
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindStylesheet": "./src/index.css",
"tailwindFunctions": ["clsx", "cva", "cn"]
}

The tailwindStylesheet option is required on v4 and points at the CSS file that imports Tailwind. The path resolves relative to the Prettier config file. tailwindFunctions sorts strings inside the listed function calls, and without it the plugin sorts only the class attribute, framework equivalents such as className, and @apply directives.

When other Prettier plugins are present, this one must load last. You also need Prettier v3 or newer, and the plugin has been ESM-only since 0.5.x.

The plugin turns text-white px-4 sm:px-8 py-2 sm:py-3 bg-sky-700 hover:bg-sky-800 into bg-sky-700 px-4 py-2 text-white hover:bg-sky-800 sm:px-8 sm:py-3.

IntelliSense does not read clsx() calls by default. The clsx README gives a tailwindCSS.experimental.classRegex setting for VS Code that adds autocomplete inside them.

Why Tailwind CSS classes stop applying in React, and when the framework is the wrong fit

Most broken-looking Tailwind CSS in React after an upgrade comes from v4 defaults that changed, not from a bug. The table below uses the upgrade guide and the custom styles documentation.

SymptomCauseFix
Borders turn darkDefault border color is currentColor, not gray-200Name a color, such as border-gray-200
Shadows and corners look offv3 sm values are now xs, and bare names moved up to smReplace sm with xs, then bare with sm
Ring looks thin and no longer blueRing width fell from 3px to 1px, and the default color changed from blue-500 to currentColorUse ring-3 ring-blue-500
transform-none no longer resets a scaleRotate, scale and translate are separate CSS propertiesReset each one, such as scale-none
text-(–my-var) sets the wrong propertyThe value could be a font size or a colorAdd a type hint: text-(length:–my-var)

Sass, Less and Stylus

Tailwind CSS v4 is not designed for Sass, Less or Stylus. The documentation tells you to treat Tailwind itself as the preprocessor.

Imports are bundled by Tailwind through @import, and nested CSS is flattened with Lightning CSS. Variables are native CSS custom properties.

Teams with a large Sass codebase should read a Tailwind versus Sass comparison before migrating. Running both in one stylesheet pipeline is unsupported.

CSS Modules

With 50 CSS modules in a project, Tailwind runs 50 separate times, because Vite, Parcel and Turbopack process each module on its own (Tailwind compatibility documentation).

The documentation does not recommend combining the two when you can avoid it. Utility classes are already scoped, since each one does the same thing wherever it appears.

If you have to combine them, write background: var(--color-blue-500); in the module instead of using @apply. Tailwind can then skip processing that file.

Underscores and escapes in arbitrary values

Tailwind converts underscores inside arbitrary values to spaces at build time, which causes a few snags in React. URLs keep their underscores where a space would be invalid, as in bg-[url('/what_a_rush.png')]. For a literal underscore, escape it with a backslash, as in before:content-['hello\_world'].

JSX strips that backslash from the rendered output, though, so wrap the string in String.raw.

<div className={String.raw`before:content-['hello\_world']`}>...</div>

Tailwind CSS in React FAQ

Can Tailwind CSS be used with React Native?

Not directly. Tailwind CSS compiles utility classes into browser CSS, which React Native does not use.

Libraries such as NativeWind and Uniwind translate Tailwind-style class names into native styles instead.

Do I still need Autoprefixer and PostCSS in Tailwind CSS v4?

No. Version 4 handles imports and vendor prefixing itself, so the upgrade guide tells you to remove postcss-import and autoprefixer from the project.

Vite projects skip PostCSS through the @tailwindcss/vite plugin, while Next.js still uses the @tailwindcss/postcss plugin.

Does Tailwind CSS need extra setup for TypeScript in React?

No. Classes are plain strings in className, so TypeScript needs no Tailwind-specific configuration.

Tailwind’s Next.js guide creates the project with the --typescript flag and uses a page.tsx file with the same install steps. The only typing work is on your own component props.

Should I use Tailwind CSS or styled-components in a React app?

The choice comes down to where styles are generated. Tailwind CSS has zero runtime: it scans your files at build time and writes a static CSS file, according to its documentation.

styled-components is a CSS-in-JS library, so styles are written in JavaScript and live inside the component.

Moving an Existing React App to Tailwind CSS v4

An existing React app moves to Tailwind CSS v4 most safely when the steps are ordered by how costly each mistake is to reverse.

  1. Check visitor browsers against the v4 baseline.
  2. Run the migration on a separate branch.
  3. Compare borders, rings and shadows in the browser.
  4. Replace old custom CSS with utilities page by page.

The browser check comes first because no code change fixes an unsupported browser, while every later step is one revert away.

Each step carries a trade-off. Version 4 gives up older browsers in exchange for modern CSS, and utility classes keep the stylesheet from growing at the price of longer class lists in the markup.

This advice changes if Tailwind ships the compatibility mode its upgrade guide describes as under exploration. The position was verified against the v4.3 documentation in October 2026.

Bogdan Sandu
Latest posts by Bogdan Sandu (see all)