tailwind-design-system

CSS-first design system framework for Tailwind v4 with tokens, components, and responsive patterns. Migrates configuration from tailwind.config.ts to CSS @theme blocks with native CSS variables, OKLCH color spaces, and @custom-variant for dark mode Provides production-ready component patterns including CVA-based variants, compound components, form controls, grids, and animations using native @keyframes and @starting-style Includes design token hierarchy (brand → semantic → component), responsive utilities like size-* shorthand, and accessibility-first patterns with ARIA attributes and focus states Covers v3-to-v4 migration with checklist, custom @utility directives, container queries, and color-mix() for alpha variants

INSTALLATION
npx skills add https://github.com/wshobson/agents --skill tailwind-design-system
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

Tailwind Design System (v4)

Build production-ready design systems with Tailwind CSS v4, including CSS-first configuration, design tokens, component variants, responsive patterns, and accessibility.

Note: This skill targets Tailwind CSS v4 (2024+). For v3 projects, refer to the upgrade guide.

When to Use This Skill

  • Creating a component library with Tailwind v4
  • Implementing design tokens and theming with CSS-first configuration
  • Building responsive and accessible components
  • Standardizing UI patterns across a codebase
  • Migrating from Tailwind v3 to v4
  • Setting up dark mode with native CSS features

Key v4 Changes

v3 Pattern

v4 Pattern

tailwind.config.ts

@theme in CSS

@tailwind base/components/utilities

@import "tailwindcss"

darkMode: "class"

@custom-variant dark (&:where(.dark, .dark *))

theme.extend.colors

@theme { --color-*: value }

require("tailwindcss-animate")

CSS @keyframes in @theme + @starting-style for entry animations

Quick Start

/* app.css - Tailwind v4 CSS-first configuration */

@import "tailwindcss";

/* Define your theme with @theme */

@theme {

  /* Semantic color tokens using OKLCH for better color perception */

  --color-background: oklch(100% 0 0);

  --color-foreground: oklch(14.5% 0.025 264);

  --color-primary: oklch(14.5% 0.025 264);

  --color-primary-foreground: oklch(98% 0.01 264);

  --color-secondary: oklch(96% 0.01 264);

  --color-secondary-foreground: oklch(14.5% 0.025 264);

  --color-muted: oklch(96% 0.01 264);

  --color-muted-foreground: oklch(46% 0.02 264);

  --color-accent: oklch(96% 0.01 264);

  --color-accent-foreground: oklch(14.5% 0.025 264);

  --color-destructive: oklch(53% 0.22 27);

  --color-destructive-foreground: oklch(98% 0.01 264);

  --color-border: oklch(91% 0.01 264);

  --color-ring: oklch(14.5% 0.025 264);

  --color-card: oklch(100% 0 0);

  --color-card-foreground: oklch(14.5% 0.025 264);

  /* Ring offset for focus states */

  --color-ring-offset: oklch(100% 0 0);

  /* Radius tokens */

  --radius-sm: 0.25rem;

  --radius-md: 0.375rem;

  --radius-lg: 0.5rem;

  --radius-xl: 0.75rem;

  /* Animation tokens - keyframes inside @theme are output when referenced by --animate-* variables */

  --animate-fade-in: fade-in 0.2s ease-out;

  --animate-fade-out: fade-out 0.2s ease-in;

  --animate-slide-in: slide-in 0.3s ease-out;

  --animate-slide-out: slide-out 0.3s ease-in;

  @keyframes fade-in {

    from {

      opacity: 0;

    }

    to {

      opacity: 1;

    }

  }

  @keyframes fade-out {

    from {

      opacity: 1;

    }

    to {

      opacity: 0;

    }

  }

  @keyframes slide-in {

    from {

      transform: translateY(-0.5rem);

      opacity: 0;

    }

    to {

      transform: translateY(0);

      opacity: 1;

    }

  }

  @keyframes slide-out {

    from {

      transform: translateY(0);

      opacity: 1;

    }

    to {

      transform: translateY(-0.5rem);

      opacity: 0;

    }

  }

}

