$ npx rulesync-cli pull✓ Wrote CLAUDE.md (2 rulesets)# Coding Standards- Always use async/await- Prefer named exports
Rule Writing

CLAUDE.md for Tailwind CSS Projects

AI mixes Tailwind with custom CSS, uses arbitrary values instead of the design scale, and ignores responsive prefixes. Rules for utility-first, design tokens, and component extraction.

7 min read·June 3, 2024

Tailwind's power is the design scale — AI uses arbitrary values instead

Utility-first discipline, design tokens, responsive prefixes, and component extraction

Why Tailwind Projects Need Strict Utility-First Rules

Tailwind CSS is utility-first by design — but AI assistants don't internalize 'utility-first' as a discipline. They mix Tailwind classes with custom CSS, use arbitrary values (p-[13px]) instead of the design scale (p-3), write @apply everywhere (defeating the utility model), and generate inline styles alongside Tailwind classes. The result is a codebase that uses Tailwind's build system but not its philosophy.

The design scale is Tailwind's core value: spacing (p-4 = 1rem), colors (bg-blue-500), typography (text-lg), and breakpoints (md:flex-row) are all from a constrained, consistent set. When AI reaches for arbitrary values, it breaks visual consistency — p-[13px] on one component and p-[15px] on another creates the kind of pixel-level inconsistency that design systems exist to prevent.

These rules enforce the utility-first discipline: use the scale, use responsive prefixes, use dark: for dark mode, and extract repeated patterns into components — not @apply.

Rule 1: Design Scale Values, Not Arbitrary

