---
title: "Tailwind v4 (4.3.x): silent failures and the correct form (craft)"
description: "Tailwind v4 never errors on a class it doesn't know. The page just looks slightly wrong."
canonical: https://void-design.vercel.app/docs/craft/tailwind-v4
lastModified: 2026-09-16
---

# Tailwind v4 (4.3.x): silent failures and the correct form

Tailwind v4 never errors on a class it doesn't know. The page just looks slightly wrong. `void lint` compiles the real CSS entry and diffs used classes against emitted CSS; run it after every styling pass.

## 1. Variables in utilities

| Wrong (v3 habit) | Right (v4) | Why |
|---|---|---|
| `duration-[--duration-fast]` | `duration-(--duration-fast)` | brackets are literal: emits `transition-duration: --duration-fast`, invalid → 0s (`lint/tw-v3-arbitrary-var`). The owner's app had 131 of these. |
| `ease-[--ease-out]` | `ease-out` (theme utility) or `ease-(--ease-out)` | same |
| `bg-[--brand]` | `bg-brand` or `bg-(--brand)` | same |
| `h-[var(--header-h)]` | `h-(--header-h)` | shorter, same output |
| `shadow-[var(--elevation-highlight)]` | `shadow-(--elevation-highlight)` | — |

## 2. Classes that emit nothing in a void project

void's `base.css` removes Tailwind defaults so off-system styling can't slip in:

| Removed | Use instead |
|---|---|
| default palette: `bg-zinc-900`, `text-gray-500`, `border-slate-200`, `from-indigo-500` | semantic roles: `bg-surface`, `text-fg-muted`, `border-line`, `bg-brand` |
| `shadow-sm/md/lg/xl/2xl` | `shadow-1`, `shadow-2`, `shadow-3`, `shadow-(--elevation-highlight)` |
| `text-5xl … text-9xl` | `text-display-sm`, `text-display`, `text-display-lg`, `text-display-xl` |
| `text-primary`, `bg-primary`, `bg-muted`, `text-muted-foreground` (shadcn names) | void roles, or import `styles/void/shadcn.css` if using shadcn components |
| anything you invent: `rounded-card`, `ease-smooth`, `bg-backdrop` | define it in `@theme` first, or use an existing token |

Kept from Tailwind: spacing scale, `font-medium`/weights, `tracking-*`, `leading-*`, `max-w-xl` etc., `rounded-full`, `rounded-none`, `ease-in`/`ease-linear`, numeric `duration-150`. Prefer tokens anyway.

**Adding a token correctly:**

```css
/* globals.css, after the direction import */
@theme {
  --color-highlight: oklch(92% 0.12 95);   /* static value → bg-highlight, text-highlight */
}
@theme inline {
  --color-chart-1: var(--brand);           /* runtime var → must be `inline` so .dark subtrees work */
}
```

Never `--x: var(--x)` in `@theme inline` (self-reference cycle). Runtime names differ from theme names (`--elevation-2` → `--shadow-2`, `--brand` → `--color-brand`).

**Defining a token can break old code:** adding `--color-primary` suddenly styles every `bg-primary` pasted from a registry. After token changes, rebuild and look at untouched pages.

## 3. Config and variants

- **JS config is ignored.** `tailwind.config.ts` does nothing without `@config` (`lint/tw-js-config-ignored`). Delete it; theme lives in CSS. Keyframes pasted into a JS config are dead animations.
- **Dark variant.** Default `dark:` = `prefers-color-scheme`. void base declares `@custom-variant dark (&:where(.dark, .dark *, [data-theme="dark"], [data-theme="dark"] *));` (`lint/tw-dark-variant-os`). With semantic tokens you rarely need `dark:` at all.
- **Hover** is already wrapped in `@media (hover: hover)`, so no sticky hover on touch. Pointer-precision effects: `pointer-fine:`.
- **Reduced motion:** `motion-safe:` / `motion-reduce:`.
- **Starting style:** `starting:opacity-0` compiles to `@starting-style`.
- **Container queries:** `@container` on parent, `@md:grid-cols-2` on children; named `@container/card` + `@lg/card:flex-row`.
- **Supports:** `supports-[backdrop-filter]:backdrop-blur-md`, `supports-[animation-timeline:view()]:…`.
- **Arbitrary variants** for descendants: `[&_p+p]:mt-6`, `[&>svg]:size-4`.

## 4. v3 → v4 renames (paste audit, `lint/tw-v3-renamed`)

| v3 | v4 |
|---|---|
| `shadow-sm` / `shadow` | `shadow-xs` / `shadow-sm` (void: `shadow-1`) |
| `rounded-sm` / `rounded` | `rounded-xs` / `rounded-sm` |
| `blur-sm` / `blur` | `blur-xs` / `blur-sm` |
| `drop-shadow-sm` | `drop-shadow-xs` |
| `outline-none` | `outline-hidden` (`outline-none` now sets `outline-style: none`) |
| `ring` (3px) | `ring-3` (bare `ring` = 1px) |
| `bg-gradient-to-r` | `bg-linear-to-r` (also `bg-radial`, `bg-conic`) |
| `flex-shrink-0` / `flex-grow` | `shrink-0` / `grow` |
| `bg-opacity-50`, `text-opacity-*` | `bg-black/50` style modifiers |
| `decoration-slice` | `box-decoration-slice` |
| `overflow-ellipsis` | `text-ellipsis` |
| default border color gray-200 | `currentColor`: always write `border-line*` |
| `space-y-*` on inline children | prefer `flex flex-col gap-*` (selector changed) |
| `@tailwind base; …` | `@import "tailwindcss";` |
| `theme()` in CSS | `var(--color-…)` |

## 5. Cascade traps

- **Unlayered CSS beats utilities** regardless of specificity. Vendor CSS (`xterm.css`, a pasted component stylesheet) must go in `@layer components` or be scoped.
- **No global `* { border-color: … }`.** It overrides every border that relies on `currentColor` (dashed dividers vanished in the owner's app).
- **`!important` soup** (`lint/tw-important-soup`) means a conflicting global rule; fix the source.
- **Direction overrides** are unlayered `:root` rules, so they correctly beat `@layer theme` defaults. Project overrides go after the direction import.
- **Fonts from `next/font`** are CSS variables on `<html>`; the direction's `--font-sans: var(--font-geist-sans, "Geist"), …` picks them up. If the variable name doesn't match, text silently falls back to system-ui. Compare `layout.tsx` variable names to the direction header.

## 6. Arbitrary values

Allowed: `max-w-[16ch]` (headline measure), one-off gradients built from tokens (`bg-[radial-gradient(…var(--brand)…)]`), `[scrollbar-width:thin]`. Not allowed: colors (`text-[#777]`), off-grid spacing (`p-[13px]`), radii (`rounded-[7px]`) (`lint/tw-arbitrary-sprawl`, `design/spacing-off-grid`). Underscores become spaces inside brackets: `bg-[radial-gradient(60%_70%_at_20%_0%,…)]`.

## 7. Verify

```bash
void lint                      # tw-unknown-class, tw-v3-*, hardcoded-colors, arbitrary sprawl
bun run build                  # next build must pass
bun packages/tokens/scripts/validate.ts   # (void repo only) contract utilities emit CSS in every direction
```