/* Dark mode variant - use @custom-variant for class-based dark mode */

@custom-variant dark (&:where(.dark, .dark *));

/* Dark mode theme overrides */

.dark {

  --color-background: oklch(14.5% 0.025 264);

  --color-foreground: oklch(98% 0.01 264);

  --color-primary: oklch(98% 0.01 264);

  --color-primary-foreground: oklch(14.5% 0.025 264);

  --color-secondary: oklch(22% 0.02 264);

  --color-secondary-foreground: oklch(98% 0.01 264);

  --color-muted: oklch(22% 0.02 264);

  --color-muted-foreground: oklch(65% 0.02 264);

  --color-accent: oklch(22% 0.02 264);

  --color-accent-foreground: oklch(98% 0.01 264);

  --color-destructive: oklch(42% 0.15 27);

  --color-destructive-foreground: oklch(98% 0.01 264);

  --color-border: oklch(22% 0.02 264);

  --color-ring: oklch(83% 0.02 264);

  --color-card: oklch(14.5% 0.025 264);

  --color-card-foreground: oklch(98% 0.01 264);

  --color-ring-offset: oklch(14.5% 0.025 264);

}

/* Base styles */

@layer base {

  * {

    @apply border-border;

  }

  body {

    @apply bg-background text-foreground antialiased;

  }

}

Core Concepts

1. Design Token Hierarchy

Brand Tokens (abstract)

    └── Semantic Tokens (purpose)

        └── Component Tokens (specific)

Example:

    oklch(45% 0.2 260) → --color-primary → bg-primary

2. Component Architecture

Base styles → Variants → Sizes → States → Overrides

Patterns

Pattern 1: CVA (Class Variance Authority) Components

// components/ui/button.tsx

import { Slot } from '@radix-ui/react-slot'

import { cva, type VariantProps } from 'class-variance-authority'

import { cn } from '@/lib/utils'

const buttonVariants = cva(

  // Base styles - v4 uses native CSS variables

  'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',

  {

    variants: {

      variant: {

        default: 'bg-primary text-primary-foreground hover:bg-primary/90',

        destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',

        outline: 'border border-border bg-background hover:bg-accent hover:text-accent-foreground',

        secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',

        ghost: 'hover:bg-accent hover:text-accent-foreground',

        link: 'text-primary underline-offset-4 hover:underline',

      },

      size: {

        default: 'h-10 px-4 py-2',

        sm: 'h-9 rounded-md px-3',

        lg: 'h-11 rounded-md px-8',

        icon: 'size-10',

      },

    },

    defaultVariants: {

      variant: 'default',

      size: 'default',

    },

  }

)

export interface ButtonProps

  extends React.ButtonHTMLAttributes<HTMLButtonElement>,

    VariantProps<typeof buttonVariants> {

  asChild?: boolean

}

// React 19: No forwardRef needed

export function Button({

  className,

  variant,

  size,

  asChild = false,

  ref,

  ...props

}: ButtonProps &#x26; { ref?: React.Ref<HTMLButtonElement> }) {

  const Comp = asChild ? Slot : 'button'

  return (

    <Comp

      className={cn(buttonVariants({ variant, size, className }))}

      ref={ref}

      {...props}

    />

  )

}

// Usage

<Button variant="destructive" size="lg">Delete</Button>

<Button variant="outline">Cancel</Button>

<Button asChild><Link href="/home">Home</Link></Button>

Pattern 2: Compound Components (React 19)

// components/ui/card.tsx

import { cn } from '@/lib/utils'

// React 19: ref is a regular prop, no forwardRef

