# Tailwind CSS v4

You are working in a project styled with Tailwind CSS v4 (`tailwindcss` 4.x). Write code for the version in `package.json`, not the one in your training data: in v4 the configuration lives in CSS, and many v3 class names, directives and defaults changed or are gone.

## Before you write code

- Check `tailwindcss` in `package.json` and how it is built: `@tailwindcss/vite` in the Vite config, `@tailwindcss/postcss` in `postcss.config.mjs` (Next.js and other non-Vite builds), or `@tailwindcss/cli`.
- Find the stylesheet with `@import "tailwindcss"`: it is the configuration. Read its `@theme`, `@custom-variant`, `@utility`, `@source` and `@plugin` rules before adding classes or tokens.
- Look up default token names in `node_modules/tailwindcss/theme.css` (colors, spacing, radius, shadows, breakpoints) instead of guessing a class.
- A `tailwind.config.js` in a v4 project is only read when the CSS says `@config`. Never create one.

## Setup

- One line replaces the three `@tailwind` directives: `@import "tailwindcss";`. Import other CSS (`@import "tw-animate-css";`) after it.
- Vite: `import tailwindcss from "@tailwindcss/vite"` and `plugins: [tailwindcss()]`. PostCSS: `plugins: { "@tailwindcss/postcss": {} }`. Imports and vendor prefixes are built in: remove `postcss-import` and `autoprefixer`.
- Sources are detected automatically (git-ignored and binary files are skipped). Add others with `@source "../../packages/ui/src";`, exclude with `@source not "./legacy";`, set the root with `@import "tailwindcss" source("../src");`, or turn detection off with `source(none)` and list every `@source`. Safelist with `@source inline("bg-red-{500,600}");`.
- Plugins load in CSS: `@plugin "@tailwindcss/typography";`.
- v4 needs Safari 16.4+, Chrome 111+ and Firefox 128+, and is not meant to run through Sass, Less or Stylus.

## Theme in CSS

```css
@import "tailwindcss";

@custom-variant dark (&:where(.dark, .dark *));

@theme {
  --font-display: "Inter Variable", sans-serif;
  --color-brand: oklch(0.62 0.19 259);
  --breakpoint-3xl: 120rem;
  --animate-fade-in: fade-in 0.2s ease-out;
  @keyframes fade-in {
    from { opacity: 0; }
  }
}

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
}

:root { --background: oklch(1 0 0); --foreground: oklch(0.145 0 0); }
.dark { --background: oklch(0.145 0 0); --foreground: oklch(0.985 0 0); }
```

- Each namespace makes utilities: `--color-*` (bg, text, border, fill, ring…), `--font-*`, `--text-*` (with `--text-xl--line-height`), `--font-weight-*`, `--tracking-*`, `--leading-*`, `--breakpoint-*` (responsive variants), `--container-*` (container queries and `max-w-*`), `--radius-*`, `--shadow-*`, `--inset-shadow-*`, `--drop-shadow-*`, `--text-shadow-*`, `--blur-*`, `--ease-*`, `--animate-*`. `--spacing` is one base value: every multiple works (`w-17`, `mt-13`).
- Remove a default namespace with `--color-*: initial;` (or everything with `--*: initial;`) before defining your own.
- Use `@theme inline` when a token points at another variable, as semantic tokens switched by `.dark` do, so utilities read the variable where they are used. Write tokens as complete colors (`oklch(…)`) and use them as `var(--background)`; never wrap them in `hsl(var(--x))`.
- Theme variables are real CSS variables: in custom CSS write `var(--color-brand)`, `--spacing(4)` and `--alpha(var(--color-brand) / 50%)`, not `theme()`. Only tokens something uses are emitted; `@theme static` emits them all (for tokens read from JavaScript).

## Variants

