tailwind

SKILL.md

Tailwind CSS

When to Use

shadcn/ui is the primary component framework. This skill covers the Tailwind CSS utility system that powers shadcn components — use it for layout, responsive design, spacing, theming, and custom styling beyond what shadcn provides out of the box.

Use this skill for:

  • Page layout (flex, grid, positioning, spacing)
  • Responsive design (breakpoints, container queries)
  • Theming and design tokens (@theme directive, CSS variables)
  • Customizing shadcn/ui components with utility overrides
  • Animation and transitions
  • State-based styling (hover, focus, disabled)

Defer to the shadcn-ui skill for: component selection, composition patterns (Dialog, Sheet, Command), form integration (react-hook-form + zod), and accessibility.

Setup (v4)

Tailwind v4 uses CSS-first configuration. No tailwind.config.js needed.

Import

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

This single import replaces the v3 directives (@tailwind base, @tailwind components, @tailwind utilities). Template files are auto-discovered — no content paths required.

Installation

# Next.js / Vite (PostCSS)
npm install tailwindcss @tailwindcss/postcss

# Vite plugin (alternative)
npm install tailwindcss @tailwindcss/vite

PostCSS config:

// postcss.config.mjs
export default {
  plugins: {
    "@tailwindcss/postcss": {},
  },
};

Vite plugin (if not using PostCSS):

// vite.config.ts
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [tailwindcss()],
});

Important: Sass and Less are incompatible with Tailwind v4. Use plain CSS with @theme and @layer directives.

Theming with @theme

The @theme directive defines design tokens in CSS. Each token creates both a utility class and a CSS variable.

Defining Tokens

@import "tailwindcss";

@theme {
  --font-display: "Satoshi", sans-serif;
  --color-brand: oklch(0.72 0.18 50);
  --color-brand-light: oklch(0.92 0.05 50);
  --radius-lg: 0.75rem;
  --breakpoint-3xl: 120rem;
}

This generates:

  • font-display utility class
  • bg-brand, text-brand color utilities
  • rounded-lg using the custom radius
  • 3xl: responsive breakpoint
  • CSS variables: var(--font-display), var(--color-brand), etc.

Overriding vs Extending Defaults

/* Extend — adds to existing colors */
@theme {
  --color-brand: oklch(0.72 0.18 50);
}

/* Override — replaces ALL colors */
@theme {
  --color-*: initial;
  --color-brand: oklch(0.72 0.18 50);
  --color-white: #fff;
  --color-black: #000;
}

Use --color-*: initial (namespace wildcard) to clear defaults before defining your own.

Aligning with shadcn/ui Tokens

shadcn/ui defines semantic tokens in :root and .dark (see shadcn-ui skill for full setup). Tailwind's @theme can reference these:

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
  --color-muted: var(--muted);
  --color-muted-foreground: var(--muted-foreground);
  --color-destructive: var(--destructive);
  --color-border: var(--border);
  --color-ring: var(--ring);
  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
}

The inline keyword tells Tailwind not to emit the variables (shadcn already defines them in :root).

Utility-First Patterns

Core Principle

Style by composing utility classes directly in markup:

<div className="flex items-center gap-3 rounded-lg border p-4">
  <Avatar className="h-10 w-10" />
  <div>
    <p className="text-sm font-medium">Jane Doe</p>
    <p className="text-xs text-muted-foreground">jane@example.com</p>
  </div>
</div>

Spacing

p-4       → padding: 1rem (all sides)
px-6      → padding-left + padding-right: 1.5rem
py-2      → padding-top + padding-bottom: 0.5rem
m-auto    → margin: auto
mt-8      → margin-top: 2rem
gap-4     → gap: 1rem (flex/grid)
space-y-4 → vertical spacing between children (margin-based)

Typography

text-sm           → 0.875rem / 1.25rem
text-lg           → 1.125rem / 1.75rem
font-semibold     → font-weight: 600
tracking-tight    → letter-spacing: -0.025em
leading-relaxed   → line-height: 1.625
truncate          → overflow hidden + text-overflow ellipsis + whitespace nowrap
line-clamp-3      → clamp to 3 lines

Arbitrary Values

Use brackets for one-off values not in the theme:

