tailwind-design-system
upstream

Build scalable design systems with Tailwind CSS, design tokens, component libraries, and responsive patterns. Use when creating component libraries, implementing design systems, or standardizing UI patterns.

ID: tailwind-design-system
Preview
Codigo

Tailwind Design System


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


When to Use This Skill


  • Creating a component library with Tailwind
  • Implementing design tokens and theming
  • Building responsive and accessible components
  • Standardizing UI patterns across a codebase
  • Migrating to or extending Tailwind CSS
  • Setting up dark mode and color schemes

Core Concepts


1. Design Token Hierarchy


``

Brand Tokens (abstract)

ÔööÔöÇÔöÇ Semantic Tokens (purpose)

ÔööÔöÇÔöÇ Component Tokens (specific)


Example:

blue-500  primary  button-bg

`


2. Component Architecture


`

Base styles  Variants  Sizes  States  Overrides

`


Quick Start


`typescript

// tailwind.config.ts

import type { Config } from 'tailwindcss'


const config: Config = {

content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],

darkMode: 'class',

theme: {

extend: {

colors: {

// Semantic color tokens

primary: {

DEFAULT: 'hsl(var(--primary))',

foreground: 'hsl(var(--primary-foreground))',

},

secondary: {

DEFAULT: 'hsl(var(--secondary))',

foreground: 'hsl(var(--secondary-foreground))',

},

destructive: {

DEFAULT: 'hsl(var(--destructive))',

foreground: 'hsl(var(--destructive-foreground))',

},

muted: {

DEFAULT: 'hsl(var(--muted))',

foreground: 'hsl(var(--muted-foreground))',

},

accent: {

DEFAULT: 'hsl(var(--accent))',

foreground: 'hsl(var(--accent-foreground))',

},

background: 'hsl(var(--background))',

foreground: 'hsl(var(--foreground))',

border: 'hsl(var(--border))',

ring: 'hsl(var(--ring))',

},

borderRadius: {

lg: 'var(--radius)',

md: 'calc(var(--radius) - 2px)',

sm: 'calc(var(--radius) - 4px)',

},

},

},

plugins: [require('tailwindcss-animate')],

}


export default config

`


`css

/* globals.css */

@tailwind base;

@tailwind components;

@tailwind utilities;


@layer base {

:root {

--background: 0 0% 100%;

--foreground: 222.2 84% 4.9%;

--primary: 222.2 47.4% 11.2%;

--primary-foreground: 210 40% 98%;

--secondary: 210 40% 96.1%;

--secondary-foreground: 222.2 47.4% 11.2%;

--muted: 210 40% 96.1%;

--muted-foreground: 215.4 16.3% 46.9%;

--accent: 210 40% 96.1%;

--accent-foreground: 222.2 47.4% 11.2%;

--destructive: 0 84.2% 60.2%;

--destructive-foreground: 210 40% 98%;

--border: 214.3 31.8% 91.4%;

--ring: 222.2 84% 4.9%;

--radius: 0.5rem;

}


.dark {

--background: 222.2 84% 4.9%;

--foreground: 210 40% 98%;

--primary: 210 40% 98%;

--primary-foreground: 222.2 47.4% 11.2%;

--secondary: 217.2 32.6% 17.5%;

--secondary-foreground: 210 40% 98%;

--muted: 217.2 32.6% 17.5%;

--muted-foreground: 215 20.2% 65.1%;

--accent: 217.2 32.6% 17.5%;

--accent-foreground: 210 40% 98%;

--destructive: 0 62.8% 30.6%;

--destructive-foreground: 210 40% 98%;

--border: 217.2 32.6% 17.5%;

--ring: 212.7 26.8% 83.9%;

}

}

`


Patterns


Pattern 1: CVA (Class Variance Authority) Components


`typescript

// components/ui/button.tsx

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

import { forwardRef } from 'react'

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


const buttonVariants = cva(

// Base styles

'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background 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-input 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: 'h-10 w-10',

},

},

