Files
2025-11-29 18:00:50 +08:00

978 lines
26 KiB
Markdown

---
name: web-artifacts-builder
description: Modern web development patterns using React + Tailwind CSS + shadcn/ui for building production-quality, accessible, and performant web applications
version: 1.0.0
---
## Overview
This skill provides comprehensive patterns and best practices for building modern web applications using the React + Tailwind CSS + shadcn/ui stack. It emphasizes component-driven development, type safety with TypeScript, accessibility, and performance optimization.
## Stack Overview
**Core Technologies**:
- **React 18+**: Component-based UI with hooks and concurrent features
- **TypeScript**: Type-safe development with excellent IDE support
- **Tailwind CSS**: Utility-first CSS framework for rapid styling
- **shadcn/ui**: High-quality, accessible React components built on Radix UI
- **Vite**: Fast build tool with HMR and optimized production builds
**Why This Stack**:
- Type safety reduces bugs and improves maintainability
- Tailwind enables rapid UI development without context switching
- shadcn/ui provides accessible, customizable components out of the box
- Vite offers excellent developer experience and fast builds
- Modern ecosystem with active community support
**Design Considerations**:
Based on research from ["Improving frontend design through Skills"](https://claude.com/blog/improving-frontend-design-through-skills):
- **Avoid Distributional Defaults**: Don't use Inter/Roboto/Open Sans, purple gradients, or plain backgrounds
- **Distinctive Typography**: Use high-contrast font pairings with extreme weight variations (100-200 or 800-900)
- **Intentional Colors**: Move beyond generic color schemes with thoughtful palettes
- **High-Impact Motion**: One well-orchestrated page load beats a dozen random animations
- **Motion Library**: Use Framer Motion for complex animation choreography in React
- See `frontend-aesthetics` skill for comprehensive design guidance
## Project Structure
### Recommended Directory Layout
```
project-root/
├── src/
│ ├── components/
│ │ ├── ui/ # shadcn/ui components
│ │ │ ├── button.tsx
│ │ │ ├── card.tsx
│ │ │ ├── dialog.tsx
│ │ │ └── ...
│ │ ├── features/ # Feature-specific components
│ │ │ ├── auth/
│ │ │ ├── dashboard/
│ │ │ └── ...
│ │ └── layout/ # Layout components
│ │ ├── Header.tsx
│ │ ├── Sidebar.tsx
│ │ └── Footer.tsx
│ ├── lib/
│ │ ├── utils.ts # Utility functions (cn, etc.)
│ │ ├── api.ts # API client
│ │ └── hooks/ # Custom React hooks
│ ├── pages/ # Page components (if using routing)
│ ├── styles/
│ │ └── globals.css # Global styles and Tailwind imports
│ ├── types/ # TypeScript type definitions
│ ├── App.tsx # Main app component
│ └── main.tsx # Entry point
├── public/ # Static assets
├── index.html
├── package.json
├── tsconfig.json
├── tailwind.config.js
├── vite.config.ts
└── components.json # shadcn/ui configuration
```
## Component Patterns
### 1. Button Component (shadcn/ui style)
```typescript
// src/components/ui/button.tsx
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.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**:
```typescript
import { Button } from "@/components/ui/button"
// Different variants
<Button>Default</Button>
<Button variant="destructive">Delete</Button>
<Button variant="outline">Cancel</Button>
<Button variant="ghost">Ghost</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
<Button disabled>Disabled</Button>
```
### 2. Card Component Pattern
```typescript
// src/components/ui/card.tsx
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
)
)
Card.displayName = "Card"
const CardHeader = React.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 = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
)
)
CardTitle.displayName = "CardTitle"
const CardDescription = React.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 = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
)
)
CardContent.displayName = "CardContent"
const CardFooter = React.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, CardFooter, CardTitle, CardDescription, CardContent }
```
**Usage**:
```typescript
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card description goes here</CardDescription>
</CardHeader>
<CardContent>
<p>Card content</p>
</CardContent>
<CardFooter>
<Button>Action</Button>
</CardFooter>
</Card>
```
### 3. Dialog/Modal Component
```typescript
// src/components/ui/dialog.tsx (simplified shadcn/ui pattern)
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.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}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.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 duration-200 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.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
export { Dialog, DialogPortal, DialogOverlay, DialogTrigger, DialogClose, DialogContent }
```
## Tailwind CSS Configuration
### Enhanced tailwind.config.js
```javascript
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
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))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}
```
### CSS Variables (globals.css)
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--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%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--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%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 48%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
```
## Utility Functions
### cn() - Class Name Utility
```typescript
// src/lib/utils.ts
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
```
**Usage**: Merge Tailwind classes without conflicts
```typescript
cn("px-2 py-1", "px-3") // Result: "py-1 px-3" (px-3 overrides px-2)
```
## TypeScript Best Practices
### Component Props Typing
```typescript
// Extend HTML attributes
interface CustomButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'outline'
size?: 'sm' | 'md' | 'lg'
loading?: boolean
}
// Or use type
type CustomButtonProps = {
variant?: 'primary' | 'secondary' | 'outline'
size?: 'sm' | 'md' | 'lg'
loading?: boolean
} & React.ButtonHTMLAttributes<HTMLButtonElement>
```
### API Response Typing
```typescript
// Define API response types
interface User {
id: string
name: string
email: string
role: 'admin' | 'user'
}
interface ApiResponse<T> {
data: T
message: string
status: 'success' | 'error'
}
// Usage
const fetchUser = async (id: string): Promise<ApiResponse<User>> => {
const response = await fetch(`/api/users/${id}`)
return response.json()
}
```
## Accessibility Patterns
### Keyboard Navigation
```typescript
// Focus management
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
onClose()
}
if (e.key === 'Enter' || e.key === ' ') {
onSelect()
e.preventDefault()
}
}
// Trap focus in modal
import { useFocusTrap } from '@/lib/hooks/use-focus-trap'
function Modal({ children }: { children: React.ReactNode }) {
const modalRef = useFocusTrap()
return (
<div ref={modalRef} role="dialog" aria-modal="true">
{children}
</div>
)
}
```
### ARIA Labels
```typescript
// Screen reader support
<button
aria-label="Close dialog"
aria-pressed={isPressed}
aria-expanded={isExpanded}
>
<X className="h-4 w-4" />
</button>
// Form accessibility
<div>
<label htmlFor="email" className="sr-only">Email</label>
<input
id="email"
type="email"
aria-required="true"
aria-invalid={hasError}
aria-describedby={hasError ? "email-error" : undefined}
/>
{hasError && <p id="email-error" role="alert">Invalid email</p>}
</div>
```
## Performance Optimization
### Code Splitting
```typescript
// Lazy load routes
import { lazy, Suspense } from 'react'
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Settings = lazy(() => import('./pages/Settings'))
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
)
}
```
### Memoization
```typescript
import { memo, useMemo, useCallback } from 'react'
// Memo component
const ExpensiveComponent = memo(({ data }: { data: Data[] }) => {
return <div>{/* Render data */}</div>
})
// Memo computation
function Component({ items }: { items: Item[] }) {
const sortedItems = useMemo(
() => items.sort((a, b) => a.name.localeCompare(b.name)),
[items]
)
const handleClick = useCallback(() => {
console.log('Clicked')
}, [])
return <List items={sortedItems} onClick={handleClick} />
}
```
## Animation with Framer Motion
### Installation
```bash
npm install framer-motion
```
### Core Principles
**High-Impact Moments Over Random Motion**:
- One well-orchestrated page load with staggered reveals > dozen random micro-animations
- Focus on: page load, route transitions, major state changes
- Use CSS for simple transitions, Framer Motion for complex choreography
- Always respect `prefers-reduced-motion`
### Page Transitions
```typescript
// app/layout.tsx or page wrapper
import { motion, AnimatePresence } from 'framer-motion'
export default function PageTransition({ children }: { children: React.ReactNode }) {
return (
<AnimatePresence mode="wait">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{
duration: 0.5,
ease: [0.22, 1, 0.36, 1] // Custom ease (easeOutExpo)
}}
>
{children}
</motion.div>
</AnimatePresence>
)
}
```
### Staggered List Animation
```typescript
import { motion } from 'framer-motion'
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.1 // Delay between each child animation
}
}
}
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 }
}
export function StaggeredList({ items }: { items: string[] }) {
return (
<motion.ul
variants={container}
initial="hidden"
animate="show"
className="space-y-2"
>
{items.map((item, i) => (
<motion.li
key={i}
variants={item}
className="p-4 bg-card rounded-lg"
>
{item}
</motion.li>
))}
</motion.ul>
)
}
```
### Card Hover Effects
```typescript
import { motion } from 'framer-motion'
export function AnimatedCard({ children }: { children: React.ReactNode }) {
return (
<motion.div
whileHover={{ scale: 1.02, y: -4 }}
whileTap={{ scale: 0.98 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
className="rounded-xl border bg-card p-6 shadow-sm cursor-pointer"
>
{children}
</motion.div>
)
}
```
### Layout Animations (Shared Layout)
```typescript
import { motion, LayoutGroup } from 'framer-motion'
export function TabsWithAnimation({ tabs }: { tabs: Tab[] }) {
const [activeTab, setActiveTab] = useState(0)
return (
<LayoutGroup>
<div className="flex gap-2">
{tabs.map((tab, i) => (
<motion.button
key={i}
onClick={() => setActiveTab(i)}
className="relative px-4 py-2 rounded-md"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
{tab.label}
{activeTab === i && (
<motion.div
layoutId="activeTab"
className="absolute inset-0 bg-primary rounded-md"
style={{ zIndex: -1 }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
/>
)}
</motion.button>
))}
</div>
</LayoutGroup>
)
}
```
### Scroll-Based Animations
```typescript
import { motion, useScroll, useTransform } from 'framer-motion'
import { useRef } from 'react'
export function ParallaxSection() {
const ref = useRef(null)
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"]
})
const y = useTransform(scrollYProgress, [0, 1], [0, -100])
const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [0, 1, 0])
return (
<motion.section
ref={ref}
style={{ y, opacity }}
className="min-h-screen flex items-center justify-center"
>
<h2 className="text-4xl font-bold">Parallax Content</h2>
</motion.section>
)
}
```
### Modal / Dialog Animations
```typescript
import { motion, AnimatePresence } from 'framer-motion'
export function AnimatedDialog({
open,
onClose,
children
}: {
open: boolean
onClose: () => void
children: React.ReactNode
}) {
return (
<AnimatePresence>
{open && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="fixed inset-0 bg-black/80 z-50"
/>
{/* Dialog content */}
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-50 bg-background p-6 rounded-lg shadow-lg max-w-md w-full"
>
{children}
</motion.div>
</>
)}
</AnimatePresence>
)
}
```
### Gesture Animations
```typescript
import { motion, useDragControls } from 'framer-motion'
export function DraggableCard() {
const controls = useDragControls()
return (
<motion.div
drag
dragControls={controls}
dragConstraints={{ left: 0, right: 300, top: 0, bottom: 300 }}
dragElastic={0.1}
whileDrag={{ scale: 1.05, cursor: "grabbing" }}
className="w-32 h-32 bg-primary rounded-lg cursor-grab"
/>
)
}
// Swipe to dismiss
export function SwipeableBanner({ onDismiss }: { onDismiss: () => void }) {
return (
<motion.div
drag="x"
dragConstraints={{ left: 0, right: 0 }}
onDragEnd={(e, { offset, velocity }) => {
if (Math.abs(offset.x) > 100 || Math.abs(velocity.x) > 500) {
onDismiss()
}
}}
className="p-4 bg-card border rounded-lg"
>
Swipe to dismiss
</motion.div>
)
}
```
### Loading States with Animation
```typescript
import { motion } from 'framer-motion'
export function LoadingSpinner() {
return (
<motion.div
animate={{ rotate: 360 }}
transition={{ repeat: Infinity, duration: 1, ease: "linear" }}
className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full"
/>
)
}
export function PulseLoader() {
return (
<div className="flex gap-2">
{[0, 1, 2].map((i) => (
<motion.div
key={i}
animate={{
scale: [1, 1.2, 1],
opacity: [0.5, 1, 0.5]
}}
transition={{
duration: 1,
repeat: Infinity,
delay: i * 0.2
}}
className="w-3 h-3 bg-primary rounded-full"
/>
))}
</div>
)
}
```
### Respecting Reduced Motion
```typescript
import { motion, useReducedMotion } from 'framer-motion'
export function AccessibleAnimation({ children }: { children: React.ReactNode }) {
const shouldReduceMotion = useReducedMotion()
return (
<motion.div
initial={shouldReduceMotion ? false : { opacity: 0, y: 20 }}
animate={shouldReduceMotion ? false : { opacity: 1, y: 0 }}
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.5 }}
>
{children}
</motion.div>
)
}
```
### Performance Best Practices
**What to Animate (GPU-Accelerated)**:
- `opacity`
- `transform` (translate, scale, rotate)
- `filter` (blur, brightness)
**What to Avoid Animating**:
- `width`, `height` (causes layout thrashing)
- `top`, `left`, `margin`, `padding` (use `transform: translate` instead)
- `color`, `background-color` (expensive, use sparingly)
**Optimization Tips**:
```typescript
// Use will-change for smoother animations
<motion.div style={{ willChange: "transform" }}>
// Lazy load motion components
import { motion, LazyMotion, domAnimation } from "framer-motion"
<LazyMotion features={domAnimation}>
<motion.div />
</LazyMotion>
// Reduce animation complexity on low-end devices
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
```
### When to Use Framer Motion vs CSS
**Use CSS Animations**:
- Simple hover effects
- Basic transitions
- Static keyframe animations
- Better performance for simple cases
**Use Framer Motion**:
- Complex orchestration (staggered lists, sequences)
- Gesture-based interactions (drag, swipe)
- Layout animations (morphing between states)
- Scroll-based animations
- Dynamic animations based on state
- Need for easier control and declarative API
## When to Apply
Use this skill when:
- Building modern React applications with TypeScript
- Implementing accessible, production-quality UI components
- Using Tailwind CSS for styling
- Integrating shadcn/ui component library
- Setting up new projects with Vite + React
- Creating reusable component libraries
- Implementing design systems with consistent patterns
- Building dashboards, admin panels, or web applications
This stack provides excellent developer experience, type safety, accessibility, and performance for modern web development.