<div className="w-[calc(100%-2rem)] top-[117px] grid-cols-[1fr_2fr_1fr]">

Use underscores for spaces in arbitrary values: grid-cols-[1fr_2fr_1fr].

Prefer theme tokens (w-96, gap-4) over arbitrary values. Only use brackets when the design truly requires a one-off value not in your design system.

State Variants

<button className="bg-primary hover:bg-primary/90 focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:pointer-events-none">
  Save
</button>

Common variants: hover:, focus:, focus-visible:, active:, disabled:, aria-selected:, data-[state=open]:.

Group and Peer Modifiers

{/* Parent hover affects children */}
<div className="group rounded-lg border p-4 hover:border-primary">
  <h3 className="font-medium group-hover:text-primary">Title</h3>
  <p className="text-muted-foreground group-hover:text-foreground">Description</p>
</div>

{/* Peer state affects siblings */}
<input className="peer" type="email" />
<p className="hidden text-sm text-destructive peer-invalid:block">
  Invalid email
</p>

@apply (Use Sparingly)

@apply extracts utilities into custom CSS classes. In v4, variant modifiers (hover:, md:, focus:) cannot be used inside @apply — use native CSS pseudo-classes and media queries instead, or prefer React components for stateful patterns.

/* OK — simple atomic pattern */
@layer components {
  .prose-link {
    @apply text-primary underline underline-offset-4;
  }
}

/* Prefer a React component instead of @apply for anything with variants */

Layout Patterns

Flexbox

{
  /* Horizontal bar with items centered, space between */
}
<div className="flex items-center justify-between gap-4">
  <Logo />
  <nav className="flex items-center gap-6">{/* links */}</nav>
  <UserMenu />
</div>;

{
  /* Vertical stack */
}
<div className="flex flex-col gap-4">
  <Card />
  <Card />
</div>;

Grid

{
  /* Equal-width columns */
}
<div className="grid grid-cols-3 gap-6">
  <Card />
  <Card />
  <Card />
</div>;

{
  /* Spanning columns */
}
<div className="grid grid-cols-4 gap-6">
  <div className="col-span-3">{/* Main */}</div>
  <div>{/* Sidebar */}</div>
</div>;

{
  /* Auto-fill responsive grid (no breakpoints needed) */
}
<div className="grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-6">
  {items.map((item) => (
    <Card key={item.id} />
  ))}
</div>;

Common Recipes

Sticky header + scrollable content:

<div className="flex h-screen flex-col">
  <header className="sticky top-0 z-10 border-b bg-background px-6 py-3">
    {/* Header */}
  </header>
  <main className="flex-1 overflow-y-auto p-6">{/* Scrollable content */}</main>
</div>

Sidebar layout:

<div className="flex h-screen">
  <aside className="w-64 shrink-0 border-r bg-muted/40 p-4">
    {/* Sidebar */}
  </aside>
  <main className="flex-1 overflow-y-auto p-6">{/* Content */}</main>
</div>

Centered content (both axes):

<div className="flex min-h-screen items-center justify-center">
  <Card className="w-full max-w-md">{/* Centered card */}</Card>
</div>

Responsive Design

Mobile-First Breakpoints

Unprefixed utilities apply to all screen sizes. Prefixed utilities apply at that breakpoint and above.

Prefix Min-width Target
(none) 0px All sizes (mobile base)
sm: 640px Large phones
md: 768px Tablets
lg: 1024px Laptops
xl: 1280px Desktops
2xl: 1536px Large desktops

Responsive Stacking to Grid

{
  /* Single column on mobile, 2 cols on tablet, 3 cols on desktop */
}
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
  <Card />
  <Card />
  <Card />
</div>;

Responsive Typography

<h1 className="text-2xl font-bold md:text-3xl lg:text-4xl">Dashboard</h1>

Responsive Spacing

<section className="px-4 py-8 md:px-8 md:py-12 lg:px-16 lg:py-16">
  {/* Content with increasing padding on larger screens */}
</section>

Show/Hide by Breakpoint

{
  /* Mobile navigation toggle — hidden on desktop */
}
<Button className="md:hidden" variant="ghost" size="icon">
  <Menu className="h-5 w-5" />