The rule: 'Use Tailwind's design scale for all values. p-4 (1rem) not p-[16px]. text-lg not text-[18px]. bg-blue-500 not bg-[#3b82f6]. rounded-lg not rounded-[8px]. gap-4 not gap-[1rem]. Arbitrary values (square bracket syntax) are allowed only when the design scale genuinely doesn't have what you need — and that's rare. If you use arbitrary values frequently, extend the scale in tailwind.config.'

For extending the scale: 'When a project needs values outside the default scale, extend in tailwind.config.ts — don't use arbitrary values. theme: { extend: { spacing: { "18": "4.5rem" }, colors: { brand: { 50: "#f0f4ff", 500: "#3b52ce" } } } }. Extended values become first-class: p-18, bg-brand-500. The IDE provides autocomplete and the team has a shared vocabulary.'

AI uses arbitrary values for everything because they're the path of least resistance — no need to check the scale. One rule ('design scale values, not arbitrary') forces the AI to use the system, maintaining visual consistency across every component.

  • p-4 not p-[16px] — text-lg not text-[18px] — bg-blue-500 not bg-[#3b82f6]
  • Arbitrary values only when scale genuinely lacks the value — very rare
  • Extend tailwind.config for project-specific values — don't use brackets
  • Extended values get autocomplete and team-wide vocabulary
  • If you see brackets frequently, the scale needs extending — not workaround
⚠️ Brackets = Broken Scale

p-[13px] on one component, p-[15px] on another — visual inconsistency that design systems exist to prevent. p-3 (12px) and p-4 (16px) are from the scale. If you need custom values, extend tailwind.config — don't use brackets.

Rule 2: Responsive Prefixes and State Variants

The rule: 'Use responsive prefixes for all breakpoint-specific styles: sm: for 640px+, md: for 768px+, lg: for 1024px+, xl: for 1280px+. Mobile-first: base styles apply to mobile, prefixed styles override for larger screens. flex-col md:flex-row = column on mobile, row on medium+. Never use CSS media queries — Tailwind's responsive prefixes handle them.'

For state variants: 'Use hover:, focus:, active:, disabled: for interactive states. Use focus-visible: instead of focus: for keyboard-only focus indicators. Use group-hover: for parent-triggered styles. Use peer-* for sibling-triggered styles. Use first:, last:, odd:, even: for list item styling.'

For dark mode: 'Use dark: variant for all color-related classes: bg-white dark:bg-gray-900, text-gray-900 dark:text-gray-100. Configure darkMode: "class" or "media" in tailwind.config. Design light mode first, add dark: overrides. Never maintain separate color schemes — dark: on the same element keeps styles co-located.'

  • Mobile-first: base = mobile, sm: md: lg: xl: for larger screens
  • flex-col md:flex-row — grid-cols-1 lg:grid-cols-3 — hidden md:block
  • hover:, focus-visible:, active:, disabled: for interactive states
  • dark: for all dark mode colors — co-located with light mode styles
  • group-hover: for parent-triggered — peer-* for sibling-triggered

Rule 3: Class Organization and Readability

The rule: 'Order utility classes logically: layout (flex, grid, position) → sizing (w, h, min-w, max-w) → spacing (p, m, gap) → typography (text, font, leading) → colors (bg, text, border) → borders (border, rounded) → effects (shadow, opacity) → transitions (transition, duration). Use prettier-plugin-tailwindcss to auto-sort — never rely on manual ordering.'

For long class strings: 'When a class string exceeds ~10 utilities, extract into a component — not @apply. Components are reusable, typed (with props), and maintainable. @apply hides utilities in CSS files, breaking the utility-first model and making classes harder to find. The only acceptable @apply use is in a global base style: @layer base { h1 { @apply text-2xl font-bold } }.'

For conditional classes: 'Use clsx or cn (from shadcn/ui) for conditional class composition: className={cn("rounded-lg p-4", isActive && "bg-blue-500 text-white", isDisabled && "opacity-50 cursor-not-allowed")}. Never use string interpolation for class conditions — it breaks Tailwind's static analysis and tree-shaking.'

💡 Components, Not @apply

@apply .btn hides utilities in CSS — breaking utility-first and making classes harder to find. A <Button> component with className props is typed, reusable, and inspectable. Extract to components at 3+ repetitions.

Rule 4: Component Extraction Over @apply

The rule: 'When the same set of utilities repeats 3+ times, extract into a React/Vue/Svelte component — not an @apply class. A Button component with className="rounded-lg px-4 py-2 bg-blue-500 text-white" is a reusable component with props for variants. An @apply .btn class is a hidden abstraction that looks like CSS but isn't — it's harder to find, harder to modify, and doesn't support props.'

For component variants: 'Use cva (class-variance-authority) or a simple variant map for component variants: const buttonVariants = cva("rounded-lg px-4 py-2 font-medium", { variants: { color: { primary: "bg-blue-500 text-white", danger: "bg-red-500 text-white" }, size: { sm: "text-sm px-3 py-1", lg: "text-lg px-6 py-3" } } }). This is type-safe and composable — @apply with BEM modifiers is neither.'

For design system components: 'Build a component library: Button, Input, Card, Badge, Modal, Alert, Avatar. Each component encapsulates its Tailwind classes and exposes typed props for variants. Import and use: <Button variant="primary" size="lg">. This is the Tailwind way to create reusable UI — components, not @apply classes.'

  • Components for reusable UI — @apply only in @layer base for global styles
  • cva for type-safe variant components — not BEM + @apply
  • Button, Input, Card as components — props for variants, not CSS classes
  • 3+ repetitions = extract to component — 1-2 repetitions = leave as utilities
  • prettier-plugin-tailwindcss for auto-sorting — clsx/cn for conditionals

Rule 5: Tailwind Config and Design Tokens

The rule: 'Define project design tokens in tailwind.config.ts: brand colors, custom spacing, font families, border radii, and shadows. Use semantic names: colors: { surface: "#ffffff", foreground: "#1a1a1a", accent: "#4f39f6" } — not raw Tailwind colors in components. This enables theming: change accent once in config, every component updates.'

For content scanning: 'Configure content paths to include all files with Tailwind classes: content: ["./src/**/*.{ts,tsx,html}", "./components/**/*.{ts,tsx}"]. Missing paths = missing classes in production (tree-shaked away). Never use dynamic class names that Tailwind can't statically analyze: `bg-${color}-500` doesn't work — use a class map instead.'

For plugins: 'Use @tailwindcss/typography for prose content (blog posts, markdown). Use @tailwindcss/forms for form resets. Use @tailwindcss/container-queries for container-based responsive design. Use tailwind-animate or tailwindcss-animate for animation utilities. Install only what you need — plugins add to the generated CSS size.'

ℹ️ Dynamic Classes Don't Work

`bg-${color}-500` is invisible to Tailwind's static analysis — the class gets tree-shaked in production. Use a class map: { blue: 'bg-blue-500', red: 'bg-red-500' }[color]. Only complete class strings survive production builds.

Complete Tailwind CSS Rules Template

Consolidated rules for Tailwind CSS projects.

  • Design scale values only: p-4 not p-[16px] — extend config for project-specific values
  • Mobile-first responsive: base = mobile, sm: md: lg: xl: for breakpoints
  • dark: variant for all dark mode — co-located with light mode on same element
  • Class order: layout → sizing → spacing → typography → colors → borders → effects
  • Components for reuse (not @apply) — cva for variants — clsx/cn for conditionals
  • Semantic design tokens in config: surface, foreground, accent — not raw colors
  • Content paths must cover all files — never dynamic class names (bg-${color}-500)
  • prettier-plugin-tailwindcss for sorting — @tailwindcss/typography for prose
CLAUDE.md for Tailwind CSS Projects — RuleSync Blog