defaultVariants: {

variant: 'default',

size: 'default',

},

}

)


export interface ButtonProps

extends React.ButtonHTMLAttributes<HTMLButtonElement>,

VariantProps<typeof buttonVariants> {

asChild?: boolean

}


const Button = forwardRef<HTMLButtonElement, ButtonProps>(

({ className, variant, size, asChild = false, ...props }, ref) => {

const Comp = asChild ? Slot : 'button'

return (

<Comp

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

ref={ref}

{...props}

/>

)

}

)

Button.displayName = 'Button'


export { Button, buttonVariants }


// 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


`typescript

// components/ui/card.tsx

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

import { forwardRef } from 'react'


const Card = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(

({ className, ...props }, ref) => (

<div

ref={ref}

className={cn(

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

className

)}

{...props}

/>

)

)

Card.displayName = 'Card'


const CardHeader = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(

({ className, ...props }, ref) => (

<div

ref={ref}

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

{...props}

/>

)

)

CardHeader.displayName = 'CardHeader'


const CardTitle = forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(

({ className, ...props }, ref) => (

<h3

ref={ref}

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

{...props}

/>

)

)

CardTitle.displayName = 'CardTitle'


const CardDescription = forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(

({ className, ...props }, ref) => (

<p

ref={ref}

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

{...props}

/>

)

)

CardDescription.displayName = 'CardDescription'


const CardContent = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(

({ className, ...props }, ref) => (

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

)

)

CardContent.displayName = 'CardContent'


const CardFooter = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(

({ className, ...props }, ref) => (

<div

ref={ref}

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

{...props}

/>

)

)

CardFooter.displayName = 'CardFooter'


export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter }


// 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


`typescript

// components/ui/input.tsx

import { forwardRef } from 'react'

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


export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {

error?: string

}


const Input = forwardRef<HTMLInputElement, InputProps>(

({ className, type, error, ...props }, ref) => {

return (

<div className="relative">

<input

type={type}

className={cn(

'flex h-10 w-full rounded-md border border-input 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 && 'border-destructive focus-visible:ring-destructive',

className

)}

ref={ref}

aria-invalid={!!error}

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

{...props}

/>

{error && (

<p

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

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

role="alert"

>

{error}

</p>

)}

</div>

)

}

)

Input.displayName = 'Input'


// 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'

)


const Label = forwardRef<HTMLLabelElement, React.LabelHTMLAttributes<HTMLLabelElement>>(

({ className, ...props }, ref) => (

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

)

)

Label.displayName = 'Label'


// Usage with React Hook Form

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

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

import * as 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


`typescript

// 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>

`


Pattern 5: Animation Utilities


`typescript

// lib/animations.ts - Tailwind CSS Animate utilities

import { cn } from './utils'


export const fadeIn = 'animate-in fade-in duration-300'

export const fadeOut = 'animate-out fade-out duration-300'

export const slideInFromTop = 'animate-in slide-in-from-top duration-300'

export const slideInFromBottom = 'animate-in slide-in-from-bottom duration-300'

export const slideInFromLeft = 'animate-in slide-in-from-left duration-300'

export const slideInFromRight = 'animate-in slide-in-from-right duration-300'

export const zoomIn = 'animate-in zoom-in-95 duration-300'

export const zoomOut = 'animate-out zoom-out-95 duration-300'


// Compound animations

export const modalEnter = cn(fadeIn, zoomIn, 'duration-200')

export const modalExit = cn(fadeOut, zoomOut, 'duration-200')

export const dropdownEnter = cn(fadeIn, slideInFromTop, 'duration-150')

export const dropdownExit = cn(fadeOut, 'slide-out-to-top', 'duration-150')


// components/ui/dialog.tsx

import * as DialogPrimitive from '@radix-ui/react-dialog'


const DialogOverlay = forwardRef<

React.ElementRef<typeof DialogPrimitive.Overlay>,

React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>

>(({ className, ...props }, ref) => (

<DialogPrimitive.Overlay

ref={ref}

className={cn(

'fixed inset-0 z-50 bg-black/80',

'data-[state=open]:animate-in data-[state=closed]:animate-out',

'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',

className

)}

{...props}

/>

))


const DialogContent = forwardRef<

React.ElementRef<typeof DialogPrimitive.Content>,

React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>

>(({ className, children, ...props }, ref) => (

<DialogPortal>

<DialogOverlay />

<DialogPrimitive.Content

ref={ref}

className={cn(

'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg',

'data-[state=open]:animate-in data-[state=closed]:animate-out',

'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',

'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',

'data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]',

'data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]',

'sm:rounded-lg',

className

)}

{...props}

>

{children}

</DialogPrimitive.Content>

</DialogPortal>

))

`


