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
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.'
@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.'
`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