# Tailwind CSS v4 practical guide [2026 edition] — CSS-first design, design tokens, dark mode, and a11y at production quality

> A complete guide to mastering Tailwind CSS v4's CSS-first configuration (@import / @theme / @custom-variant) at production quality. With real code it explains single-source management of design tokens, the pitfall of runtime-switching dark-mode design, fluid typography, container queries, and accessibility support such as prefers-contrast / forced-colors.

- Published: 2026-06-24
- Author: 友田 陽大
- Tags: Tailwind CSS, フロントエンド, アクセシビリティ, Next.js, アーキテクチャ設計, パフォーマンス
- URL: https://tomodahinata.com/en/blog/tailwind-css-v4-css-first-design-tokens-production-guide
- Category: Frontend
- Pillar guide: https://tomodahinata.com/en/blog/nextjs-16-app-router-cache-components-data-fetching

## Key points

- Tailwind v4 moves configuration from JavaScript to CSS, defining design tokens with @import and @theme.
- Consolidate design tokens in @theme and utilities like bg-primary auto-generate, becoming DRY.
- Define colors with @theme inline and the values are baked in at build time, breaking runtime dark-mode switching.
- The correct answer is a two-stage setup: put raw HSL channel values in :root / .dark and map them with a non-inline @theme.
- Build a11y into CSS with prefers-contrast, forced-colors, reduced-motion, and :focus-visible.

---

The new fast engine (Oxide) is up to 5× faster on a full build and over 100× faster on an incremental build. But its true value is less in speed than in the point that you can **make CSS the single source of truth of the design system.**

---

## 1. v3 → v4: what changed

The biggest change is **where configuration lives.** v3 wrote colors and breakpoints in a JS config file, but v4 completes inside CSS.

```css
/* ❌ v3 の書き方（v4 では誤り） */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* ✅ v4 の正しい書き方：これ1行 */
@import "tailwindcss";
@plugin "@tailwindcss/typography"; /* プラグインも CSS で読み込む */
```

In addition, v4 now ships the following as standard.

- **Automatic content detection.** Manual configuration of the `content` array is in principle no longer needed.
- **container queries are built in** (no plugin needed). With `@container` and `@sm:`, etc., you can write styles responsive to the parent element's size.
- **A modern CSS foundation.** Leverages cascade layers, `@property`, and `color-mix()`. Also supports `@starting-style` (entry animation), the `not-*` variant, `field-sizing`, `inert`, etc.

With configuration gone from JS, design tokens are consolidated into "the single place of CSS," reducing the gap between design and implementation.

---

## 2. `@theme`: manage design tokens centrally

Variables defined in `@theme` are not mere CSS variables but determine "**which utility classes are generated.**" For example, define `--color-primary` and `bg-primary` / `text-primary` / `border-primary` become automatically usable.

```css
@theme {
  /* 角丸トークン */
  --radius-sm: 0.375rem;
  --radius: 0.625rem;
  --radius-lg: 0.875rem;

  /* 影トークン（2層で奥行きを出す） */
  --shadow-sm: 0 1px 2px -1px rgb(16 24 40 / 0.06),
    0 1px 3px 0 rgb(16 24 40 / 0.05);
}
```

Now `rounded-lg` and `shadow-sm` run on "your design definitions." Instead of hardcoding colors and spacing into each component, reference tokens — this is the heart of ETC (easy to change). A spec change ripples to every screen with a one-spot token edit.

---

## 3. The dark-mode pitfall: `@theme inline` breaks switching

This is the most valuable practical insight of this article. When implementing dark mode with CSS variables in Tailwind v4, **using `@theme inline` makes dark mode stop working.**

The reason is this. `@theme inline` **bakes the variable's value into the utility at build time.** That is, `bg-background` is fixed as `background-color: hsl(0 0% 100%)` (the light value), and doesn't change even when the `.dark` class is added at runtime.

The correct answer is a two-stage setup: "**put the raw channel values in `:root` / `.dark` and map to tokens with `@theme` (non-inline).**"

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

/* ① 生の HSL チャンネル値を単一の真実として持つ */
:root {
  --background: 0 0% 100%;
  --foreground: 222 47% 11%;
  --primary: 222 47% 11%;
  --primary-foreground: 210 20% 98%;
}
.dark {
  --background: 222 47% 5%;
  --foreground: 210 20% 98%;
  --primary: 210 20% 98%;
  --primary-foreground: 222 47% 11%;
}