Pattern 6: Dark Mode Implementation


`typescript

// providers/ThemeProvider.tsx

'use client'


import { createContext, useContext, useEffect, useState } from 'react'


type Theme = 'dark' | 'light' | 'system'


interface ThemeProviderProps {

children: React.ReactNode

defaultTheme?: Theme

storageKey?: string

}


interface ThemeContextType {

theme: Theme

setTheme: (theme: Theme) => void

resolvedTheme: 'dark' | 'light'

}


const ThemeContext = createContext<ThemeContextType | undefined>(undefined)


export function ThemeProvider({

children,

defaultTheme = 'system',

storageKey = 'theme',

}: ThemeProviderProps) {

const [theme, setTheme] = useState<Theme>(defaultTheme)

const [resolvedTheme, setResolvedTheme] = useState<'dark' | 'light'>('light')


useEffect(() => {

const stored = localStorage.getItem(storageKey) as Theme | null

if (stored) setTheme(stored)

}, [storageKey])


useEffect(() => {

const root = window.document.documentElement

root.classList.remove('light', 'dark')


let resolved: 'dark' | 'light'


if (theme === 'system') {

resolved = window.matchMedia('(prefers-color-scheme: dark)').matches

? 'dark'

: 'light'

} else {

resolved = theme

}


root.classList.add(resolved)

setResolvedTheme(resolved)

}, [theme])


const value = {

theme,

setTheme: (newTheme: Theme) => {

localStorage.setItem(storageKey, newTheme)

setTheme(newTheme)

},

resolvedTheme,

}


return (

<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>

)

}


export const useTheme = () => {

const context = useContext(ThemeContext)

if (!context) throw new Error('useTheme must be used within ThemeProvider')

return context

}


// components/ThemeToggle.tsx

import { Moon, Sun } from 'lucide-react'

import { useTheme } from '@/providers/ThemeProvider'


export function ThemeToggle() {

const { resolvedTheme, setTheme } = useTheme()


return (

<Button

variant="ghost"

size="icon"

onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}

>

<Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />

<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />

<span className="sr-only">Toggle theme</span>

</Button>

)

}

`


Utility Functions


`typescript

// 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'

`


Best Practices


Do's

  • Use CSS variables - Enable runtime theming
  • Compose with CVA - Type-safe variants
  • Use semantic colors - primary not blue-500`
  • Forward refs - Enable composition
  • Add accessibility - ARIA attributes, focus states

Don'ts

  • Don't use arbitrary values - Extend theme instead
  • Don't nest @apply - Hurts readability
  • Don't skip focus states - Keyboard users need them
  • Don't hardcode colors - Use semantic tokens
  • Don't forget dark mode - Test both themes

Resources


  • [Tailwind CSS Documentation](https://tailwindcss.com/docs)
  • [CVA Documentation](https://cva.style/docs)
  • [shadcn/ui](https://ui.shadcn.com/)
  • [Radix Primitives](https://www.radix-ui.com/primitives)

Informacion

Estado
Activo
Origen upstream
Usos 3
Ultimo uso 23/01 16:57
Actualizado 09/01/2026 02:14

Archivo origen

Z:\Repositorios\core\upstream\plugins\frontend-mobile-development\skills\tailwind-design-system\SKILL.md