export function Card({

  className,

  ref,

  ...props

}: React.HTMLAttributes<HTMLDivElement> &#x26; { ref?: React.Ref<HTMLDivElement> }) {

  return (

    <div

      ref={ref}

      className={cn(

        'rounded-lg border border-border bg-card text-card-foreground shadow-sm',

        className

      )}

      {...props}

    />

  )

}

export function CardHeader({

  className,

  ref,

  ...props

}: React.HTMLAttributes<HTMLDivElement> &#x26; { ref?: React.Ref<HTMLDivElement> }) {

  return (

    <div

      ref={ref}

      className={cn('flex flex-col space-y-1.5 p-6', className)}

      {...props}

    />

  )

}

export function CardTitle({

  className,

  ref,

  ...props

}: React.HTMLAttributes<HTMLHeadingElement> &#x26; { ref?: React.Ref<HTMLHeadingElement> }) {

  return (

    <h3

      ref={ref}

      className={cn('text-2xl font-semibold leading-none tracking-tight', className)}

      {...props}

    />

  )

}

export function CardDescription({

  className,

  ref,

  ...props

}: React.HTMLAttributes<HTMLParagraphElement> &#x26; { ref?: React.Ref<HTMLParagraphElement> }) {

  return (

    <p

      ref={ref}

      className={cn('text-sm text-muted-foreground', className)}

      {...props}

    />

  )

}

export function CardContent({

  className,

  ref,

  ...props

}: React.HTMLAttributes<HTMLDivElement> &#x26; { ref?: React.Ref<HTMLDivElement> }) {

  return (

    <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />

  )

}

export function CardFooter({

  className,

  ref,

  ...props

}: React.HTMLAttributes<HTMLDivElement> &#x26; { ref?: React.Ref<HTMLDivElement> }) {

  return (

    <div

      ref={ref}

      className={cn('flex items-center p-6 pt-0', className)}

      {...props}

    />

  )

}

// Usage

<Card>

  <CardHeader>

    <CardTitle>Account</CardTitle>

    <CardDescription>Manage your account settings</CardDescription>

  </CardHeader>

  <CardContent>

    <form>...</form>

  </CardContent>

  <CardFooter>

    <Button>Save</Button>

  </CardFooter>

</Card>

Pattern 3: Form Components

// components/ui/input.tsx

import { cn } from '@/lib/utils'

export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {

  error?: string

  ref?: React.Ref<HTMLInputElement>

}

export function Input({ className, type, error, ref, ...props }: InputProps) {

  return (

    <div className="relative">

      <input

        type={type}

        className={cn(

          'flex h-10 w-full rounded-md border border-border bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',

          error &#x26;&#x26; 'border-destructive focus-visible:ring-destructive',

          className

        )}

        ref={ref}

        aria-invalid={!!error}

        aria-describedby={error ? `${props.id}-error` : undefined}

        {...props}

      />

      {error &#x26;&#x26; (

        <p

          id={`${props.id}-error`}

          className="mt-1 text-sm text-destructive"

          role="alert"

        >

          {error}

        </p>

      )}

    </div>

  )

}

// components/ui/label.tsx

import { cva, type VariantProps } from 'class-variance-authority'

const labelVariants = cva(

  'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'

)

export function Label({

  className,

  ref,

  ...props

}: React.LabelHTMLAttributes<HTMLLabelElement> &#x26; { ref?: React.Ref<HTMLLabelElement> }) {

  return (

    <label ref={ref} className={cn(labelVariants(), className)} {...props} />

  )

}

// Usage with React Hook Form + Zod

import { useForm } from 'react-hook-form'

import { zodResolver } from '@hookform/resolvers/zod'

import { z } from 'zod'

const schema = z.object({

  email: z.string().email('Invalid email address'),

  password: z.string().min(8, 'Password must be at least 8 characters'),

})

