diff --git a/plugins/frontend-mobile-development/skills/tailwind-design-system/SKILL.md b/plugins/frontend-mobile-development/skills/tailwind-design-system/SKILL.md index baed911..0a8f806 100644 --- a/plugins/frontend-mobile-development/skills/tailwind-design-system/SKILL.md +++ b/plugins/frontend-mobile-development/skills/tailwind-design-system/SKILL.md @@ -1,20 +1,165 @@ --- name: tailwind-design-system -description: 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. +description: Build scalable design systems with Tailwind CSS v4, design tokens, component libraries, and responsive patterns. Use when creating component libraries, implementing design systems, or standardizing UI patterns. --- -# Tailwind Design System +# Tailwind Design System (v4) -Build production-ready design systems with Tailwind CSS, including design tokens, component variants, responsive patterns, and accessibility. +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](https://tailwindcss.com/docs/upgrade-guide). ## When to Use This Skill -- Creating a component library with Tailwind -- Implementing design tokens and theming +- 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 to or extending Tailwind CSS -- Setting up dark mode and color schemes +- 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 + +```css +/* 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 @@ -26,7 +171,7 @@ Brand Tokens (abstract) └── Component Tokens (specific) Example: - blue-500 → primary → button-bg + oklch(45% 0.2 260) → --color-primary → bg-primary ``` ### 2. Component Architecture @@ -35,120 +180,25 @@ Example: 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 { Slot } from '@radix-ui/react-slot' 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', + // 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-input bg-background hover:bg-accent hover:text-accent-foreground', + 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', @@ -157,7 +207,7 @@ const buttonVariants = cva( 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', + icon: 'size-10', }, }, defaultVariants: { @@ -173,21 +223,24 @@ export interface ButtonProps asChild?: boolean } -const Button = forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : 'button' - return ( - - ) - } -) -Button.displayName = 'Button' - -export { Button, buttonVariants } +// React 19: No forwardRef needed +export function Button({ + className, + variant, + size, + asChild = false, + ref, + ...props +}: ButtonProps & { ref?: React.Ref }) { + const Comp = asChild ? Slot : 'button' + return ( + + ) +} // Usage @@ -195,79 +248,95 @@ export { Button, buttonVariants } ``` -### Pattern 2: Compound Components +### Pattern 2: Compound Components (React 19) ```typescript // components/ui/card.tsx import { cn } from '@/lib/utils' -import { forwardRef } from 'react' -const Card = forwardRef>( - ({ className, ...props }, ref) => ( +// React 19: ref is a regular prop, no forwardRef +export function Card({ + className, + ref, + ...props +}: React.HTMLAttributes & { ref?: React.Ref }) { + return (
) -) -Card.displayName = 'Card' +} -const CardHeader = forwardRef>( - ({ className, ...props }, ref) => ( +export function CardHeader({ + className, + ref, + ...props +}: React.HTMLAttributes & { ref?: React.Ref }) { + return (
) -) -CardHeader.displayName = 'CardHeader' +} -const CardTitle = forwardRef>( - ({ className, ...props }, ref) => ( +export function CardTitle({ + className, + ref, + ...props +}: React.HTMLAttributes & { ref?: React.Ref }) { + return (

) -) -CardTitle.displayName = 'CardTitle' +} -const CardDescription = forwardRef>( - ({ className, ...props }, ref) => ( +export function CardDescription({ + className, + ref, + ...props +}: React.HTMLAttributes & { ref?: React.Ref }) { + return (

) -) -CardDescription.displayName = 'CardDescription' +} -const CardContent = forwardRef>( - ({ className, ...props }, ref) => ( +export function CardContent({ + className, + ref, + ...props +}: React.HTMLAttributes & { ref?: React.Ref }) { + return (

) -) -CardContent.displayName = 'CardContent' +} -const CardFooter = forwardRef>( - ({ className, ...props }, ref) => ( +export function CardFooter({ + className, + ref, + ...props +}: React.HTMLAttributes & { ref?: React.Ref }) { + return (
) -) -CardFooter.displayName = 'CardFooter' - -export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } +} // Usage @@ -288,43 +357,40 @@ export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } ```typescript // components/ui/input.tsx -import { forwardRef } from 'react' import { cn } from '@/lib/utils' export interface InputProps extends React.InputHTMLAttributes { error?: string + ref?: React.Ref } -const Input = forwardRef( - ({ className, type, error, ...props }, ref) => { - return ( -
- - {error && ( - +export function Input({ className, type, error, ref, ...props }: InputProps) { + return ( +
+ - ) - } -) -Input.displayName = 'Input' + ref={ref} + aria-invalid={!!error} + aria-describedby={error ? `${props.id}-error` : undefined} + {...props} + /> + {error && ( + + )} +
+ ) +} // components/ui/label.tsx import { cva, type VariantProps } from 'class-variance-authority' @@ -333,17 +399,20 @@ const labelVariants = cva( 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70' ) -const Label = forwardRef>( - ({ className, ...props }, ref) => ( +export function Label({ + className, + ref, + ...props +}: React.LabelHTMLAttributes & { ref?: React.Ref }) { + return (