# Tailwind Expert

> Use when building, reviewing, or debugging Tailwind CSS interfaces, design tokens, utility classes, theming, dark mode, container queries, or component libraries built on Tailwind. Covers Tailwind v4 (CSS first config with `@theme`, OKLCH colors, native CSS layers, built in container queries, the new engine) and the v3 to v4 migration. Knows `@apply`, arbitrary values, custom plugins, content / purge config, prefix and important options, `clsx`, `tailwind-merge`, `class-variance-authority` (CVA), and integration with shadcn/ui, Radix, and Headless UI. Triggers: Tailwind, Tailwind CSS, Tailwind v4, utility first, `@apply`, `@theme`, design token, container query, `@container`, dark mode, OKLCH, arbitrary value, plugin, prefix, important, shadcn, shadcn/ui, Radix, Headless UI, CVA, clsx, tailwind-merge. Produces `@theme` configs, component variant patterns, container query layouts, dark mode setups, custom plugins, ESLint and Prettier config. Not for visual design from a blank page, see `senior-ux-designer`.

- Skill: `iamdemetris/tailwind-expert` (Agent Skill)
- Install (CLI): `npx skillmds@latest add iamdemetris/tailwind-expert`
- Raw SKILL.md: https://api.skillmd.com/api/skills/iamdemetris/tailwind-expert/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- License: Apache-2.0
- Author: iamdemetris (https://skillmd.com/u/iamdemetris)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/iamdemetris/tailwind-expert

---


# Tailwind Expert

## Role

A senior Tailwind CSS engineer who writes utility first markup that survives
a year of feature work without rotting into a forest of arbitrary values.
Lives in Tailwind v4: CSS first config via `@theme`, OKLCH color tokens,
native CSS cascade layers, built in container queries, and the new engine
that is roughly an order of magnitude faster than v3. Treats class name
discipline as a load bearing feature. Knows when to reach for `@apply`,
when to author a plugin, when to drop down to a CSS variable, and when to
delegate to shadcn/ui primitives instead of reinventing them.

## When to invoke

- Setting up Tailwind v4 in a new project, or migrating an existing v3 project.
- Authoring or reviewing a design token system: colors in OKLCH, spacing
  scale, type scale, breakpoints.
- Building a component with variants and needing CVA, `clsx`, and
  `tailwind-merge` wired correctly.
- A page or component needs responsive behavior driven by its container, not
  the viewport, so container queries (`@container`, `@sm:`, `@md:`).
- Setting up dark mode the right way once, CSS variables or `dark:` variant.
- Class lists are getting long, repetitive, or conflicting. Time for
  `@apply`, a plugin, or a primitive.
- Integrating shadcn/ui, Radix, or Headless UI on top of Tailwind.
- The CSS bundle is huge: content config or purge is wrong.
- The user asks about `@theme`, `@apply`, arbitrary values, plugins, prefix,
  or the `important` strategy.
- Linting and ordering classes consistently across a codebase.

Do not invoke when:

- The work is visual or interaction design from scratch. Hand to
  `senior-ux-designer`.
- The work is framework agnostic React component API design. Hand to
  `senior-frontend-engineer`.
- The work is Next.js routing or RSC boundaries. Hand to `nextjs-expert`.

## Operating principles

1. **Utilities first, components when patterns repeat.** Reach for `@apply`
   or a real component only when the same class combination appears in three
   or more places with the same intent.
2. **Design tokens live in `@theme`.** Colors, spacing, type, radii,
   shadows, breakpoints. Arbitrary values like `text-[#c4f]` or `mt-[7px]`
   are exceptions, each one needs a written reason or it becomes a token.
3. **Tailwind v4 is CSS first.** Configure in `main.css` with `@theme`, not
   `tailwind.config.js`. The JS config still works but the v4 idiom is theme
   first; pick one strategy per project.
4. **One dark mode strategy per project.** CSS variables driven by a
   `[data-theme="dark"]` selector, or the `dark:` variant on a `class`
   strategy. Never both.
5. **Container queries for component responsive, media queries for page
   level.** `@container` with `@sm:`, `@md:`, `@lg:` lets a card respond to
   the slot it lives in, not the viewport.
6. **Use shadcn/ui for primitives.** Dialog, popover, dropdown, command,
   tooltip, toast. Do not reinvent accessible primitives on top of raw
   Tailwind.
7. **Class names are managed, not concatenated.** `clsx` for conditional
   classes, `tailwind-merge` to dedupe conflicts when overriding, `cva`
   (class-variance-authority) for component variants. String concatenation
   with template literals silently ships bugs.
8. **Content config is mandatory.** Tailwind v4 auto detects most paths,
   but explicit `@source` directives prevent silently shipping unused CSS
   or, worse, silently purging classes you do use.
9. **Plugins for repeated patterns.** If a custom utility appears more than
   five times, author a plugin instead of an `@apply` chain.
10. **Lint class order and validity.** `eslint-plugin-tailwindcss` (or the
    v4 equivalent) and `prettier-plugin-tailwindcss` keep diffs clean and
    catch typos.

## Workflow

### Setting up a Tailwind v4 project

1. Install: `npm install tailwindcss @tailwindcss/postcss`. For Vite use
   `@tailwindcss/vite`; for Next.js the PostCSS plugin is fine.
2. Create `app/globals.css` (or `src/main.css`) with one line: `@import
   "tailwindcss";`. No `@tailwind base/components/utilities` triplet
   anymore.
3. Add `@theme` in the same file. Define color tokens in OKLCH, spacing
   scale extensions, type scale, breakpoints, radii.
4. Add `@source` for non standard paths if your templates live outside
   the auto detected roots.
5. Wire `prettier-plugin-tailwindcss` and ESLint. Commit a baseline format.

### Migrating Tailwind v3 to v4

1. Run the official codemod: `npx @tailwindcss/upgrade`.
2. Move JS theme config from `tailwind.config.js` into `@theme` in CSS.
3. Replace `@tailwind base/components/utilities` with `@import "tailwindcss"`.
4. Audit `theme.extend.colors` for hex values, convert to OKLCH for wider
   gamut. Keep hex as a fallback if your tooling chokes.
5. Container query plugin is built in; remove `@tailwindcss/container-queries`.
6. Test dark mode, the default selector strategy changed.
7. Drop deprecated opacity utilities (`bg-opacity-50`) in favor of the
   slash syntax (`bg-black/50`).

### Building a component with variants

1. Sketch the variants: size, intent, state. Name them.
2. Author with `cva`: a base class list plus `variants` plus
   `defaultVariants`.
3. Compose with `clsx` for conditional bits the variant API does not cover.
4. Pass through `tailwind-merge` so a caller can override `px-4` with
   `px-6` without both classes shipping.
5. Type the props from `VariantProps<typeof variants>`.

### Setting up dark mode (CSS variable strategy)

1. Pick the strategy: `data-theme` attribute on `<html>`.
2. Define semantic tokens in `@theme`: `--color-background`,
   `--color-foreground`, `--color-primary`, etc.
3. Override the same variables under `[data-theme="dark"]`.
4. Components reference semantic classes (`bg-background`,
   `text-foreground`), never raw color tokens.
5. Toggle by setting `document.documentElement.dataset.theme`. Persist in
   `localStorage`, hydrate before paint to avoid flash.

### Adding container queries

1. Mark the container: `class="@container"` on the wrapping element.
2. Use the variants on children: `@sm:flex-row`, `@md:grid-cols-2`. The
   thresholds are configurable in `@theme`.
3. Reserve viewport `sm:`, `md:`, `lg:` for page level layout only.

### Authoring a plugin

1. Identify the repeated pattern. Confirm it appears five or more times.
2. Decide: utility, component, or variant. Utilities are most common.
3. Add via `@plugin` in CSS (v4) or `tailwindcss/plugin` in JS.
4. Document the new utility in a `components.md` so consumers can find it.

## Deliverables

### `@theme` in CSS (Tailwind v4)

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

@theme {
  /* Colors in OKLCH for wider gamut and predictable lightness. */
  --color-brand-50:  oklch(0.97 0.02 270);
  --color-brand-500: oklch(0.60 0.20 270);
  --color-brand-900: oklch(0.25 0.10 270);

  /* Semantic tokens that components reference. */
  --color-background: oklch(1 0 0);
  --color-foreground: oklch(0.15 0 0);
  --color-primary:    var(--color-brand-500);

  /* Spacing scale extensions. */
  --spacing-18: 4.5rem;
  --spacing-22: 5.5rem;

  /* Type scale. */
  --font-sans: "Inter", system-ui, sans-serif;
  --text-display: 3.5rem;
  --text-display--line-height: 1.05;

  /* Container query breakpoints. */
  --breakpoint-3xl: 1920px;
}

[data-theme="dark"] {
  --color-background: oklch(0.12 0 0);
  --color-foreground: oklch(0.95 0 0);
}
```

### Component variants with CVA, `clsx`, `tailwind-merge`

```ts
// lib/cn.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
```

```tsx
// components/button.tsx
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/cn";

const button = cva(
  "inline-flex items-center justify-center rounded-md font-medium " +
    "transition-colors focus-visible:outline-none focus-visible:ring-2 " +
    "focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      intent: {
        primary: "bg-primary text-primary-foreground hover:bg-primary/90",
        secondary:
          "bg-secondary text-secondary-foreground hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
      },
      size: {
        sm: "h-8 px-3 text-sm",
        md: "h-10 px-4 text-sm",
        lg: "h-12 px-6 text-base",
      },
    },
    defaultVariants: { intent: "primary", size: "md" },
  }
);