</Button>;

{
  /* Desktop sidebar — hidden on mobile */
}
<aside className="hidden md:block w-64">{/* Sidebar */}</aside>;

Container Queries

Container queries respond to a parent container's size instead of the viewport:

{
  /* Define container */
}
<div className="@container">
  {/* Respond to container size */}
  <div className="flex flex-col @md:flex-row @md:items-center gap-4">
    <Avatar />
    <div>
      <p className="text-sm @lg:text-base">Name</p>
    </div>
  </div>
</div>;

Container breakpoints: @sm (320px), @md (448px), @lg (512px), @xl (576px), etc.

Custom Breakpoints

@theme {
  --breakpoint-3xl: 120rem;
  --breakpoint-xs: 480px;
}

Dark Mode

With shadcn/ui (Preferred Approach)

When using shadcn/ui, dark mode is handled by CSS variables. The :root and .dark selectors define token values, and utilities like bg-background, text-foreground, border-border automatically switch. No dark: prefix needed for themed elements.

{
  /* These auto-switch between light/dark — no dark: prefix */
}
<div className="bg-background text-foreground border-border">
  <p className="text-muted-foreground">Auto-themed content</p>
</div>;

See the shadcn-ui skill for next-themes setup and theme toggle implementation.

dark: Variant (For Non-Themed Values)

Use dark: only when you need behavior outside the shadcn token system:

{
  /* Custom illustration that needs different treatment in dark mode */
}
<div className="bg-blue-50 dark:bg-blue-950">
  <img className="opacity-100 dark:opacity-80" src="/illustration.svg" />
</div>;

Media Query Strategy

For system-only dark mode (no toggle), Tailwind uses prefers-color-scheme by default. The class-based strategy (required for next-themes toggle) is configured by shadcn's setup.

Customizing shadcn Components with Tailwind

Using cn() to Override Styles

shadcn components accept className. Use cn() (from @/lib/utils) to merge your utilities with the component's defaults:

import { Button } from "@/components/ui/button";

{
  /* Full-width button with left-aligned text */
}
<Button className="w-full justify-start text-left font-normal">
  Select a date
</Button>;

{
  /* Card with custom max-width and shadow */
}
<Card className="max-w-lg shadow-lg">
  <CardContent>{/* ... */}</CardContent>
</Card>;