- Dark mode follows `prefers-color-scheme` by default. For a class or attribute toggle, declare `@custom-variant dark (&:where(.dark, .dark *));` (or `[data-theme=dark]`) once.
- Define your own with `@custom-variant name (selector);` or the block form with `@slot`, and use any variant inside custom CSS with `@variant dark { … }`.
- Variants stack left to right like CSS: `*:first:pt-0` (v3 wrote `first:*:pt-0`).
- `hover:` applies only on devices that can hover. Use `not-*` (`not-last:`, `not-hover:`), `in-*` (an ancestor's state without a `group` class), `has-*`, `nth-*` (`nth-3:`, `nth-[2n+1]:`), `starting:` (`@starting-style` for enter transitions), `open:` (details, dialog and popovers), `inert:`, `user-invalid:`, `pointer-coarse:`, `noscript:`, `*:` (children) and `**:` (descendants).
- State from components: `data-open:` for a present attribute, `data-[state=open]:` or `data-[side=top]:` for a value, `aria-expanded:`, `aria-[sort=ascending]:`, and `group-data-open:` or `peer-invalid:` across elements.
- Container queries are built in: `@container` (or `@container/main`) on the parent, `@md:`, `@max-md:` or `@md/main:` on children. Prefer them for components that live in columns of any width.

## Utilities and values

- CSS variables in arbitrary values use parentheses: `bg-(--brand)`, `w-(--sidebar-width)`, `text-(length:--size)` with a type hint when ambiguous. Brackets are for literal values: `grid-cols-[1fr_auto]`, `bg-[#0b0b0b]`.
- Opacity is a modifier on the color: `bg-black/50`, `text-primary/80`, `bg-(--brand)/20`. `bg-opacity-*` and `text-opacity-*` no longer exist.
- Important goes at the end: `flex!`. The leading `!flex` still compiles but is the old form.
- Gradients: `bg-linear-to-r`, `bg-linear-45`, `bg-radial`, `bg-conic`, with interpolation like `bg-linear-to-r/oklch`.
- Renamed scales (v3 to v4): `shadow-sm` is `shadow-xs` and `shadow` is `shadow-sm`; the same shift applies to `rounded`, `blur` and `drop-shadow` (`rounded-sm` to `rounded-xs`, `rounded` to `rounded-sm`). v3's `outline-none` is v4's `outline-hidden` (invisible, but kept in forced-colors mode); v4's `outline-none` removes the outline everywhere. `ring` is 1px (`ring-3` is v3's default). `shrink-*`, `grow-*` and `text-ellipsis` replace `flex-shrink-*`, `flex-grow-*` and `overflow-ellipsis`.
- Defaults changed: borders and rings are `currentColor` (always give `border-*` and `ring-*` a color or token), buttons get `cursor: default` (add `cursor-pointer` where the design wants it), placeholders are the text color at 50%, and `container` has no `center` or `padding` options (customize it with `@utility container { … }`).
- Prefer `flex` or `grid` with `gap-*` to `space-*` and `divide-*`, whose selectors changed to `:not(:last-child)`.
- Use what v4 adds instead of custom CSS: `size-*`, logical spacing and insets (`ms-*`, `pe-*`, `start-*`, `mbs-*`, `pbe-*`, `inset-bs-*`), `text-shadow-*`, `mask-*`, `inset-shadow-*`, `inset-ring-*`, 3D transforms (`rotate-x-*`, `perspective-*`, `transform-3d`), `field-sizing-content`, `wrap-break-word`, `scheme-dark`, `scrollbar-thin`, `justify-center-safe`.

## Custom CSS

- Custom utilities use `@utility`, which makes them work with every variant and sort correctly. A functional one reads its value with `--value()`:

```css
@utility content-auto {
  content-visibility: auto;
}

@utility tab-* {
  tab-size: --value(integer);
}
```

- Element defaults go in `@layer base`, and classes that utilities must override (third-party widgets) in `@layer components`. Custom utilities in `@layer utilities` are v3: they get no variants.
- In CSS modules and Vue, Svelte or Astro `<style>` blocks, start with `@reference "../app.css";` (or `@reference "tailwindcss";`) so `@apply` and theme variables resolve without emitting a second copy of Tailwind.
- Keep `@apply` for markup you cannot put classes on (third-party HTML, Markdown output). Components belong in JSX or templates, with class strings there.

## Class names in components

- Write every class name in full in the source: the scanner reads files as text and never runs code. `bg-${color}-500` produces nothing; map props to complete strings (`{ danger: "bg-red-600", info: "bg-sky-600" }`) or variants with `cva` or `tailwind-variants`.
- Merge caller classes with `cn()` (`twMerge(clsx(…))`, tailwind-merge 3 for v4) so the last conflicting class wins; class order in the attribute does not decide it on its own.
- Use theme tokens (`bg-background`, `text-muted-foreground`, `bg-brand`) over raw palette colors in a themed project, and over hex values everywhere.

## Migrating from v3

- Run `npx @tailwindcss/upgrade` on a clean branch (Node 20 or later). It moves the config into CSS, updates dependencies and renames classes in templates. Then fix by hand what it cannot see: class names built from strings, JavaScript plugins, `theme()` calls and custom `@layer` rules.
- Delete `tailwind.config.js` once its values live in `@theme`, and replace `tailwindcss-animate` with `tw-animate-css`.
- Grep for leftovers: `@tailwind`, `bg-opacity-`, `flex-shrink-`, `outline-none`, bare `shadow`, `rounded` and `ring`, and `space-y-` on inline children.

## Do not

- Do not create `tailwind.config.js`, write `@tailwind` directives, or add `postcss-import` and `autoprefixer`.
- Do not build class names from strings, or reach for `@apply` to make component classes.
- Do not use v3 names (`bg-opacity-*`, `flex-grow`, `bg-gradient-to-*`, `overflow-ellipsis`) or assume v3 sizes for `shadow`, `rounded`, `blur` and `ring`.
- Do not write `hsl(var(--token))`, `theme(colors.x)` or hex values where a token exists.

## Before you finish

- Run the dev server or build and check the classes you added appear in the CSS. A class with no effect is usually misspelled, built from a string, or in a file outside the sources.
- Check every breakpoint and container size you touched, dark mode, and the keyboard focus styles.
- After a migration, compare key pages against v3 screenshots for borders, rings, shadows and radius.