/* ② 非 inline の @theme でトークンへ。これは
   `--color-*: hsl(var(--channel))` を「本物の CSS 変数」として出力するため、
   var(--channel) がランタイムに解決され、ライト/ダークが正しく切り替わる。 */
@theme {
  --color-background: hsl(var(--background));
  --color-foreground: hsl(var(--foreground));
  --color-primary: hsl(var(--primary));
  --color-primary-foreground: hsl(var(--primary-foreground));
}
```

The point is to hold colors as "an HSL channel triplet (`222 47% 11%`)." In the form `hsl(var(--token) / <alpha>)`, you can append **arbitrary transparency** afterward (e.g., `bg-primary/15`). Choose `@theme inline` and this "runtime resolution" is lost and dark mode dies — remember this one point without fail.

---

## 4. Fluid typography: bundle 4 attributes in one utility

Writing a heading's size per breakpoint as `text-2xl md:text-4xl lg:text-6xl` is verbose. With `clamp()` and `@theme`'s **modifier-bearing tokens**, you can apply font-size, line-height, letter-spacing, and font-weight together with a single `text-display-2xl`.

```css
@theme {
  --text-display-2xl: clamp(2.75rem, 1.6rem + 5.1vw, 5.5rem);
  --text-display-2xl--line-height: 1.08;
  --text-display-2xl--letter-spacing: -0.02em;
  --text-display-2xl--font-weight: 700;
}
```

```tsx
// 1クラスで「滑らかに伸縮する見出し」が完成。改行制御は text-balance を併用
<h1 className="text-display-2xl text-balance">見出し</h1>
```

For numeric display, applying `tabular-nums` (monospaced digits) aligns the columns and avoids jitter in count-ups and the like. It's a small difference, but it's the accumulation of refined UI.

---

## 5. Build accessibility into CSS

Not "a11y later" but built in at the token-design stage. Even in Tailwind v4, plain CSS media queries are usable as-is.

```css
@layer base {
  /* キーボード操作時だけフォーカスリングを見せる */
  :focus-visible {
    outline: 2px solid hsl(var(--ring));
    outline-offset: 2px;
  }

  /* アニメーションは prefers-reduced-motion を尊重 */
  @media (prefers-reduced-motion: reduce) {
    *,
    ::before,
    ::after {
      animation-duration: 0.01ms !important;
      transition-duration: 0.01ms !important;
    }
  }

  /* スクロールバー出現での横ズレ（CLS）を防ぐ */
  html {
    scrollbar-gutter: stable;
  }
}

/* ハイコントラスト設定では、細い罫線と淡色テキストを濃くする */
@media (prefers-contrast: more) {
  :root {
    --border: 222 30% 35%;
    --muted-foreground: 222 25% 28%;
  }
}

/* Windows ハイコントラスト（forced-colors）でもフォーカスを維持 */
@media (forced-colors: active) {
  :focus-visible {
    outline-color: Highlight;
  }
}
```

When deciding color tokens, satisfy **contrast ratio AA (body 4.5:1, large text 3:1).** Light text (`muted-foreground`) tends to lose on cards, so choose values that secure 4.5:1 not only on white backgrounds but also on "lightly colored surfaces." For the big picture of accessibility, see the [WCAG 2.2 implementation guide](/blog/react-nextjs-web-accessibility-wcag22-guide).

---

## 6. container queries: truly reusable components

Media queries branch by "screen width," but container queries branch by "**the parent element's width.**" You can place the same card in both a sidebar and the main area, each optimized for its width. This is true reusability (ETC).

```tsx
<div className="@container">
  {/* 親が広いときだけ横並びに */}
  <article className="flex flex-col @md:flex-row @md:gap-6">
    <img className="aspect-video @md:w-48" />
    <div>...</div>
  </article>