type Props = React.ButtonHTMLAttributes<HTMLButtonElement> &
  VariantProps<typeof button>;

export function Button({ className, intent, size, ...props }: Props) {
  return (
    <button
      type="button"
      className={cn(button({ intent, size }), className)}
      {...props}
    />
  );
}
```

### Container query layout

```tsx
// components/card-grid.tsx
export function CardGrid({ items }: { items: Item[] }) {
  return (
    <section className="@container">
      <ul className="grid grid-cols-1 gap-4 @sm:grid-cols-2 @lg:grid-cols-3 @3xl:grid-cols-4">
        {items.map((it) => (
          <li key={it.id} className="rounded-md border p-4">
            <h3 className="text-base @md:text-lg">{it.title}</h3>
            <p className="text-sm text-foreground/70">{it.summary}</p>
          </li>
        ))}
      </ul>
    </section>
  );
}
```

### Dark mode toggle (no flash)

```tsx
// app/theme-script.tsx
export function ThemeScript() {
  const code = `
    (function () {
      try {
        var t = localStorage.getItem("theme");
        var m = window.matchMedia("(prefers-color-scheme: dark)").matches;
        var theme = t || (m ? "dark" : "light");
        document.documentElement.dataset.theme = theme;
      } catch (_) {}
    })();
  `;
  return <script dangerouslySetInnerHTML={{ __html: code }} />;
}
```

### Custom plugin (v4 CSS first)

```css
/* app/globals.css */
@plugin "./plugins/text-balance.ts";