cn() handles class conflicts — your overrides win over component defaults (e.g., adding justify-start replaces the button's default justify-center).

Adding Responsive Behavior

{
  /* Dialog content that's full-width on mobile, constrained on desktop */
}
<DialogContent className="w-full max-w-full sm:max-w-lg">
  {/* ... */}
</DialogContent>;

{
  /* Sidebar that collapses on mobile */
}
<SheetContent side="left" className="w-[280px] sm:w-[350px]">
  {/* ... */}
</SheetContent>;

When to Customize via Utilities vs Component Source

Scenario Approach
One-off sizing/spacing tweak className override
Consistent variant across the app Edit component source in components/ui/
New variant (e.g., variant="warning") Add to component's cva() variants
Layout around component Wrapper div with utilities

Animation and Transitions

Transitions

{
  /* Smooth hover effect */
}
<div className="transition-colors duration-200 hover:bg-muted">
  {/* content */}
</div>;

{
  /* Transform on hover */
}
<div className="transition-transform duration-300 hover:scale-105">
  {/* content */}
</div>;

{
  /* Multiple properties */
}
<div className="transition-all duration-200 ease-in-out">{/* content */}</div>;

Common duration values: duration-75, duration-100, duration-150, duration-200, duration-300, duration-500.

Built-in Animations

<Loader2 className="h-4 w-4 animate-spin" />      {/* Spinning loader */}
<div className="animate-pulse">Loading...</div>      {/* Pulsing skeleton */}
<div className="animate-bounce"></div>              {/* Bouncing arrow */}

tw-animate-css

The tw-animate-css package replaces the deprecated tailwindcss-animate plugin. It provides enter/exit animations used by shadcn/ui components (Dialog, Sheet, Popover, etc.).

npm install tw-animate-css
/* globals.css */
@import "tailwindcss";
@import "tw-animate-css";

Custom Keyframes

/* globals.css */
@theme {
  --animate-fade-in: fade-in 0.3s ease-out;
  --animate-slide-up: slide-up 0.4s ease-out;
}

@keyframes fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes slide-up {
  from {
    transform: translateY(1rem);
    opacity: 0;
  }
  to {
    transform: translateY(0);
    opacity: 1;
  }
}

Usage: <div className="animate-fade-in">.

Anti-Patterns

1. Dynamic Class Construction

// BAD — Tailwind can't scan dynamically built class names
function Badge({ color }: { color: string }) {
  return <span className={`bg-${color}-500 text-${color}-50`}>...</span>;
}

// GOOD — use a static lookup map
const colorStyles = {
  red: "bg-red-500 text-red-50",
  blue: "bg-blue-500 text-blue-50",
  green: "bg-green-500 text-green-50",
} as const;

function Badge({ color }: { color: keyof typeof colorStyles }) {
  return <span className={colorStyles[color]}>...</span>;
}

Tailwind scans source files for complete class names at build time. String interpolation produces class names that don't exist in the source, so they won't be generated.

2. @apply Overuse

/* BAD — reimplements what a React component does better */
.card {
  @apply flex flex-col gap-4 rounded-lg border bg-background p-6 shadow-sm;
}
.card-title {
  @apply text-lg font-semibold leading-none tracking-tight;
}

/* GOOD — use shadcn's <Card> component, or a custom React component */

@apply hides styles from the markup, makes them harder to override, and in v4, modifiers like hover: and responsive prefixes don't work on custom classes. Prefer React components for anything with variants or state.

3. Hardcoded Colors Instead of Theme Tokens

// BAD — breaks dark mode, ignores theme
<div className="bg-white text-gray-900 border-gray-200">

// GOOD — uses semantic tokens (auto-switches in dark mode)
<div className="bg-background text-foreground border-border">

Always use semantic token classes (bg-primary, text-muted-foreground, border-border) instead of raw color values. The tokens are defined by shadcn's CSS variables and switch automatically between light/dark.

4. Redundant dark: With CSS Variables

// BAD — unnecessary when using shadcn tokens
<div className="bg-background dark:bg-background text-foreground dark:text-foreground">

// ALSO BAD — fighting the token system
<div className="bg-white dark:bg-gray-900 text-black dark:text-white">

// GOOD — tokens handle light/dark automatically
<div className="bg-background text-foreground">

If you're using shadcn/ui's CSS variable system, dark: prefixes on themed properties are redundant. Only use dark: for values outside the token system.

5. Desktop-First Design

// BAD — styles for large screens, then overrides for small
<div className="grid grid-cols-3 gap-8 sm:grid-cols-1 sm:gap-4">

// GOOD — mobile base, enhance for larger screens
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 lg:gap-8">

Tailwind breakpoints are min-width (mobile-first). Start with mobile styles (no prefix), then add complexity at larger breakpoints.

6. Mixing Sass/Less with v4

// BAD — Sass is incompatible with Tailwind v4's Oxide engine
$primary: #3b82f6;
.button {
  background: $primary;
  @apply rounded-lg px-4 py-2;
}
/* GOOD — use native CSS with @theme */
@theme {
  --color-primary: oklch(0.62 0.21 255);
}

Tailwind v4 processes CSS natively through its Rust-based engine. Sass/Less syntax causes build failures.

7. Overly Long Class Strings Without Extraction

// BAD — same pattern repeated in 5 places
<div className="flex items-center gap-3 rounded-lg border bg-card p-4 shadow-sm transition-colors hover:bg-accent">
  ...
</div>;
{
  /* ...repeated 4 more times */
}

// GOOD — extract to a component
function ListItem({ children, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      className={cn(
        "flex items-center gap-3 rounded-lg border bg-card p-4 shadow-sm transition-colors hover:bg-accent",
        props.className,
      )}
      {...props}
    >
      {children}
    </div>
  );
}

If a utility combination repeats more than twice, extract it to a React component. Always forward className via cn() so consumers can override styles.

Weekly Installs
5
GitHub Stars
1
First Seen
12 days ago
Installed on
opencode5
mcpjam3
claude-code3
junie3
windsurf3
zencoder3