</div>
```

Thinking in "this placement is wide" rather than "the screen is wide" makes the component independent of its placement context and stronger as a part of the design system.

---

## 7. When to use custom utilities and `@apply`

Repeating patterns can be carved out into custom utilities. But **don't overuse** them (YAGNI).

```css
/* 自前ユーティリティ：本当に何度も使うものだけ */
@utility container-tight {
  margin-inline: auto;
  max-width: 48rem;
  padding-inline: 1rem;
}
```

`@apply` tempts you with "the urge to bundle existing classes," but overusing it loses the benefit of utility-first (you can tell by looking at the HTML). The principle is **to line up utilities on the HTML side.** Limit `@apply` to design-system primitives (buttons, etc.) and avoid it in app screens.

---

## 8. Performance and cost efficiency

| Item | Handling in v4 | Effect |
| ---- | ----------- | ---- |
| Build speed | Oxide engine (Rust) | full up to 5×, incremental over 100× |
| Unused CSS | automatic content detection + tree-shaking | production CSS is small, favorable to LCP |
| Delivery size | token = CSS variable reduces duplication | optimizes cache efficiency and transfer volume |
| Runtime | static CSS at build time | zero JS execution cost (favorable to INP) |

CSS being small and static directly leads to Core Web Vitals improvement (details in the [Core Web Vitals optimization guide](/blog/core-web-vitals-nextjs-inp-lcp-cls-optimization-guide)). "A fast site is a cheap site" too, helping reduce delivery cost.

---

## 9. Testability / observability

- **Automatic contrast inspection.** Build `@axe-core/playwright` into CI to detect whether a token change introduces insufficient contrast. Because changing a token changes the colors of every screen, automatic guards work.
- **Visual regression.** With snapshot comparison, visualize the impact range of a token change.
- **Test both dark and light.** Take screenshots in both modes to check that chapter 3's design isn't broken.

---

## 10. Anti-patterns

- ❌ **Defining colors with `@theme inline` and dark mode dies.** Use non-inline + raw CSS variables for runtime switching (chapter 3).
- ❌ **Scattering arbitrary values (`bg-[#1a2b3c]`) everywhere.** Tokenize to `bg-primary`. The single source of truth of color collapses.
- ❌ **Removing focus with `:focus { outline: none }`.** Show it with `:focus-visible`.
- ❌ **Ignoring `prefers-reduced-motion` / `forced-colors`.** They're essentials that can be handled in a few lines of CSS.
- ❌ **Overusing `@apply` in app screens.** Limit it to primitives and build screens with utilities.
- ❌ **Building parts with media queries alone.** Use container queries for parts that don't depend on the placement context.

---

## 11. FAQ (frequently asked questions)

**Q. Should I migrate from v3 to v4?**
A. For a new project, v4 is the only choice. For existing, there's an official upgrade tool, but because it requires replacing `@tailwind` directives, moving config to CSS, and changing plugin loading, migrate after confirming the impact range.

**Q. Can I no longer use `tailwind.config.js`?**
A. CSS-first is the standard, but loading a JS config for compatibility is also possible. For new work, the CSS `@theme` is recommended (since it can be consolidated into a single place).

**Q. Dark mode doesn't switch.**
A. The leading cause is color definition with `@theme inline`. Put raw CSS variables in `:root`/`.dark` and map with non-inline `@theme` (chapter 3).

**Q. Can I use it with shadcn/ui?**
A. The compatibility is excellent. shadcn assumes a CSS-variable-based theme and meshes directly with this article's token design ([shadcn/ui design guide](/blog/shadcn-ui-design-system-architecture-production-guide)).

**Q. How finely should I split design tokens?**
A. Split by "meaning" (`primary` / `muted` / `destructive`). Naming by role rather than the color itself (`blue-500`) makes a rebrand or color-scheme change one-spot.

---

## Conclusion: make CSS the single source of truth of the design system

The essence of Tailwind v4 is not speed but **consolidation of design.** Gather color, typography, radius, shadow, and breakpoints in `@theme`, generate utilities from there, and manage a11y, contrast, and dark mode in the same place — this becomes the foundation of a design system that doesn't break down.

1. Unify into a **CSS-first configuration** with `@import` + `@theme`.
2. Make colors support runtime/dark mode with **raw channels + non-inline `@theme`.**
3. Build placement-context-resilient parts with **fluid typography and container queries.**
4. **Embed a11y into CSS** with `prefers-contrast` / `forced-colors` / `reduced-motion` / `:focus-visible`.
5. **Optimize performance and cost** with the lightness of static CSS.

Design consistency directly ties to the product's sense of trust. A product whose tokens are tidy is high not only in appearance but in maintainability and extensibility.

**If you need to build a design system, or migrate to Tailwind v4 and design a theme, feel free to reach out.** The case study below introduces the process of designing and implementing a UI used by multilingual, multicultural users with consistent design and operability.