function LoginForm() {

  const { register, handleSubmit, formState: { errors } } = useForm({

    resolver: zodResolver(schema),

  })

  return (

    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">

      <div className="space-y-2">

        <Label htmlFor="email">Email</Label>

        <Input

          id="email"

          type="email"

          {...register('email')}

          error={errors.email?.message}

        />

      </div>

      <div className="space-y-2">

        <Label htmlFor="password">Password</Label>

        <Input

          id="password"

          type="password"

          {...register('password')}

          error={errors.password?.message}

        />

      </div>

      <Button type="submit" className="w-full">Sign In</Button>

    </form>

  )

}

Pattern 4: Responsive Grid System

// components/ui/grid.tsx

import { cn } from '@/lib/utils'

import { cva, type VariantProps } from 'class-variance-authority'

const gridVariants = cva('grid', {

  variants: {

    cols: {

      1: 'grid-cols-1',

      2: 'grid-cols-1 sm:grid-cols-2',

      3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',

      4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',

      5: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-5',

      6: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-6',

    },

    gap: {

      none: 'gap-0',

      sm: 'gap-2',

      md: 'gap-4',

      lg: 'gap-6',

      xl: 'gap-8',

    },

  },

  defaultVariants: {

    cols: 3,

    gap: 'md',

  },

})

interface GridProps

  extends React.HTMLAttributes<HTMLDivElement>,

    VariantProps<typeof gridVariants> {}

export function Grid({ className, cols, gap, ...props }: GridProps) {

  return (

    <div className={cn(gridVariants({ cols, gap, className }))} {...props} />

  )

}

// Container component

const containerVariants = cva('mx-auto w-full px-4 sm:px-6 lg:px-8', {

  variants: {

    size: {

      sm: 'max-w-screen-sm',

      md: 'max-w-screen-md',

      lg: 'max-w-screen-lg',

      xl: 'max-w-screen-xl',

      '2xl': 'max-w-screen-2xl',

      full: 'max-w-full',

    },

  },

  defaultVariants: {

    size: 'xl',

  },

})

interface ContainerProps

  extends React.HTMLAttributes<HTMLDivElement>,

    VariantProps<typeof containerVariants> {}

export function Container({ className, size, ...props }: ContainerProps) {

  return (

    <div className={cn(containerVariants({ size, className }))} {...props} />

  )

}

// Usage

<Container>

  <Grid cols={4} gap="lg">

    {products.map((product) => (

      <ProductCard key={product.id} product={product} />

    ))}

  </Grid>

</Container>

For advanced animation and dark mode patterns, see references/advanced-patterns.md:

  • Pattern 5: Native CSS Animations — dialog @keyframes, native popover API with @starting-style, allow-discrete transitions, and a full DialogContent/DialogOverlay implementation using Radix UI
  • Pattern 6: Dark ModeThemeProvider context with localStorage persistence, prefers-color-scheme detection, meta theme-color update, and a ThemeToggle button component

Utility Functions

// lib/utils.ts

import { type ClassValue, clsx } from "clsx";

import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {

  return twMerge(clsx(inputs));

}

// Focus ring utility

export const focusRing = cn(

  "focus-visible:outline-none focus-visible:ring-2",

  "focus-visible:ring-ring focus-visible:ring-offset-2",

);

// Disabled utility

export const disabled = "disabled:pointer-events-none disabled:opacity-50";

For advanced v4 CSS patterns, the full v3-to-v4 migration checklist, and complete best practices, see references/advanced-patterns.md:

  • **Custom @utility** — reusable CSS utilities for decorative lines and text gradients
  • Theme modifiers@theme inline (reference other CSS vars), @theme static (always output), @import "tailwindcss" theme(static)
  • Namespace overrides — clearing default Tailwind color scales with --color-*: initial
  • Semi-transparent variantscolor-mix() for alpha scale generation
  • Container queries--container-* token definitions
  • v3→v4 migration checklist — 10-item checklist covering config, directives, colors, dark mode, animations, React 19 ref changes
  • Best practices — full Do's and Don'ts list
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card