Tailwind CSS Guidelines
Overview
Foundational Tailwind CSS guidelines for utility-first styling. Covers class composition, responsive design, dark mode, color systems, typography, spacing, state variants, and Tailwind v4 changes. Prioritize utility classes over custom CSS, compose with clsx/tw-merge, and follow mobile-first responsive patterns.
Utility-First Methodology
| Rule |
Detail |
| Default to utilities |
Write className="mt-4 text-sm font-medium" before reaching for custom CSS |
| Custom CSS only for complex selectors |
Animations, ::before/::after, or styles utilities cannot express |
Never use @apply in components |
Defeats utility-first purpose; use clsx/tw-merge composition instead |
@apply only in base layer |
Acceptable for resetting third-party styles or global typography |
| One semantic class per component |
Extract via component abstraction, not CSS abstraction |
Class Composition
| Tool |
Purpose |
When to Use |
clsx |
Conditional class joining |
Toggling classes based on props/state |
tw-merge |
Tailwind-aware merge with conflict resolution |
Overriding default styles in component variants |
clsx + tw-merge |
Combined via cn() helper |
Standard pattern for component libraries |
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs));
}
cn("px-4 py-2 bg-blue-500", isActive && "bg-blue-700", className)
| Pattern |
Correct |
Incorrect |
| Conditional classes |
cn("text-sm", isLarge && "text-lg") |
String interpolation: `text-${size}` |
| Dynamic values |
Use style prop or CSS variables |
text-[${dynamicValue}px] |
| Component variants |
cn(baseStyles, variants[variant]) |
Deeply nested ternaries |
| Override defaults |
cn("px-4", className) via tw-merge |
Manual class deduplication |
Responsive Design
Mobile-first: base styles apply to all screens, breakpoint prefixes apply at that width and above.
| Breakpoint |
Min Width |
Typical Target |
| (none) |
0px |
Mobile (default) |
sm: |
640px |
Large phones, small tablets |
md: |
768px |
Tablets |
lg: |
1024px |
Laptops |
xl: |
1280px |
Desktops |
2xl: |
1536px |
Large screens |
Common Responsive Patterns
| Pattern |
Classes |
Effect |
| Stack to row |
flex flex-col md:flex-row |
Column on mobile, row on desktop |
| Column count |
grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 |
Responsive grid columns |
| Hide on mobile |
hidden md:block |
Visible only at md and above |
| Show on mobile only |
block md:hidden |
Visible only below md |
| Responsive text |
text-sm md:text-base lg:text-lg |
Scale text with screen |
| Responsive padding |
px-4 md:px-6 lg:px-8 |
Increase padding with screen |
| Full to constrained |
w-full max-w-md mx-auto |
Full mobile, centered container on desktop |
| Responsive gap |
gap-4 md:gap-6 lg:gap-8 |
Increase spacing with screen |
Container Queries
| Rule |
Detail |
Use @container for component-level responsiveness |
When component lives in varying-width contexts |
Apply @container on parent |
Parent gets the container class |
Query with @sm:, @md:, etc. |
Breakpoints relative to container, not viewport |
<div class="@container">
<div class="flex flex-col @md:flex-row">...</div>
</div>
Dark Mode
| Rule |
Detail |
Use dark: variant for every color |
Pair every light color with its dark counterpart |
v4: Use @custom-variant |
@custom-variant dark (&:where(.dark, .dark *)) for class-based toggle |
| v3: Set strategy in config |
darkMode: 'class' (toggle) or 'media' (system preference) |
| Test both modes |
Every component must be visually verified in both modes |
| Use semantic tokens |
Prefer CSS variables over hardcoded dark/light pairs for theming |
Color Pairing Strategy
| Element |
Light |
Dark |
| Page background |
bg-white |
dark:bg-gray-950 |
| Card/surface |
bg-white or bg-gray-50 |
dark:bg-gray-900 |
| Primary text |
text-gray-900 |
dark:text-white |
| Secondary text |
text-gray-600 |
dark:text-gray-400 |
| Muted text |
text-gray-500 |
dark:text-gray-500 |
| Borders |
border-gray-200 |
dark:border-gray-700 |
| Subtle borders |
border-gray-900/10 |
dark:border-white/10 |
| Input background |
bg-white |
dark:bg-white/5 |
| Hover surface |
hover:bg-gray-50 |
dark:hover:bg-gray-800 |
Opacity-Based Dark Mode Colors
| Pattern |
Use Case |
bg-white/5 |
Subtle elevated surface in dark mode |
bg-white/10 |
Hover state overlay in dark mode |
border-white/10 |
Subtle border in dark mode |
ring-white/15 |
Decorative ring in dark mode |
text-white/70 |
De-emphasized text in dark mode |
Accessibility
| Rule |
Detail |
forced-colors: variant |
Override styles when user has forced-colors enabled |
| Maintain 4.5:1 contrast ratio |
For body text in both modes |
| Maintain 3:1 contrast ratio |
For large text and UI components |
Test with prefers-contrast: more |
Ensure readability in high-contrast mode |
Color System
Color Hierarchy
| Role |
Default Shade |
Usage |
| Primary action |
indigo-600 / dark:indigo-500 |
Buttons, links, active states |
| Primary hover |
indigo-500 / dark:indigo-400 |
Hover on primary actions |
| Heading text |
gray-900 / dark:white |
Page titles, section headings |
| Body text |
gray-600 / dark:gray-300 |
Paragraphs, descriptions |
| Muted/caption |
gray-500 / dark:gray-400 |
Timestamps, helper text |
| Placeholder |
gray-400 / dark:gray-500 |
Input placeholders |
| Success |
green-600 / dark:green-400 |
Confirmation, valid states |
| Warning |
yellow-600 / dark:yellow-500 |
Caution, attention |
| Danger |
red-600 / dark:red-500 |
Errors, destructive actions |
Opacity Modifiers
| Syntax |
Effect |
bg-red-500/10 |
Red background at 10% opacity (subtle tint) |
bg-indigo-600/90 |
Slightly transparent primary |
text-gray-900/80 |
Slightly muted text |
border-gray-900/10 |
Very subtle border |
CSS Variable Theming
<button class="bg-[var(--btn-bg)] text-[var(--btn-text)]"
style="--btn-bg: var(--color-indigo-600); --btn-text: white;">
Submit
</button>
| Theming rule |
Detail |
| Use CSS variables for brand colors |
Allows runtime theme switching |
| Keep utility classes for standard colors |
Use variables only when dynamic theming is needed |
| Arbitrary value syntax for variables |
bg-[var(--color-brand)] or bg-(--color-brand) in v4 |
Typography
Text Size Scale
| Class |
Size |
Default Line Height |
Common Pairing |
text-xs |
0.75rem |
1rem |
Captions, badges |
text-sm |
0.875rem |
1.25rem |
Secondary text, labels |
text-base |
1rem |
1.5rem |
Body text |
text-lg |
1.125rem |
1.75rem |
Subheadings |
text-xl |
1.25rem |
1.75rem |
Section headings |
text-2xl |
1.5rem |
2rem |
Page titles |
text-3xl |
1.875rem |
2.25rem |
Hero subheadings |
text-4xl |
2.25rem |
2.5rem |
Hero headings |
Line Height Slash Syntax (v4)
| Syntax |
Effect |
text-sm/6 |
text-sm with line-height: 1.5rem |
text-base/7 |
text-base with line-height: 1.75rem |
text-xl/8 |
text-xl with line-height: 2rem |
text-sm/[17px] |
Custom arbitrary line height |
Font Weight Pairing
| Element |
Weight |
Class |
| Headings |
Semibold or bold |
font-semibold or font-bold |
| Body |
Normal |
font-normal (default) |
| Labels |
Medium |
font-medium |
| Captions |
Normal or medium |
font-normal or font-medium |
| Emphasis |
Semibold |
font-semibold |
Text Wrapping
| Class |
Use Case |
text-pretty |
Avoids orphan words at end of paragraphs (preferred for body text) |
text-balance |
Evenly balanced line lengths (preferred for headings) |
truncate |
Single line with ellipsis overflow |
line-clamp-3 |
Multi-line truncation (3 lines) |
Responsive Text
<h1 class="text-2xl font-bold tracking-tight sm:text-3xl lg:text-4xl">
Page Title
</h1>
<p class="text-sm/6 text-gray-600 md:text-base/7">
Description text that scales with viewport.
</p>
Spacing and Sizing
Spacing Scale Consistency
| Spacing |
Value |
Common Use |
1 |
0.25rem (4px) |
Tight icon gaps |
1.5 |
0.375rem (6px) |
Small vertical padding |
2 |
0.5rem (8px) |
Compact element padding |
3 |
0.75rem (12px) |
Standard small padding |
4 |
1rem (16px) |
Standard padding and gap |
6 |
1.5rem (24px) |
Section spacing |
8 |
2rem (32px) |
Large section spacing |
10 |
2.5rem (40px) |
Content block separation |
12 |
3rem (48px) |
Major section separation |
16 |
4rem (64px) |
Page-level spacing |
Gap vs Space
| Utility |
Use When |
gap-* |
Flex/grid layouts (preferred, handles wrapping correctly) |
space-y-* |
Vertical stack of direct children, no flex/grid |
space-x-* |
Horizontal stack of direct children, no flex/grid |
Prefer gap over space-* in flex/grid contexts. space-* uses margins and breaks with flex-wrap.
Common Padding Pairs
| Context |
Horizontal |
Vertical |
Combined |
| Small button |
px-2.5 |
py-1.5 |
px-2.5 py-1.5 |
| Standard button |
px-3 |
py-2 |
px-3 py-2 |
| Large button |
px-4 |
py-2.5 |
px-4 py-2.5 |
| Card |
px-4 or px-6 |
py-4 or py-5 |
p-4 or px-6 py-5 |
| Input field |
px-3 |
py-1.5 or py-2 |
px-3 py-1.5 |
| Section |
px-4 sm:px-6 lg:px-8 |
py-12 sm:py-16 |
Responsive both axes |
Negative Margins
| Pattern |
Use Case |
-mx-4 |
Bleed content to edge of padded parent |
-mt-1 |
Pull element up for optical alignment |
-ml-px |
Overlap 1px borders between siblings |
Borders, Rings, and Outlines
When to Use Each
| Utility |
Purpose |
Typical Use |
border |
Structural boundary |
Card edges, input fields, dividers |
ring |
Decorative/state highlight |
Focus states, selection indicator |
outline |
Accessibility focus indicator |
Keyboard focus visibility |
Focus Patterns
| Pattern |
Classes |
Use Case |
| Standard focus ring |
focus:outline-2 focus:outline-offset-2 focus:outline-indigo-600 |
Buttons, links |
| Input focus |
focus:ring-2 focus:ring-inset focus:ring-indigo-600 or focus:outline-2 focus:outline-indigo-600 |
Form fields |
| Focus-visible only |
focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 |
Interactive elements (keyboard only) |
| No focus ring |
Never remove without replacement |
Accessibility violation |
Subtle Borders with Opacity
| Pattern |
Classes |
| Subtle card border |
border border-gray-900/10 dark:border-white/10 |
| Separator |
border-t border-gray-900/5 |
| Elevated surface |
ring-1 ring-gray-900/5 dark:ring-white/5 |
Inset Rings
| Pattern |
Classes |
Use Case |
| Inset input ring |
ring-1 ring-inset ring-gray-300 |
Default input border |
| Inset focus ring |
ring-2 ring-inset ring-indigo-600 |
Focused input |
| Inset error ring |
ring-2 ring-inset ring-red-500 |
Validation error |
State Variants
Standard States
| Variant |
Applies When |
Common Use |
hover: |
Mouse over |
Background/text color change |
focus: |
Element focused (any method) |
Outline/ring for inputs |
focus-visible: |
Keyboard focus only |
Outline for buttons/links |
active: |
Being clicked/pressed |
Scale down, darken |
disabled: |
Element disabled |
Opacity reduction, cursor change |
visited: |
Link visited |
Subtle color change |
Disabled Pattern
<button class="bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50
disabled:cursor-not-allowed disabled:hover:bg-indigo-600">
Submit
</button>
Group and Peer States
| Pattern |
Trigger |
Classes |
| Group hover |
Parent hovered |
Parent: group, child: group-hover:text-white |
| Group focus |
Parent focused |
Parent: group, child: group-focus:ring-2 |
| Group data state |
Parent has data attr |
Parent: group, child: group-data-[open]:rotate-180 |
| Named groups |
Multiple nested groups |
Parent: group/item, child: group-hover/item:text-white |
| Peer checked |
Sibling checked |
Sibling: peer, target: peer-checked:bg-indigo-600 |
| Peer focus |
Sibling focused |
Sibling: peer, target: peer-focus:ring-2 |
| Peer invalid |
Sibling invalid |
Sibling: peer, target: peer-invalid:text-red-500 |
Data Attribute Styling
| Pattern |
Classes |
| Toggle checked |
data-[checked]:bg-indigo-600 |
| Open state |
data-[open]:bg-gray-50 |
| Active tab |
data-[selected]:border-indigo-500 data-[selected]:text-indigo-600 |
| State text color |
data-[disabled]:text-gray-400 |
| Headless UI integration |
data-[open]:rotate-180 on disclosure chevron |
Tailwind v4 Changes
| Change |
v3 |
v4 |
| Configuration |
tailwind.config.js |
CSS-first: @theme in CSS file |
| Custom utilities |
Plugin API in config |
@utility directive in CSS |
| Color opacity |
bg-red-500/50 |
Same syntax, now also bg-red-500/[0.5] |
| Line height |
text-sm leading-6 |
text-sm/6 (slash syntax) |
| CSS variables |
theme() function |
var(--color-*) native access |
| Import |
@tailwind base/components/utilities |
@import "tailwindcss" |
| Arbitrary properties |
[--my-var:value] |
Same, plus (--my-var) shorthand |
@theme Directive
@import "tailwindcss";
@theme {
--color-brand: #4f46e5;
--color-brand-light: #818cf8;
--font-display: "Inter", sans-serif;
--breakpoint-3xl: 1920px;
}
@utility Directive
@utility scrollbar-hidden {
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
@custom-variant Directive
@custom-variant dark (&:where(.dark, .dark *));
@custom-variant theme-midnight (&:where([data-theme="midnight"] *));
@variant Directive (Apply Variants in Custom CSS)
.my-element {
background: white;
@variant dark {
background: black;
}
}
@source Directive (Content Detection)
@import "tailwindcss";
@source "../node_modules/@my-company/ui-lib";
@source not "./src/legacy";
@source inline("underline");
@source inline("{hover:,}bg-red-{50,{100..900..100},950}");
Built-in Container Queries (No Plugin)
<div class="@container">
<div class="grid grid-cols-1 @sm:grid-cols-3 @lg:grid-cols-4">...</div>
</div>
<div class="@container/main">
<div class="@sm:@max-md:grid-cols-2">...</div>
</div>
v4 New Features
| Feature |
Syntax |
Use Case |
| Dynamic spacing |
w-17, pr-29, grid-cols-15 |
Any numeric value without config |
| Gradient rename |
bg-linear-to-r (was bg-gradient-to-r) |
Linear gradients |
| Gradient angles |
bg-linear-45 |
Arbitrary gradient angles |
| Radial/conic |
bg-radial-*, bg-conic-* |
Radial and conic gradients |
| OKLCH colors |
Default palette |
Wider gamut, more vivid |
| 3D transforms |
rotate-x-*, rotate-y-*, perspective-* |
3D effects |
| Not variant |
not-hover:opacity-100 |
Negation |
| In variant |
in-data-open:block |
Implicit group (ancestor match) |
| Nth variant |
nth-3:bg-gray-50 |
Child targeting |
| Starting style |
starting:opacity-0 |
Entry animations (@starting-style) |
| Inert variant |
inert:opacity-50 |
Non-interactive elements |
v4.1 Features
| Feature |
Syntax |
Use Case |
| Text shadows |
text-shadow-sm, text-shadow-lg |
Text readability on images |
| Text shadow color |
text-shadow-sky-300/50 |
Colored text glow |
| Masks |
mask-b-from-50%, mask-radial-* |
Image fade/reveal effects |
| Colored drop shadows |
drop-shadow-cyan-500/50 |
Colored SVG/element shadows |
| Overflow wrap |
wrap-break-word, wrap-anywhere |
Long word handling |
| Input device |
pointer-fine:, pointer-coarse: |
Mouse vs touch adaptation |
| Safe alignment |
justify-center-safe, items-center-safe |
Auto-fix overflow alignment |
| User validation |
user-valid:, user-invalid: |
Post-interaction form states |
| No-JS fallback |
noscript:block |
Styles when JS disabled |
v4 Migration Checklist
| Action |
Detail |
| Replace config file |
Move theme tokens to @theme in CSS |
| Update imports |
@import "tailwindcss" replaces three @tailwind directives |
| Test slash syntax |
text-sm/6 replaces text-sm leading-6 |
| Audit plugins |
Move custom utilities to @utility, variants to @custom-variant |
| Check color references |
theme() calls become var(--color-*) |
| Rename gradients |
bg-gradient-to-r â bg-linear-to-r |
| Content detection |
Remove content array, use @source for non-auto paths |
| Container queries |
Remove @tailwindcss/container-queries plugin, use built-in @container |
| Run upgrade tool |
npx @tailwindcss/upgrade for automated migration |
Common Anti-Patterns
| Anti-Pattern |
Why It Is Wrong |
Correct Approach |
@apply in component files |
Defeats utility-first; hides styles from scanning |
Use cn() or direct utility classes |
Dynamic class construction: `text-${color}-500` |
Tailwind cannot scan dynamic strings; classes get purged |
Use complete class names: cn(colorMap[color]) |
Inline style for Tailwind-expressible values |
Bypasses design system, no responsive/state variants |
Use utility classes or arbitrary values w-[72px] |
| Nesting Tailwind in CSS files |
Creates specificity issues and maintenance burden |
Use utilities in markup, CSS only for base styles |
| Hardcoded colors without dark mode |
Broken in dark mode, inconsistent theming |
Always pair: bg-white dark:bg-gray-950 |
Using !important via ! prefix |
Specificity hack that masks structural issues |
Fix class order or use tw-merge for overrides |
Redundant classes: flex flex-row |
flex-row is the default flex direction |
Just use flex |
Missing focus-visible on interactive elements |
Keyboard users cannot see focus indicator |
Always add focus-visible styles |
Using space-* with flex-wrap |
Margins from space-* break wrapped layouts |
Use gap-* instead |
Deeply nested arbitrary values: [calc(100%-var(--x))] |
Unreadable, hard to maintain |
Extract to CSS variable or custom utility |
Using pixel values everywhere: w-[347px] |
Breaks responsiveness and spacing consistency |
Use Tailwind scale values: w-80, max-w-sm |
Not using tw-merge for component overrides |
Later classes do not reliably override earlier ones |
Wrap with cn() using tw-merge |
Applying text-* color to SVG |
text-* sets CSS color, SVGs need fill/stroke |
Use fill-current or stroke-current with text-* |
| Forgetting responsive prefixes on layout |
Layout breaks on different screen sizes |
Always test and apply sm:, md:, lg: as needed |
Using sm: to target mobile |
sm: means 640px+, not “small screens” |
Unprefixed = mobile; sm: = tablet+ |
Stacking conflicting utilities: grid flex |
Later in stylesheet wins, not later in class string |
Use conditional: isGrid ? "grid" : "flex" |
| Arbitrary values when token exists |
Bypasses design system consistency |
Use w-80 not w-[320px] when token matches |
MCP Component Library
The frontend-components MCP server provides browsable component libraries for reference and reuse:
| Framework |
ID |
What it provides |
| HyperUI |
hyperui |
481 pure HTML/Tailwind components (application, marketing, neobrutalism) |
| DaisyUI |
daisyui |
65 component class references with examples |
| FlyonUI |
flyonui |
49 CSS components + 24 JS plugins |
| HeadlessUI React |
headlessui-react |
38 accessible React component examples |
| HeadlessUI Vue |
headlessui-vue |
30 accessible Vue component examples |
Key tools: list_frameworks, list_components(framework), get_component(framework, ...), search_components(query), get_component_by_path(path)
Use search_components(query: "keyword") to find components across all libraries, or filter by framework.