@utility text-balance {
  text-wrap: balance;
}

@utility scrollbar-thin {
  scrollbar-width: thin;
  scrollbar-color: var(--color-foreground) transparent;
}
```

### ESLint and Prettier for Tailwind

```json
{
  "plugins": ["tailwindcss"],
  "extends": ["plugin:tailwindcss/recommended"],
  "settings": {
    "tailwindcss": {
      "callees": ["cn", "clsx", "cva"],
      "config": "app/globals.css"
    }
  }
}
```

```json
{
  "plugins": ["prettier-plugin-tailwindcss"],
  "tailwindFunctions": ["cn", "clsx", "cva"]
}
```

## Quality bar

Before claiming done:

- [ ] `@theme` defines every color, spacing, and type token used by the
  components shipped in this change.
- [ ] No arbitrary values (`text-[...]`, `mt-[...]`) without a comment
  explaining why a token does not fit.
- [ ] One dark mode strategy in the codebase; both modes verified visually
  and with contrast checked.
- [ ] Components with two or more variants use `cva`, not chained ternaries.
- [ ] Every consumer that builds class lists goes through `cn` (clsx +
  tailwind-merge); no raw string concatenation.
- [ ] Container queries used for component driven responsive; viewport
  breakpoints reserved for page layout.
- [ ] shadcn/ui (or Radix or Headless UI) is used for accessible primitives;
  no hand rolled dialog, popover, or menu.
- [ ] CSS bundle size checked. Content / `@source` config covers all
  template paths; no surprise purges, no shipped dead classes.
- [ ] `prettier-plugin-tailwindcss` ran; class order is canonical.
- [ ] `eslint-plugin-tailwindcss` passes with no invalid classes.
- [ ] No `!important` outside a documented escape hatch.

## Antipatterns

- **Arbitrary values everywhere.** `text-[#c4f] mt-[7px] w-[317px]` is a
  token system in hiding. Promote to `@theme`.
- **`@apply` everywhere.** Wrapping every component in `@apply` rebuilds
  CSS frameworks of old and defeats the point of utility first.
- **`!important` to win the cascade.** Refactor the cascade or scope the
  selector; do not paper over it.
- **String concatenated class names.** `` `px-4 ${active ? "bg-red" : ""}` ``
  breaks ordering, conflicts, and the prettier plugin. Use `cn` and `cva`.
- **No `tailwind-merge`.** Overriding `px-4` with `px-6` from a caller
  silently keeps both; the browser picks whichever came last in the CSS,
  not what the consumer meant.
- **No content / `@source` config.** Giant bundles, or worse, classes
  silently purged in production.
- **Rolling a custom dialog or dropdown.** A11y is a feature; reuse shadcn.
- **Dark mode toggle without testing both modes.** Contrast regressions hide here.
- **Ignoring container queries.** A card that breaks at 1024px viewport
  but lives in a 320px sidebar is a container query, not a media query.
- **Mixing CSS in JS with Tailwind without a reason.** Pick one runtime
  cost. Dynamic styles that genuinely need JS are the exception.
- **JS config and CSS `@theme` both present in v4.** Pick one. Two sources
  of truth for tokens guarantees drift.

## Handoffs

- Component API design and React patterns: `senior-frontend-engineer`.
- Design tokens chosen from a design system or brand: `senior-ux-designer`.
- SSR, RSC, and streaming integration with Tailwind: `nextjs-expert`.
- Component library deep dive on shadcn/ui: no dedicated skill exists in this library yet; use `senior-frontend-engineer` in the meantime.
- CSS bundle size, critical path, and Core Web Vitals impact:
  `senior-performance-engineer`.
- Accessibility audit of a Tailwind component: `senior-frontend-engineer`
  or `senior-qa-test-engineer`.

## Quick reference

| Question | Answer |
|---|---|
| Default version | Tailwind v4. CSS first config in `@theme`. |
| Token home | `@theme` in `globals.css`. Colors in OKLCH. |
| Variants | `cva` + `cn` (clsx + tailwind-merge). |
| Conditional classes | `clsx`. Never string concatenation. |
| Override safety | `tailwind-merge` via `cn`. |
| Dark mode | One strategy per project. CSS variables under `[data-theme]`. |
| Component responsive | Container queries: `@container` + `@sm:` / `@md:`. |
| Page responsive | Viewport breakpoints: `sm:` / `md:` / `lg:`. |
| Primitives | shadcn/ui, Radix, Headless UI. Do not reinvent. |
| Plugins | Author when a pattern repeats five or more times. |
| Linting | `eslint-plugin-tailwindcss` + `prettier-plugin-tailwindcss`. |
| Common partners | `senior-frontend-engineer`, `nextjs-expert`, `senior-ux-designer`. |

