
Nextjs Shadcn Builder
- 243 installs
- 51 repo stars
- Updated November 25, 2025
- ovachiever/droid-tings
nextjs-shadcn-builder is an agent skill that builds new Next.js 15+ applications or migrates existing frontends to Next.js with shadcn/ui using a 5-phase analysis and conversion workflow.
About
nextjs-shadcn-builder is an agent skill for bootstrapping production Next.js 15+ App Router applications and systematically migrating existing frontends to Next.js with shadcn/ui. It enforces shadcn design principles: CSS variables for theming, standard shadcn components only, no hardcoded colors or spacing, mobile-first layouts, and WCAG 2.1 AA accessibility targets. The workflow spans five phases: codebase analysis, migration planning, Next.js and shadcn setup, batched component conversion, and verification cleanup. Automated scripts detect framework, inventory components, flag hardcoded values, and produce analysis reports. Developers reach for this skill when creating new Next.js apps, adopting shadcn/ui, or converting legacy React, Vue, Angular, or vanilla JS frontends. It integrates shadcn MCP documentation access and includes seven conversion steps per component batch.
- Agent skill
- Developer productivity
- Workflow automation
Nextjs Shadcn Builder by the numbers
- 243 all-time installs (skills.sh)
- Ranked #2,559 of 16,556 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 27, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ovachiever/droid-tings --skill nextjs-shadcn-builderAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 243 |
|---|---|
| repo stars | ★ 51 |
| Last updated | November 25, 2025 |
| Repository | ovachiever/droid-tings ↗ |
How do you migrate a frontend to Next.js shadcn?
Extends Claude Code with specialized agent capabilities for developer workflows.
Who is it for?
Frontend developers bootstrapping Next.js apps or migrating multi-framework legacy UIs to a standardized shadcn/ui design system.
Skip if: Backend-only API work, mobile native UI projects, or teams not adopting Next.js App Router and shadcn/ui conventions.
When should I use this skill?
The user wants to create a Next.js app, migrate a frontend to shadcn/ui, or standardize components with CSS variable theming.
What you get
Next.js 15+ project with shadcn/ui, CSS variable design tokens, converted components, and a migration analysis report.
- Next.js application scaffold
- migration analysis report
- shadcn component library setup
By the numbers
- Follows a 5-phase migration and build workflow
- Runs 7 conversion steps per component batch
- Targets Next.js 15+ App Router with shadcn/ui
Files
Next.js + shadcn/ui Builder & Migration Tool
Build production-grade Next.js applications or systematically migrate existing frontends to Next.js + shadcn/ui following strict design principles and best practices.
Overview
This skill handles two primary workflows:
1. Creating New Next.js Applications - Initialize projects with Next.js 15+ (App Router), shadcn/ui, and proper design system setup 2. Migrating Existing Frontends - Analyze any frontend codebase (React, Vue, Angular, vanilla JS) and systematically convert to Next.js + shadcn/ui
Core Philosophy: 100% adherence to shadcn/ui design principles:
- CSS variables for all theming (colors, spacing, typography)
- Standard shadcn/ui components only (no custom UI components)
- No hardcoded values (colors, spacing, fonts)
- Consistent design tokens across the application
- Mobile-first responsive design for all devices (phone, tablet, desktop)
- WCAG 2.1 Level AA accessibility compliance
- Best practices from https://ui.shadcn.com
Workflow Decision Tree
User Request
├─ Creating New Next.js App
│ └─ Follow "Creating New Application" workflow (Phase 3 onwards)
│
└─ Migrating Existing Codebase
├─ Phase 1: Codebase Analysis
├─ Phase 2: Migration Planning
├─ Phase 3: Next.js + shadcn Setup
├─ Phase 4: Systematic Conversion
└─ Phase 5: Verification & Cleanup---
High-Level Workflow for Migration
Phase 1: Codebase Analysis
Automated analysis of existing frontend to understand scope and complexity.
Steps: 1. Framework and version detection 2. Component inventory and categorization 3. Hardcoded value detection (colors, spacing, custom components) 4. Styling approach analysis (CSS, SCSS, CSS-in-JS, Tailwind, etc.) 5. State management and routing pattern identification 6. Generate comprehensive analysis report
Deliverables:
- Framework analysis report
- Component inventory (JSON)
- Hardcoded values report
- Complexity assessment
Phase 2: Migration Planning
Create systematic conversion plan with prioritized batches.
Steps: 1. Map existing components to shadcn/ui equivalents 2. Identify components requiring custom development 3. Organize conversion into batches (5-10 components per batch) 4. Assess risk and complexity per batch 5. Create detailed migration plan
Deliverables:
- Component mapping table
- Batched conversion plan
- Risk assessment
- Estimated complexity per component
Phase 3: Next.js + shadcn Setup
Initialize Next.js infrastructure alongside or replacing existing codebase.
Steps: 1. Check/install shadcn MCP server for documentation access 2. Initialize Next.js 15+ with App Router and TypeScript 3. Install and configure Tailwind CSS 4. Run shadcn/ui initialization 5. Set up CSS variables and design tokens 6. Configure path aliases (@/) 7. Install essential shadcn components 8. Create design system documentation
Deliverables:
- Configured Next.js project
- Design token system (CSS variables)
- Component library setup
- Path aliases configured
Phase 4: Systematic Conversion
Convert components batch by batch with testing after each batch.
Steps: 1. Batch 1: Layout & Structure (Header, Footer, Layout wrappers) 2. Batch 2: Simple UI Components (Buttons, Cards, Badges, Alerts) 3. Batch 3: Form Components (Inputs, Selects, Checkboxes, Forms) 4. Batch 4: Complex Components (Tables, Dialogs, Command Menus, Data visualizations) 5. Batch 5: Styling Standardization (Remove hardcoded values, apply CSS variables) 6. Batch 6: Pages & Routes (Convert pages, set up Next.js routing)
Per Batch Workflow: 1. Select 5-10 related components 2. Use MCP to find appropriate shadcn components 3. Convert components following shadcn patterns 4. Replace hardcoded values with CSS variables 5. Test functionality 6. Verify visual consistency 7. Move to next batch
Deliverables:
- Migrated components (batch by batch)
- Updated styling with CSS variables
- Next.js App Router pages
- Passing tests per batch
Phase 5: Verification & Cleanup
Final testing, optimization, and old code removal.
Steps: 1. Run full test suite 2. Visual regression testing 3. Responsive design testing (mobile, tablet, desktop) 4. Performance audit 5. Accessibility audit (WCAG 2.1 Level AA compliance) 6. Remove old framework code 7. Documentation updates 8. Generate completion report
Deliverables:
- Test results
- Responsive design verification report
- Performance metrics
- Accessibility audit report (WCAG 2.1 AA)
- Clean codebase
- Migration summary
---
Phase 1: Codebase Analysis (Detailed Instructions)
1.1 Framework Detection
Run the automated analysis script:
python ./scripts/analyze-codebase.py /path/to/existing/codebaseThis script will:
- Detect framework (React, Vue, Angular, Svelte, vanilla JS, etc.)
- Identify framework version
- Detect build tool (Vite, Webpack, Parcel, etc.)
- Find package.json dependencies
- Map directory structure
Output: codebase-analysis.json with framework metadata
1.2 Component Inventory
The analysis script automatically generates a component inventory including:
- Component name and file path
- Component type (functional, class, Vue SFC, etc.)
- Props/inputs
- State usage
- Child components
- External dependencies
Output: component-inventory.json
Example structure:
{
"components": [
{
"name": "UserCard",
"path": "src/components/UserCard.tsx",
"type": "functional",
"complexity": "simple",
"shadcn_equivalent": "Card",
"hardcoded_values": ["#3b82f6", "16px padding"],
"dependencies": ["react", "styled-components"]
}
]
}1.3 Hardcoded Value Detection
Run the detection script:
bash ./scripts/detect-hardcoded-values.sh /path/to/existing/codebaseThis script detects:
- Hardcoded colors:
#hex,rgb(),rgba(),hsl(), color names - Inline spacing:
margin: 20px,padding: 1rem - Custom font declarations: non-standard fonts
- Magic numbers: arbitrary values in components
- Inline styles:
style={{...}} - Non-standard patterns: CSS-in-JS, styled-components that should be Tailwind
Output: hardcoded-values-report.md with violations grouped by category
1.4 Generate Analysis Report
Run the report generator:
python ./scripts/generate-migration-report.pyThis combines all analysis data into a comprehensive markdown report:
Output: migration-analysis-report.md
# Frontend Migration Analysis Report
## Executive Summary
[One-paragraph overview: framework, size, complexity]
## Current State Analysis
- **Framework**: React 18.2.0
- **Build Tool**: Vite 4.3.0
- **Component Count**: 47 components
- **Styling**: styled-components + custom CSS
- **State Management**: Redux Toolkit
- **Routing**: React Router v6
## Hardcoded Values Detected
- Colors: 142 instances across 34 files
- Spacing: 89 instances across 28 files
- Custom fonts: 3 non-standard fonts
- Inline styles: 67 instances
## Component Categorization
- **Simple (shadcn mapping exists)**: 28 components
- **Moderate (requires adaptation)**: 13 components
- **Complex (custom development needed)**: 6 components
## Recommended Migration Plan
1. Phase 3: Setup Next.js + shadcn infrastructure
2. Phase 4.1: Convert layout components (Header, Footer, Layout)
3. Phase 4.2: Convert simple UI (Button, Card, Badge → shadcn equivalents)
4. Phase 4.3: Convert forms (Input, Select → shadcn/ui Form components)
5. Phase 4.4: Convert complex components (DataTable, Charts)
6. Phase 4.5: Styling standardization (CSS variables)
7. Phase 4.6: Pages and routing
8. Phase 5: Verification and cleanup
## Estimated Effort
- **Total Components**: 47
- **Batches**: 9-10 batches
- **Complexity**: Moderate---
Phase 2: Migration Planning (Detailed Instructions)
2.1 Component Mapping Strategy
Review the component-inventory.json and create a mapping table using the shadcn component reference.
VERY IMPORTANT: Use MCP to discover shadcn components
Before mapping, check if shadcn MCP server is available:
# Check if MCP server is available
# Try accessing https://ui.shadcn.com/docs/mcpIf MCP is not available, install it:
npx shadcn@latest mcp init --client claudeUse MCP to query available components:
- "What shadcn components are available for buttons?"
- "Show me form components in shadcn"
- "What's the shadcn equivalent of a modal/dialog?"
- "Available data display components in shadcn"
Component Mapping Table Template:
| Existing Component | shadcn Equivalent | Complexity | Priority | Notes |
|---|---|---|---|---|
| CustomButton | Button | Low | 1 | Props mostly compatible |
| Modal | Dialog | Medium | 2 | Different API, uses Radix |
| DataTable | Table + DataTable | High | 3 | Requires custom hooks |
| Dropdown | DropdownMenu | Low | 1 | Direct mapping |
| DatePicker | Calendar + Popover | Medium | 2 | Composition pattern |
Load framework-specific migration guide:
- For React: Read
./references/react-to-nextjs.md - For Vue: Read
./references/vue-to-nextjs.md - For Angular: Read
./references/angular-to-nextjs.md - For styling: Read
./references/styling-migration.md
2.2 Batch Organization
Organize components into batches following these principles:
Batching Strategy: 1. Group by type (layout, forms, data display, navigation) 2. Simple to complex (start with easy wins) 3. Dependency order (convert dependencies first) 4. Batch size: 5-10 components per batch
Example Batch Plan:
Batch 1: Layout & Structure (Priority: Critical)
- Header
- Footer
- MainLayout
- Container
- Sidebar
Batch 2: Simple UI Components (Priority: High)
- Button → shadcn Button
- Card → shadcn Card
- Badge → shadcn Badge
- Alert → shadcn Alert
- Avatar → shadcn Avatar
Batch 3: Form Components (Priority: High)
- Input → shadcn Input
- Select → shadcn Select
- Checkbox → shadcn Checkbox
- RadioGroup → shadcn RadioGroup
- Form validation → shadcn Form + react-hook-form
Batch 4: Navigation (Priority: Medium)
- NavBar → shadcn NavigationMenu
- Breadcrumbs → shadcn Breadcrumb
- Tabs → shadcn Tabs
- Pagination → shadcn Pagination
Batch 5: Data Display (Priority: Medium)
- Table → shadcn Table
- DataGrid → shadcn DataTable (with sorting, filtering)
- List → shadcn custom composition
- Accordion → shadcn Accordion
Batch 6: Overlays & Modals (Priority: Medium)
- Modal → shadcn Dialog
- Tooltip → shadcn Tooltip
- Popover → shadcn Popover
- DropdownMenu → shadcn DropdownMenu
Batch 7: Complex Components (Priority: Low)
- Charts → shadcn Charts (Recharts integration)
- Calendar/DatePicker → shadcn Calendar
- CommandPalette → shadcn Command
- DataVisualization → Custom with shadcn primitives
Batch 8: Styling Standardization (Priority: Critical)
- Extract all hardcoded colors → CSS variables
- Convert spacing to Tailwind classes
- Standardize typography
- Apply theme system consistently
Batch 9: Pages & Routing (Priority: Critical)
- Convert pages to Next.js App Router
- Set up layouts with Next.js layout.tsx
- Implement routing patterns
- Add loading and error states
2.3 Risk Assessment
For each batch, identify risks:
- API Differences: Components with significantly different APIs
- Missing Features: Features in old components not in shadcn
- State Management: Complex state that needs refactoring
- Dependencies: External libraries that need replacement
- Custom Logic: Business logic tightly coupled to UI
Risk Mitigation:
- Document API differences before conversion
- Create adapter/wrapper components when needed
- Write tests before migration
- Keep old components temporarily during transition
2.4 Create Detailed Migration Plan
Generate a detailed plan document: migration-plan.md
# Next.js + shadcn Migration Plan
## Project: [Project Name]
## Date: [Current Date]
## Estimated Timeline: [X batches]
## Migration Strategy
### Approach
- Incremental migration with parallel running old and new code
- Batch-based conversion (5-10 components per batch)
- Test after each batch before proceeding
- Feature flag new components during transition
### Success Criteria
- All components use shadcn/ui or shadcn patterns
- Zero hardcoded colors/spacing (CSS variables only)
- 100% TypeScript coverage
- Passing test suite
- Lighthouse score >= 90
- No accessibility violations
## Detailed Batch Plan
[Include all batches from 2.2 with specific components listed]
## Timeline
Batch 1: Layout & Structure (Days 1-2)
Batch 2: Simple UI (Days 3-4)
[etc.]
## Notes and Considerations
[Any special requirements, blockers, or dependencies]---
Phase 3: Next.js + shadcn Setup (Detailed Instructions)
3.1 Check/Install shadcn MCP Server
CRITICAL: Always use MCP for shadcn component discovery
1. Check if MCP server is accessible:
- Try to access documentation at
https://ui.shadcn.com/docs/mcp - Check if you can query shadcn components via MCP
2. If not available, install MCP server:
npx shadcn@latest mcp init --client claudeThis enables:
- Real-time shadcn documentation access
- Component discovery and search
- Block and chart template discovery
- Theme and design token reference
Using MCP during development:
- "What components are available for [use case]?"
- "Show me the props for shadcn Button"
- "Available chart types in shadcn"
- "How to use shadcn Form with validation"
3.2 Initialize Next.js Project
Run the initialization script:
bash ./scripts/init-nextjs-shadcn.sh [project-name]Or manually initialize:
# Check Node.js version (18+ required)
node -v
# Create Next.js project with App Router
npx create-next-app@latest [project-name] \
--typescript \
--tailwind \
--app \
--src-dir \
--import-alias "@/*" \
--no-turbopack
cd [project-name]3.3 Install and Configure shadcn/ui
# Initialize shadcn/ui
npx shadcn@latest init
# Configuration prompts:
# - TypeScript: Yes
# - Style: Default
# - Base color: Choose from slate/gray/zinc/neutral/stone
# - CSS variables: Yes (CRITICAL - required for theming)
# - Import alias: @/componentsThis creates:
components.jsonconfig filelib/utils.tswith cn() helper- Updated
tailwind.config.tswith shadcn theme - CSS variables in
app/globals.css
3.4 Configure Design Tokens (CSS Variables)
VERY IMPORTANT: All theming MUST use CSS variables
Color Format: OKLCH (Recommended)
This skill uses OKLCH (OKLab Lightness Chroma Hue) color space instead of HSL for better perceptual uniformity and color accuracy.
OKLCH Benefits:
- Perceptually uniform (equal changes = equal perceived differences)
- Better gradient interpolation
- More predictable lightness
- Better for accessibility (more accurate contrast ratios)
Edit app/globals.css to define your design system:
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--radius: 0.65rem;
/* Background - Pure white */
--background: 1 0 0;
--foreground: 0.141 0.005 285.823;
/* Card */
--card: 1 0 0;
--card-foreground: 0.141 0.005 285.823;
/* Popover */
--popover: 1 0 0;
--popover-foreground: 0.141 0.005 285.823;
/* Primary - Warm orange */
--primary: 0.646 0.222 41.116;
--primary-foreground: 0.98 0.016 73.684;
/* Secondary - Light purple-gray */
--secondary: 0.967 0.001 286.375;
--secondary-foreground: 0.21 0.006 285.885;
/* Muted - Subtle elements */
--muted: 0.967 0.001 286.375;
--muted-foreground: 0.552 0.016 285.938;
/* Accent */
--accent: 0.967 0.001 286.375;
--accent-foreground: 0.21 0.006 285.885;
/* Destructive - Red */
--destructive: 0.577 0.245 27.325;
/* Border and Input */
--border: 0.92 0.004 286.32;
--input: 0.92 0.004 286.32;
/* Focus ring */
--ring: 0.75 0.183 55.934;
/* Chart colors */
--chart-1: 0.837 0.128 66.29;
--chart-2: 0.705 0.213 47.604;
--chart-3: 0.646 0.222 41.116;
--chart-4: 0.553 0.195 38.402;
--chart-5: 0.47 0.157 37.304;
}
.dark {
/* Dark mode backgrounds */
--background: 0.141 0.005 285.823;
--foreground: 0.985 0 0;
/* Dark mode card */
--card: 0.21 0.006 285.885;
--card-foreground: 0.985 0 0;
/* Dark mode popover */
--popover: 0.21 0.006 285.885;
--popover-foreground: 0.985 0 0;
/* Dark mode primary - Brighter for contrast */
--primary: 0.705 0.213 47.604;
--primary-foreground: 0.98 0.016 73.684;
/* Dark mode secondary */
--secondary: 0.274 0.006 286.033;
--secondary-foreground: 0.985 0 0;
/* Dark mode muted */
--muted: 0.274 0.006 286.033;
--muted-foreground: 0.705 0.015 286.067;
/* Dark mode accent */
--accent: 0.274 0.006 286.033;
--accent-foreground: 0.985 0 0;
/* Dark mode destructive */
--destructive: 0.704 0.191 22.216;
/* Dark mode borders (with alpha) */
--border: 1 0 0 / 10%;
--input: 1 0 0 / 15%;
/* Dark mode focus ring */
--ring: 0.408 0.123 38.172;
/* Chart colors (consistent) */
--chart-1: 0.837 0.128 66.29;
--chart-2: 0.705 0.213 47.604;
--chart-3: 0.646 0.222 41.116;
--chart-4: 0.553 0.195 38.402;
--chart-5: 0.47 0.157 37.304;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}Customizing for Migrated Project:
If migrating an existing app with a design system, extract existing colors and map to CSS variables:
# Use the detection script to find existing colors
bash ./scripts/detect-hardcoded-values.sh /path/to/old/codebase
# Map old colors to new CSS variables (OKLCH format)
# Use https://oklch.com or https://colorjs.io to convert
# Example:
# Old: #FF6B35 (brand orange) → --primary: 0.646 0.222 41.116
# Old: #3B82F6 (blue) → --primary: 0.630 0.213 255.5
# Old: #10B981 (green) → --success: 0.710 0.180 165.43.5 Install Essential shadcn Components
Use MCP to discover which components you need!
Install core components:
# Layout & Structure
npx shadcn@latest add card
npx shadcn@latest add separator
# Forms
npx shadcn@latest add button
npx shadcn@latest add input
npx shadcn@latest add label
npx shadcn@latest add select
npx shadcn@latest add checkbox
npx shadcn@latest add radio-group
npx shadcn@latest add form
# Navigation
npx shadcn@latest add navigation-menu
npx shadcn@latest add tabs
npx shadcn@latest add breadcrumb
# Feedback
npx shadcn@latest add alert
npx shadcn@latest add toast
npx shadcn@latest add dialog
npx shadcn@latest add tooltip
# Data Display
npx shadcn@latest add table
npx shadcn@latest add badge
npx shadcn@latest add avatar
# Overlays
npx shadcn@latest add popover
npx shadcn@latest add dropdown-menu
npx shadcn@latest add sheetQuery MCP for additional components as needed during development.
3.6 Set Up Theme Provider (Dark Mode Support)
Install next-themes:
npm install next-themesCreate components/theme-provider.tsx:
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}Update app/layout.tsx:
import { ThemeProvider } from "@/components/theme-provider"
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
)
}3.7 Create Example Component
Create components/example-card.tsx demonstrating best practices:
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
export function ExampleCard() {
return (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>shadcn/ui Best Practices</CardTitle>
<CardDescription>
This card demonstrates proper shadcn patterns
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Notice: No hardcoded colors, using CSS variables via Tailwind classes,
standard shadcn components, and proper typography scale.
</p>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline">Cancel</Button>
<Button>Continue</Button>
</CardFooter>
</Card>
)
}Key Patterns Demonstrated:
- Uses standard shadcn components (Card, Button)
- No hardcoded colors (uses
text-muted-foreground) - CSS variables automatically applied via Tailwind
- Semantic variants (
outline, default) - Proper component composition
---
Phase 4: Systematic Conversion (Detailed Instructions)
4.1 Component Conversion Workflow
For each batch of 5-10 components:
Step 1: Review Components in Batch
Load the component files and understand:
- Current functionality
- Props/API
- Styling approach
- State management
- Event handlers
Step 2: Query MCP for shadcn Equivalents
Before converting, use MCP:
- "What's the shadcn component for [component type]?"
- "Show me shadcn [component] props and examples"
- "How to use shadcn [component] with [feature]?"
Step 3: Convert Component
Follow this pattern:
// OLD: Custom button with hardcoded styles
const CustomButton = ({ children, onClick, variant = 'primary' }) => {
const styles = {
primary: {
backgroundColor: '#3b82f6', // HARDCODED!
color: '#ffffff',
padding: '8px 16px', // HARDCODED!
borderRadius: '6px'
},
secondary: {
backgroundColor: '#6b7280',
color: '#ffffff',
padding: '8px 16px',
borderRadius: '6px'
}
}
return (
<button style={styles[variant]} onClick={onClick}>
{children}
</button>
)
}
// NEW: shadcn Button with CSS variables
import { Button } from "@/components/ui/button"
const CustomButton = ({ children, onClick, variant = 'default' }) => {
return (
<Button variant={variant} onClick={onClick}>
{children}
</Button>
)
}Conversion Checklist:
- ✅ Replace with shadcn component
- ✅ Remove all hardcoded colors
- ✅ Remove inline styles
- ✅ Use Tailwind classes only
- ✅ Use semantic variants (default, outline, destructive, etc.)
- ✅ Preserve all functionality
- ✅ TypeScript types properly defined
Step 4: Replace Hardcoded Values with CSS Variables
// OLD: Hardcoded spacing and colors
<div style={{
backgroundColor: '#f3f4f6', // WRONG
padding: '20px', // WRONG
margin: '10px 0' // WRONG
}}>
// NEW: Tailwind classes using CSS variables
<div className="bg-secondary p-5 my-2.5">
// Or for custom spacing:
<div className="bg-secondary" style={{ padding: 'var(--spacing-5)' }}>Step 5: Test Functionality
After converting each component:
# Run tests
npm test
# Visual testing
npm run dev
# Manually verify component renders correctlyStep 6: Verify No Violations
Run detection script on new components:
bash ./scripts/detect-hardcoded-values.sh src/components/[batch-name]Should return 0 violations.
Step 7: Mark Batch Complete, Move to Next
Update todo list and migration-plan.md with progress.
4.2 Common Component Migrations
Refer to `./references/shadcn-component-mapping.md` for detailed mappings.
Quick reference:
| Pattern | Old Approach | shadcn Approach |
|---|---|---|
| Button | Custom styled button | <Button variant="..."> |
| Modal/Dialog | Custom overlay | <Dialog> with <DialogTrigger> and <DialogContent> |
| Form Input | Custom input with validation | <Form> + <FormField> + react-hook-form |
| Dropdown | Custom select | <Select> or <DropdownMenu> |
| Table | Custom table | <Table> or DataTable pattern |
| Tooltip | Custom hover component | <Tooltip> |
| Toast/Notification | Custom notification | useToast() hook + <Toaster> |
| Tabs | Custom tab component | <Tabs> with <TabsList> and <TabsContent> |
| Card | Custom card | <Card> with subcomponents |
| Badge | Custom badge/pill | <Badge variant="..."> |
4.3 Handling Complex Components
For components without direct shadcn equivalents:
Option 1: Composition
Build using shadcn primitives:
// Complex dashboard widget using shadcn primitives
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
export function DashboardWidget({ title, data, onRefresh }) {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>{title}</CardTitle>
<Button variant="outline" size="sm" onClick={onRefresh}>
Refresh
</Button>
</CardHeader>
<CardContent>
{data.map(item => (
<div key={item.id} className="flex items-center justify-between py-2">
<span className="text-sm">{item.label}</span>
<Badge variant={item.status === 'success' ? 'default' : 'destructive'}>
{item.value}
</Badge>
</div>
))}
</CardContent>
</Card>
)
}Option 2: Extend shadcn Components
Create custom components that extend shadcn:
// Custom component extending shadcn Button
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
interface IconButtonProps extends React.ComponentProps<typeof Button> {
icon: React.ReactNode
}
export function IconButton({ icon, children, className, ...props }: IconButtonProps) {
return (
<Button className={cn("flex items-center gap-2", className)} {...props}>
{icon}
{children}
</Button>
)
}Option 3: Use shadcn Blocks
Use MCP to discover shadcn blocks (pre-built complex components):
"What blocks are available in shadcn for [use case]?"
Install blocks:
npx shadcn@latest add [block-name]4.4 Page and Route Conversion
Convert pages to Next.js App Router structure:
Old Structure (React Router):
src/
pages/
Home.tsx
About.tsx
Dashboard.tsx
users/
UserList.tsx
UserDetail.tsxNew Structure (Next.js App Router):
app/
page.tsx # Home
about/
page.tsx # About
dashboard/
page.tsx # Dashboard
layout.tsx # Dashboard layout
users/
page.tsx # UserList
[id]/
page.tsx # UserDetail
layout.tsx # Root layout
loading.tsx # Loading state
error.tsx # Error boundaryExample Conversion:
// OLD: React Router page
// src/pages/Dashboard.tsx
import { useNavigate } from 'react-router-dom'
export function Dashboard() {
const navigate = useNavigate()
return (
<div style={{ padding: '20px' }}>
<h1>Dashboard</h1>
<button onClick={() => navigate('/users')}>
View Users
</button>
</div>
)
}
// NEW: Next.js App Router page
// app/dashboard/page.tsx
import Link from 'next/link'
import { Button } from '@/components/ui/button'
export default function DashboardPage() {
return (
<div className="container py-6">
<h1 className="text-3xl font-bold mb-6">Dashboard</h1>
<Link href="/users">
<Button>View Users</Button>
</Link>
</div>
)
}Key Changes:
- Default export for pages
- Use Next.js
<Link>instead of router navigation - Remove hardcoded padding → Tailwind classes
- Use shadcn Button
4.5 State Management Migration
Load reference: ./references/react-to-nextjs.md for state management patterns
Common patterns:
Local State (useState):
- Remains the same in Next.js App Router
- Mark client components with
"use client"
Global State:
- Redux → Zustand or React Context
- MobX → Zustand
- Recoil → Zustand or Jotai
Server State:
- React Query → TanStack Query (still works in Next.js)
- SWR → SWR (Next.js compatible)
- Or use Next.js Server Components for server data
---
Phase 5: Verification & Cleanup (Detailed Instructions)
5.1 Run Test Suite
# Run all tests
npm test
# Run with coverage
npm test -- --coverage
# Ensure 100% of migrated components have passing tests5.2 Visual Regression Testing
Manual verification checklist:
- [ ] All pages render correctly
- [ ] All components match design system
- [ ] Dark mode works correctly
- [ ] Responsive design maintained
- [ ] No hardcoded colors visible
- [ ] Consistent spacing throughout
- [ ] Typography follows design system
Optional: Set up automated visual regression testing with Percy, Chromatic, or Playwright.
5.3 Responsive Design Testing
Critical: Test on real devices, not just browser DevTools
Refer to: ./references/responsive-design-patterns.md for comprehensive responsive testing guidelines.
Device Testing Checklist:
- [ ] iPhone SE (320px - smallest modern viewport)
- [ ] iPhone 14 Pro (390px)
- [ ] iPad (768px)
- [ ] iPad Pro (1024px)
- [ ] Desktop (1280px - 1920px)
- [ ] Ultrawide (2560px+)
Orientation Testing:
- [ ] Portrait mode on mobile/tablet
- [ ] Landscape mode on mobile/tablet
- [ ] Responsive behavior when rotating device
Responsive Patterns Verification:
- [ ] Navigation: Mobile hamburger → tablet sidebar → desktop navbar
- [ ] Data tables: Mobile cards → tablet scrollable → desktop full table
- [ ] Grids: 1 col mobile → 2 cols tablet → 3-4 cols desktop
- [ ] Forms: Single column mobile → multi-column desktop
- [ ] Touch targets: Minimum 44x44px on mobile
- [ ] Typography: Readable at all screen sizes (minimum 16px body text)
- [ ] Images: Responsive sizing with proper aspect ratios
Breakpoint Verification:
# Test all Tailwind breakpoints
# sm: 640px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px
# Verify components respond correctly at each breakpointExample Components: Review ./assets/component-templates/ for responsive design examples:
responsive-navigation.tsx- Mobile/tablet/desktop navigation patternsresponsive-data-table.tsx- Responsive table with mobile cardsresponsive-dashboard.tsx- Responsive grid layoutscomplex-form.tsx- Multi-step responsive form
5.4 Run Final Hardcoded Values Check
bash ./scripts/detect-hardcoded-values.sh src/Expected result: 0 violations
If violations found, return to Phase 4 and fix.
5.5 Accessibility Audit (WCAG 2.1 Level AA)
Critical: Achieve WCAG 2.1 Level AA compliance
Refer to: ./references/accessibility-best-practices.md for comprehensive accessibility testing guidelines.
# Install axe DevTools or use Lighthouse
npm install -D @axe-core/playwright
# Run accessibility tests
npm run test:a11yshadcn/ui components are built with accessibility in mind (using Radix UI primitives), but verify:
Automated Testing:
- [ ] Run axe DevTools browser extension on all pages
- [ ] Lighthouse accessibility score >= 90
- [ ] No WCAG violations reported by automated tools
- [ ] eslint-plugin-jsx-a11y passing (if configured)
Keyboard Navigation (WCAG 2.1.1, 2.1.2):
- [ ] All interactive elements accessible via Tab
- [ ] Logical tab order (follows visual layout)
- [ ] No keyboard traps (can Tab out of all components)
- [ ] Enter/Space activates buttons/links
- [ ] Escape closes modals/dialogs
- [ ] Arrow keys navigate within components (tabs, menus, radio groups)
- [ ] Focus indicators visible on all interactive elements
Screen Reader Testing: Test with real screen readers:
- macOS: VoiceOver (Cmd + F5)
- Windows: NVDA (free) or JAWS
- Mobile: VoiceOver (iOS) or TalkBack (Android)
Screen Reader Checklist (WCAG 1.3.1, 4.1.2):
- [ ] All images have alt text
- [ ] Form inputs have associated labels
- [ ] Buttons have descriptive text or aria-label
- [ ] Headings follow logical hierarchy (h1 → h2 → h3)
- [ ] Landmarks present (header, nav, main, aside, footer)
- [ ] Dynamic content announces via aria-live regions
- [ ] Error messages announced to screen readers
Color Contrast (WCAG 1.4.3):
- [ ] Body text: Minimum 4.5:1 contrast ratio
- [ ] Large text (18pt+): Minimum 3:1 contrast ratio
- [ ] UI components: Minimum 3:1 contrast ratio
- [ ] Focus indicators: Minimum 3:1 contrast ratio
- [ ] Test both light and dark modes
Forms Accessibility (WCAG 3.3.1, 3.3.2):
- [ ] All inputs have visible labels
- [ ] Required fields clearly indicated
- [ ] Error messages specific and helpful
- [ ] Error messages associated with inputs (aria-describedby)
- [ ] Form validation doesn't rely on color alone
Touch Targets (WCAG 2.5.5):
- [ ] Minimum 44x44 CSS pixels for all touch targets on mobile
- [ ] Adequate spacing between touch targets (8px minimum)
ARIA Usage (WCAG 4.1.2):
- [ ] ARIA labels for icon-only buttons
- [ ] aria-expanded for collapsible sections
- [ ] aria-selected for tabs
- [ ] aria-hidden for decorative elements
- [ ] role="alert" for error messages
- [ ] role="status" for non-critical updates
Complete WCAG 2.1 AA Checklist: See ./references/accessibility-best-practices.md for full WCAG 2.1 Level AA checklist.
5.6 Performance Audit
# Build for production
npm run build
# Analyze bundle
npm run analyze # If you have bundle analyzer configuredRun Lighthouse audit:
- Performance >= 90
- Accessibility >= 90
- Best Practices >= 90
- SEO >= 90
Performance Checklist:
- [ ] Core Web Vitals meet "Good" thresholds
- LCP (Largest Contentful Paint) < 2.5s
- FID (First Input Delay) < 100ms
- CLS (Cumulative Layout Shift) < 0.1
- [ ] Images optimized (using Next.js Image component)
- [ ] Code splitting implemented for large components
- [ ] Lazy loading below-the-fold content
- [ ] Bundle size reasonable (check with bundle analyzer)
5.7 Remove Old Code
Once migration is verified:
# Remove old framework code
rm -rf src/old-components/ # or whatever old structure was
# Remove old dependencies
npm uninstall [old-framework] [old-ui-library] styled-components emotion ...
# Clean up old config files
rm -f .babelrc webpack.config.js # etc.5.8 Generate Completion Report
Create migration-complete-report.md:
# Migration Completion Report
## Summary
Successfully migrated [Project Name] from [Old Framework] to Next.js + shadcn/ui.
## Statistics
- **Components Migrated**: 47
- **Lines of Code Changed**: ~5,200
- **Hardcoded Values Removed**: 231
- **CSS Variables Added**: 48
- **shadcn Components Used**: 18
## Test Results
- **Unit Tests**: 142/142 passing
- **Integration Tests**: 23/23 passing
- **Accessibility Score**: 98/100
- **Lighthouse Performance**: 94/100
## Before/After Comparison
### Before
- Framework: React 18 + Vite
- Styling: styled-components + custom CSS
- Hardcoded values: 231 violations
- Bundle size: 523 KB
- Lighthouse: 76
### After
- Framework: Next.js 15 + App Router
- Styling: Tailwind CSS + shadcn/ui
- Hardcoded values: 0 violations
- Bundle size: 398 KB (24% reduction)
- Lighthouse: 94 (23% improvement)
## Design System
All components now use CSS variables defined in globals.css:
- 24 color tokens
- 12 spacing tokens
- 8 typography tokens
- Full dark mode support
## Next Steps
- Deploy to production
- Monitor performance metrics
- Gather user feedback
- Optional: Implement additional shadcn blocks---
shadcn Design Principles (CRITICAL)
Core Principles
1. CSS Variables for All Theming
- NEVER hardcode colors, spacing, or typography
- Define all design tokens as CSS variables in
globals.css - Use Tailwind classes that reference these variables
2. Standard Components Only
- Use shadcn/ui components as-is
- Extend via composition, not modification
- If shadcn doesn't have it, build with shadcn primitives
3. Consistent Design Language
- Follow shadcn's design patterns
- Use semantic variants (default, outline, destructive, ghost, link, secondary)
- Maintain consistent spacing scale
- Typography hierarchy from shadcn
4. No Custom UI Components
- Don't create custom buttons, inputs, cards, etc.
- Use shadcn components or build with shadcn primitives
- Exception: Business logic components (not UI primitives)
5. Accessibility First
- shadcn uses Radix UI (accessible by default)
- Don't override accessibility features
- Test with keyboard navigation and screen readers
Anti-Patterns to Avoid
NEVER DO THESE:
❌ Hardcoded Colors
// WRONG
<div style={{ backgroundColor: '#3b82f6' }}>
// RIGHT
<div className="bg-primary">❌ Hardcoded Spacing
// WRONG
<div style={{ padding: '20px', margin: '10px' }}>
// RIGHT
<div className="p-5 m-2.5">❌ Custom Styled Components for UI Primitives
// WRONG
const CustomButton = styled.button`
background: #3b82f6;
padding: 8px 16px;
border-radius: 6px;
`
// RIGHT
import { Button } from '@/components/ui/button'❌ Inline Styles
// WRONG
<div style={{ color: 'red', fontSize: '14px' }}>
// RIGHT
<div className="text-destructive text-sm">❌ Hardcoded Fonts
// WRONG
<h1 style={{ fontFamily: 'Montserrat' }}>
// RIGHT
<h1 className="font-sans text-4xl font-bold">❌ Emoji Icons (use Lucide React icons instead)
// WRONG
<span>❌ Delete</span>
// RIGHT
import { X } from 'lucide-react'
<Button variant="destructive">
<X className="mr-2 h-4 w-4" />
Delete
</Button>❌ Arbitrary CSS Values
// WRONG
<div className="text-[#ff0000]">
// RIGHT
<div className="text-destructive">Best Practices
✅ Use Semantic Color Names
// Use semantic CSS variable names
bg-background
text-foreground
bg-primary
text-primary-foreground
bg-secondary
text-muted-foreground
border-border✅ Use Tailwind Spacing Scale
// Consistent spacing using Tailwind
p-2, p-4, p-6, p-8 // padding
m-2, m-4, m-6, m-8 // margin
gap-2, gap-4 // flex/grid gap✅ Compose Components
// Build complex UIs by composing shadcn components
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'✅ Use MCP for Discovery
// Before building, ask MCP:
// "What shadcn component should I use for [use case]?"
// "Show me examples of shadcn [component]"✅ Extend via Composition
// Create higher-level components that compose shadcn
export function FeatureCard({ feature }: { feature: Feature }) {
return (
<Card>
<CardHeader>
<CardTitle>{feature.title}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">{feature.description}</p>
<Button className="mt-4">Learn More</Button>
</CardContent>
</Card>
)
}---
Reference Files
Framework-Specific Migration:
- React to Next.js Migration
- Vue to Next.js Migration
- Angular to Next.js Migration
Component & Styling:
- shadcn Component Mapping Reference
- Advanced shadcn Components Guide ⭐ NEW
- Styling Migration Guide
Responsive Design & Accessibility:
- Responsive Design Patterns Guide ⭐ NEW
- Accessibility Best Practices (WCAG 2.1) ⭐ NEW
Example Component Templates:
- Responsive Navigation - Mobile/tablet/desktop navigation patterns ⭐ NEW
- Responsive Data Table - Card view on mobile, table on desktop ⭐ NEW
- Responsive Dashboard - Responsive grid layouts with stats and charts ⭐ NEW
- Complex Multi-Step Form - Form validation, file uploads, conditional fields ⭐ NEW
- Feature Card Example - Basic shadcn component composition
- User Profile Example - Avatar, badges, and layout patterns
Scripts:
./scripts/analyze-codebase.py- Automated codebase analysis./scripts/detect-hardcoded-values.sh- Find anti-patterns./scripts/init-nextjs-shadcn.sh- Project initialization./scripts/generate-migration-report.py- Create migration report
Official Documentation:
- shadcn/ui: https://ui.shadcn.com
- Next.js: https://nextjs.org/docs
- Tailwind CSS: https://tailwindcss.com/docs
- Radix UI: https://www.radix-ui.com
- WCAG 2.1 Guidelines: https://www.w3.org/WAI/WCAG21/quickref/
---
Quick Start for New Projects
If creating a new Next.js app (not migrating):
1. Initialize
bash ./scripts/init-nextjs-shadcn.sh my-app2. Check MCP Access
npx shadcn@latest mcp init --client claude3. Install Components
- Use MCP to discover needed components
- Install with
npx shadcn@latest add [component]
4. Build with Best Practices
- Use CSS variables only
- Compose shadcn components
- Follow design principles above
---
Summary
This skill enables systematic frontend migration to Next.js + shadcn/ui with strict adherence to design principles:
- Automated analysis of existing codebases
- Systematic batch conversion (5-10 components at a time)
- Zero tolerance for hardcoded values (CSS variables only)
- MCP integration for shadcn component discovery
- Multi-framework support (React, Vue, Angular, vanilla JS)
- Comprehensive verification (tests, accessibility, performance)
Result: Production-grade Next.js applications following shadcn/ui best practices with consistent design systems, full dark mode support, and no anti-patterns.
/**
* Complex Multi-Step Form Component
*
* Demonstrates advanced form patterns:
* - Multi-step wizard with progress indication
* - Form validation using react-hook-form + zod
* - Conditional fields based on user input
* - File upload with preview
* - Responsive layout (single column mobile, multi-column desktop)
*
* shadcn/ui components used: Form, Input, Select, Checkbox, RadioGroup, Button, Card, Progress
* Best practices: Type-safe validation, error handling, accessibility
*/
"use client"
import * as React from "react"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
import {
ChevronLeft,
ChevronRight,
Check,
Upload,
X,
FileText,
AlertCircle,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Checkbox } from "@/components/ui/checkbox"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Textarea } from "@/components/ui/textarea"
import { Progress } from "@/components/ui/progress"
import { Separator } from "@/components/ui/separator"
import { Badge } from "@/components/ui/badge"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { cn } from "@/lib/utils"
// Form schema with validation
const formSchema = z.object({
// Step 1: Personal Information
firstName: z.string().min(2, "First name must be at least 2 characters"),
lastName: z.string().min(2, "Last name must be at least 2 characters"),
email: z.string().email("Invalid email address"),
phone: z.string().regex(/^\+?[\d\s\-()]+$/, "Invalid phone number"),
dateOfBirth: z.string().min(1, "Date of birth is required"),
// Step 2: Professional Information
occupation: z.string().min(1, "Please select your occupation"),
company: z.string().optional(),
yearsOfExperience: z.string().min(1, "Please select years of experience"),
employmentType: z.enum(["full-time", "part-time", "contract", "freelance"], {
required_error: "Please select employment type",
}),
isCurrentlyEmployed: z.boolean(),
// Step 3: Preferences & Additional Info
interests: z.array(z.string()).min(1, "Select at least one interest"),
skillLevel: z.enum(["beginner", "intermediate", "advanced", "expert"], {
required_error: "Please select your skill level",
}),
bio: z.string().min(10, "Bio must be at least 10 characters").max(500, "Bio must not exceed 500 characters"),
newsletter: z.boolean().default(false),
terms: z.boolean().refine((val) => val === true, {
message: "You must accept the terms and conditions",
}),
// File upload (optional in schema, but can be required)
resume: z.any().optional(),
})
type FormData = z.infer<typeof formSchema>
// Step configuration
const steps = [
{
id: 1,
title: "Personal Info",
description: "Basic information about you",
},
{
id: 2,
title: "Professional",
description: "Your work experience",
},
{
id: 3,
title: "Preferences",
description: "Interests and settings",
},
]
// Interest options
const interestOptions = [
{ id: "technology", label: "Technology" },
{ id: "design", label: "Design" },
{ id: "marketing", label: "Marketing" },
{ id: "sales", label: "Sales" },
{ id: "management", label: "Management" },
{ id: "analytics", label: "Analytics" },
]
interface ComplexFormProps {
onSubmit?: (data: FormData) => void | Promise<void>
}
export function ComplexForm({ onSubmit }: ComplexFormProps) {
const [currentStep, setCurrentStep] = React.useState(1)
const [isSubmitting, setIsSubmitting] = React.useState(false)
const [uploadedFile, setUploadedFile] = React.useState<File | null>(null)
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
firstName: "",
lastName: "",
email: "",
phone: "",
dateOfBirth: "",
occupation: "",
company: "",
yearsOfExperience: "",
employmentType: "full-time",
isCurrentlyEmployed: true,
interests: [],
skillLevel: "intermediate",
bio: "",
newsletter: false,
terms: false,
},
})
const isCurrentlyEmployed = form.watch("isCurrentlyEmployed")
// Calculate progress percentage
const progress = (currentStep / steps.length) * 100
// Handle next step
const handleNext = async () => {
let fieldsToValidate: (keyof FormData)[] = []
switch (currentStep) {
case 1:
fieldsToValidate = ["firstName", "lastName", "email", "phone", "dateOfBirth"]
break
case 2:
fieldsToValidate = ["occupation", "yearsOfExperience", "employmentType"]
if (isCurrentlyEmployed) {
fieldsToValidate.push("company")
}
break
case 3:
fieldsToValidate = ["interests", "skillLevel", "bio", "terms"]
break
}
const isValid = await form.trigger(fieldsToValidate)
if (isValid) {
setCurrentStep((prev) => Math.min(prev + 1, steps.length))
}
}
// Handle previous step
const handlePrevious = () => {
setCurrentStep((prev) => Math.max(prev - 1, 1))
}
// Handle file upload
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (file) {
// Validate file size (max 5MB)
if (file.size > 5 * 1024 * 1024) {
form.setError("resume", {
type: "manual",
message: "File size must be less than 5MB",
})
return
}
// Validate file type
const allowedTypes = [
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
]
if (!allowedTypes.includes(file.type)) {
form.setError("resume", {
type: "manual",
message: "Only PDF and DOC/DOCX files are allowed",
})
return
}
setUploadedFile(file)
form.setValue("resume", file)
form.clearErrors("resume")
}
}
// Handle form submission
const handleSubmit = async (data: FormData) => {
setIsSubmitting(true)
try {
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 2000))
if (onSubmit) {
await onSubmit(data)
} else {
console.log("Form submitted:", data)
}
// Reset form on success
form.reset()
setCurrentStep(1)
setUploadedFile(null)
} catch (error) {
console.error("Form submission error:", error)
} finally {
setIsSubmitting(false)
}
}
return (
<div className="w-full max-w-4xl mx-auto">
<Card>
<CardHeader>
<CardTitle>Registration Form</CardTitle>
<CardDescription>
Complete all steps to finish your registration
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Progress Bar */}
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="font-medium">
Step {currentStep} of {steps.length}
</span>
<span className="text-muted-foreground">{Math.round(progress)}%</span>
</div>
<Progress value={progress} className="h-2" />
</div>
{/* Step Indicators */}
<div className="flex items-center justify-between">
{steps.map((step, index) => (
<React.Fragment key={step.id}>
<div className="flex flex-col items-center gap-2">
<div
className={cn(
"flex h-10 w-10 items-center justify-center rounded-full border-2 transition-colors",
currentStep > step.id
? "border-primary bg-primary text-primary-foreground"
: currentStep === step.id
? "border-primary bg-background text-primary"
: "border-muted bg-background text-muted-foreground"
)}
>
{currentStep > step.id ? (
<Check className="h-5 w-5" />
) : (
<span className="text-sm font-medium">{step.id}</span>
)}
</div>
<div className="text-center hidden sm:block">
<p className="text-sm font-medium">{step.title}</p>
<p className="text-xs text-muted-foreground hidden md:block">
{step.description}
</p>
</div>
</div>
{index < steps.length - 1 && (
<Separator
className={cn(
"flex-1 mx-2",
currentStep > step.id ? "bg-primary" : "bg-muted"
)}
/>
)}
</React.Fragment>
))}
</div>
<Separator />
{/* Form */}
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
{/* Step 1: Personal Information */}
{currentStep === 1 && (
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="firstName"
render={({ field }) => (
<FormItem>
<FormLabel>First Name</FormLabel>
<FormControl>
<Input placeholder="John" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lastName"
render={({ field }) => (
<FormItem>
<FormLabel>Last Name</FormLabel>
<FormControl>
<Input placeholder="Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
placeholder="john.doe@example.com"
{...field}
/>
</FormControl>
<FormDescription>
We'll never share your email with anyone else
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="phone"
render={({ field }) => (
<FormItem>
<FormLabel>Phone Number</FormLabel>
<FormControl>
<Input placeholder="+1 (555) 123-4567" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="dateOfBirth"
render={({ field }) => (
<FormItem>
<FormLabel>Date of Birth</FormLabel>
<FormControl>
<Input type="date" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
)}
{/* Step 2: Professional Information */}
{currentStep === 2 && (
<div className="space-y-4">
<FormField
control={form.control}
name="occupation"
render={({ field }) => (
<FormItem>
<FormLabel>Occupation</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select your occupation" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="software-engineer">
Software Engineer
</SelectItem>
<SelectItem value="designer">Designer</SelectItem>
<SelectItem value="product-manager">
Product Manager
</SelectItem>
<SelectItem value="data-scientist">
Data Scientist
</SelectItem>
<SelectItem value="marketing">Marketing</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="isCurrentlyEmployed"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>I am currently employed</FormLabel>
<FormDescription>
Check this if you're working at a company
</FormDescription>
</div>
</FormItem>
)}
/>
{/* Conditional field - only show if employed */}
{isCurrentlyEmployed && (
<FormField
control={form.control}
name="company"
render={({ field }) => (
<FormItem>
<FormLabel>Company Name</FormLabel>
<FormControl>
<Input placeholder="Acme Inc." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="yearsOfExperience"
render={({ field }) => (
<FormItem>
<FormLabel>Years of Experience</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select years" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="0-1">0-1 years</SelectItem>
<SelectItem value="2-5">2-5 years</SelectItem>
<SelectItem value="6-10">6-10 years</SelectItem>
<SelectItem value="10+">10+ years</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="employmentType"
render={({ field }) => (
<FormItem>
<FormLabel>Employment Type</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="full-time">Full-time</SelectItem>
<SelectItem value="part-time">Part-time</SelectItem>
<SelectItem value="contract">Contract</SelectItem>
<SelectItem value="freelance">Freelance</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* File Upload */}
<FormField
control={form.control}
name="resume"
render={({ field }) => (
<FormItem>
<FormLabel>Resume (Optional)</FormLabel>
<FormControl>
<div className="space-y-2">
<div className="flex items-center gap-2">
<Input
type="file"
accept=".pdf,.doc,.docx"
onChange={handleFileChange}
className="hidden"
id="resume-upload"
/>
<label htmlFor="resume-upload">
<Button
type="button"
variant="outline"
className="cursor-pointer"
asChild
>
<span>
<Upload className="mr-2 h-4 w-4" />
Choose File
</span>
</Button>
</label>
{uploadedFile && (
<div className="flex items-center gap-2 text-sm">
<FileText className="h-4 w-4 text-muted-foreground" />
<span className="truncate max-w-[200px]">
{uploadedFile.name}
</span>
<Button
type="button"
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={() => {
setUploadedFile(null)
form.setValue("resume", undefined)
}}
>
<X className="h-4 w-4" />
</Button>
</div>
)}
</div>
</div>
</FormControl>
<FormDescription>
Upload your resume (PDF, DOC, or DOCX, max 5MB)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
{/* Step 3: Preferences & Additional Info */}
{currentStep === 3 && (
<div className="space-y-4">
<FormField
control={form.control}
name="interests"
render={() => (
<FormItem>
<FormLabel>Areas of Interest</FormLabel>
<FormDescription>
Select all that apply to you
</FormDescription>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 mt-2">
{interestOptions.map((option) => (
<FormField
key={option.id}
control={form.control}
name="interests"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={field.value?.includes(option.id)}
onCheckedChange={(checked) => {
return checked
? field.onChange([...field.value, option.id])
: field.onChange(
field.value?.filter(
(value) => value !== option.id
)
)
}}
/>
</FormControl>
<FormLabel className="font-normal cursor-pointer">
{option.label}
</FormLabel>
</FormItem>
)}
/>
))}
</div>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="skillLevel"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Skill Level</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="flex flex-col space-y-2"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="beginner" />
</FormControl>
<FormLabel className="font-normal cursor-pointer">
Beginner - Just starting out
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="intermediate" />
</FormControl>
<FormLabel className="font-normal cursor-pointer">
Intermediate - Comfortable with basics
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="advanced" />
</FormControl>
<FormLabel className="font-normal cursor-pointer">
Advanced - Proficient and experienced
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="expert" />
</FormControl>
<FormLabel className="font-normal cursor-pointer">
Expert - Master of the craft
</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="bio"
render={({ field }) => (
<FormItem>
<FormLabel>Bio</FormLabel>
<FormControl>
<Textarea
placeholder="Tell us a bit about yourself..."
className="resize-none min-h-[120px]"
{...field}
/>
</FormControl>
<FormDescription>
{field.value.length}/500 characters
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="newsletter"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>Subscribe to newsletter</FormLabel>
<FormDescription>
Receive updates and news via email
</FormDescription>
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="terms"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel className="text-sm">
I accept the{" "}
<a
href="#"
className="underline underline-offset-4 hover:text-primary"
>
terms and conditions
</a>
</FormLabel>
<FormMessage />
</div>
</FormItem>
)}
/>
{isSubmitting && (
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
Submitting your registration...
</AlertDescription>
</Alert>
)}
</div>
)}
</form>
</Form>
</CardContent>
<CardFooter className="flex flex-col-reverse sm:flex-row sm:justify-between gap-2">
<Button
type="button"
variant="outline"
onClick={handlePrevious}
disabled={currentStep === 1 || isSubmitting}
className="w-full sm:w-auto"
>
<ChevronLeft className="mr-2 h-4 w-4" />
Previous
</Button>
{currentStep < steps.length ? (
<Button
type="button"
onClick={handleNext}
disabled={isSubmitting}
className="w-full sm:w-auto"
>
Next
<ChevronRight className="ml-2 h-4 w-4" />
</Button>
) : (
<Button
type="button"
onClick={form.handleSubmit(handleSubmit)}
disabled={isSubmitting}
className="w-full sm:w-auto"
>
{isSubmitting ? "Submitting..." : "Submit"}
<Check className="ml-2 h-4 w-4" />
</Button>
)}
</CardFooter>
</Card>
</div>
)
}
/**
* Usage Example:
*
* ```tsx
* import { ComplexForm } from "@/components/complex-form"
*
* export default function RegistrationPage() {
* const handleFormSubmit = async (data) => {
* // Send data to your API
* const response = await fetch("/api/register", {
* method: "POST",
* body: JSON.stringify(data),
* })
*
* if (response.ok) {
* // Handle success (redirect, show message, etc.)
* router.push("/dashboard")
* }
* }
*
* return (
* <div className="container py-8">
* <ComplexForm onSubmit={handleFormSubmit} />
* </div>
* )
* }
* ```
*
* Key Features:
*
* 1. Multi-Step Wizard:
* - Visual progress indicator with steps
* - Step validation before proceeding
* - Navigation between steps
* - Progress percentage display
*
* 2. Form Validation:
* - react-hook-form for form state management
* - Zod schema for type-safe validation
* - Real-time field validation
* - Custom error messages
* - Conditional validation (company field)
*
* 3. Conditional Fields:
* - Company field shown only if currently employed
* - Dynamic validation based on selections
* - Reactive form behavior
*
* 4. File Upload:
* - Custom file input with button trigger
* - File type validation (PDF, DOC, DOCX)
* - File size validation (5MB max)
* - File preview with name display
* - Remove file functionality
*
* 5. Responsive Layout:
* - Single column on mobile (< 768px)
* - Multi-column grid on desktop (>= 768px)
* - Flexible button layout
* - Touch-friendly mobile experience
*
* Advanced Patterns:
*
* 1. Checkbox Arrays:
* - Multiple selection with interests
* - Array validation (min 1 selection)
* - Proper checked state management
*
* 2. Radio Groups:
* - Skill level selection
* - Single choice from options
* - Descriptive labels for each option
*
* 3. Textarea with Character Count:
* - Bio field with 500 char limit
* - Live character counter
* - Validation for min/max length
*
* 4. Terms & Conditions:
* - Required checkbox validation
* - Link to T&C document
* - Custom validation message
*
* Accessibility Features:
* - Semantic form structure
* - ARIA labels via shadcn Form components
* - Keyboard navigation support
* - Focus management between steps
* - Error announcements
* - Screen reader friendly
* - Proper label associations
*
* shadcn/ui Best Practices:
* - All colors use CSS variables
* - No hardcoded values
* - Proper Form component usage
* - FormField, FormItem, FormLabel, FormControl pattern
* - FormMessage for error display
* - FormDescription for help text
* - Lucide React icons only
* - cn() utility for conditional classes
* - Responsive Tailwind utilities
*
* Validation Best Practices:
* - Type-safe with Zod + TypeScript
* - Clear, actionable error messages
* - Progressive validation (per step)
* - Immediate feedback on blur
* - Prevent submission with invalid data
* - Custom validation logic support
*
* Performance Considerations:
* - React Hook Form minimizes re-renders
* - Zod validation is fast and efficient
* - File size check before upload
* - Debounce search/filter fields if needed
* - Consider React.memo for complex steps
*/
/**
* Example Feature Card Component
*
* Demonstrates shadcn/ui best practices:
* - Uses standard shadcn components (Card, Button, Badge)
* - No hardcoded colors or spacing
* - CSS variables via Tailwind classes
* - Proper TypeScript types
* - Clean composition pattern
*/
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { ArrowRight } from "lucide-react"
interface Feature {
id: string
title: string
description: string
category: string
status: "active" | "beta" | "coming-soon"
}
interface FeatureCardProps {
feature: Feature
onLearnMore?: (featureId: string) => void
}
export function FeatureCard({ feature, onLearnMore }: FeatureCardProps) {
const statusVariant = {
active: "default" as const,
beta: "secondary" as const,
"coming-soon": "outline" as const,
}
return (
<Card className="w-full max-w-md hover:shadow-lg transition-shadow">
<CardHeader>
<div className="flex items-start justify-between">
<div className="space-y-1">
<CardTitle className="text-2xl">{feature.title}</CardTitle>
<CardDescription>{feature.category}</CardDescription>
</div>
<Badge variant={statusVariant[feature.status]}>
{feature.status.replace("-", " ")}
</Badge>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground leading-relaxed">
{feature.description}
</p>
</CardContent>
<CardFooter>
<Button
className="w-full"
onClick={() => onLearnMore?.(feature.id)}
variant="default"
>
Learn More
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</CardFooter>
</Card>
)
}
/**
* Usage Example:
*
* const features: Feature[] = [
* {
* id: "1",
* title: "Dark Mode",
* description: "Automatic dark mode support with theme toggle",
* category: "Theming",
* status: "active"
* }
* ]
*
* <FeatureCard
* feature={features[0]}
* onLearnMore={(id) => console.log(`Learning about ${id}`)}
* />
*/
/**
* Example User Profile Component
*
* Demonstrates:
* - Complex component composition with shadcn primitives
* - Avatar, Badge, Button, Card usage
* - Responsive design with Tailwind
* - No hardcoded values
* - Semantic color usage
*/
import { Card, CardContent, CardDescription, CardHeader } from "@/components/ui/card"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Separator } from "@/components/ui/separator"
import { Mail, MapPin, Calendar } from "lucide-react"
interface UserProfileProps {
user: {
id: string
name: string
email: string
avatar?: string
role: string
location: string
joinedDate: string
isVerified: boolean
}
onMessage?: () => void
onViewProfile?: () => void
}
export function UserProfile({ user, onMessage, onViewProfile }: UserProfileProps) {
// Get initials for avatar fallback
const getInitials = (name: string) => {
return name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
.slice(0, 2)
}
return (
<Card className="w-full max-w-md">
<CardHeader className="pb-4">
<div className="flex items-start gap-4">
<Avatar className="h-16 w-16">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="bg-primary text-primary-foreground text-lg">
{getInitials(user.name)}
</AvatarFallback>
</Avatar>
<div className="flex-1 space-y-1">
<div className="flex items-center gap-2">
<h3 className="text-xl font-semibold">{user.name}</h3>
{user.isVerified && (
<Badge variant="secondary" className="text-xs">
Verified
</Badge>
)}
</div>
<CardDescription className="text-sm">{user.role}</CardDescription>
</div>
</div>
</CardHeader>
<Separator />
<CardContent className="pt-6 space-y-4">
{/* Contact Information */}
<div className="space-y-3">
<div className="flex items-center gap-3 text-sm">
<Mail className="h-4 w-4 text-muted-foreground" />
<span className="text-muted-foreground">{user.email}</span>
</div>
<div className="flex items-center gap-3 text-sm">
<MapPin className="h-4 w-4 text-muted-foreground" />
<span className="text-muted-foreground">{user.location}</span>
</div>
<div className="flex items-center gap-3 text-sm">
<Calendar className="h-4 w-4 text-muted-foreground" />
<span className="text-muted-foreground">
Joined {new Date(user.joinedDate).toLocaleDateString()}
</span>
</div>
</div>
{/* Actions */}
<div className="flex gap-2 pt-2">
<Button
variant="default"
className="flex-1"
onClick={onMessage}
>
<Mail className="mr-2 h-4 w-4" />
Message
</Button>
<Button
variant="outline"
className="flex-1"
onClick={onViewProfile}
>
View Profile
</Button>
</div>
</CardContent>
</Card>
)
}
/**
* Usage Example:
*
* const user = {
* id: "1",
* name: "Jane Doe",
* email: "jane@example.com",
* avatar: "https://github.com/janedoe.png",
* role: "Senior Developer",
* location: "San Francisco, CA",
* joinedDate: "2024-01-15",
* isVerified: true
* }
*
* <UserProfile
* user={user}
* onMessage={() => console.log("Send message")}
* onViewProfile={() => console.log("View profile")}
* />
*/
/**
* Responsive Dashboard Component
*
* Demonstrates mobile-first responsive dashboard layout:
* - Mobile (< 768px): Single column stacked layout
* - Tablet (768px - 1024px): 2-column grid layout
* - Desktop (>= 1024px): 3-4 column grid layout with dynamic sizing
*
* shadcn/ui components used: Card, Button, Badge, Progress, Separator, Avatar
* Best practices: Responsive grids, CSS variables, accessible charts placeholder
*/
"use client"
import * as React from "react"
import {
TrendingUp,
TrendingDown,
Users,
DollarSign,
ShoppingCart,
Activity,
ArrowUpRight,
ArrowDownRight,
MoreHorizontal,
Download,
} from "lucide-react"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Progress } from "@/components/ui/progress"
import { Separator } from "@/components/ui/separator"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { cn } from "@/lib/utils"
// Dashboard data types
interface StatCardData {
title: string
value: string
change: number
changeLabel: string
icon: React.ComponentType<{ className?: string }>
}
interface RecentActivity {
id: string
user: string
action: string
timestamp: string
avatar?: string
}
interface Goal {
id: string
title: string
progress: number
target: string
current: string
}
interface ResponsiveDashboardProps {
stats?: StatCardData[]
recentActivity?: RecentActivity[]
goals?: Goal[]
}
// Default mock data
const defaultStats: StatCardData[] = [
{
title: "Total Revenue",
value: "$45,231.89",
change: 20.1,
changeLabel: "from last month",
icon: DollarSign,
},
{
title: "Active Users",
value: "2,350",
change: 15.3,
changeLabel: "from last month",
icon: Users,
},
{
title: "Sales",
value: "12,234",
change: -4.5,
changeLabel: "from last month",
icon: ShoppingCart,
},
{
title: "Active Now",
value: "573",
change: 12.0,
changeLabel: "from last hour",
icon: Activity,
},
]
const defaultActivity: RecentActivity[] = [
{
id: "1",
user: "Olivia Martin",
action: "Made a purchase of $299.00",
timestamp: "2 minutes ago",
avatar: "https://avatar.vercel.sh/olivia",
},
{
id: "2",
user: "Jackson Lee",
action: "Subscribed to Pro plan",
timestamp: "15 minutes ago",
avatar: "https://avatar.vercel.sh/jackson",
},
{
id: "3",
user: "Isabella Nguyen",
action: "Updated profile information",
timestamp: "1 hour ago",
avatar: "https://avatar.vercel.sh/isabella",
},
{
id: "4",
user: "William Kim",
action: "Created a new project",
timestamp: "2 hours ago",
avatar: "https://avatar.vercel.sh/william",
},
{
id: "5",
user: "Sofia Davis",
action: "Left a review (5 stars)",
timestamp: "3 hours ago",
avatar: "https://avatar.vercel.sh/sofia",
},
]
const defaultGoals: Goal[] = [
{
id: "1",
title: "Monthly Revenue Goal",
progress: 72,
target: "$50,000",
current: "$36,000",
},
{
id: "2",
title: "New User Signups",
progress: 45,
target: "1,000 users",
current: "450 users",
},
{
id: "3",
title: "Customer Satisfaction",
progress: 88,
target: "90%",
current: "88%",
},
]
// Stat Card Component
function StatCard({ data }: { data: StatCardData }) {
const Icon = data.icon
const isPositive = data.change >= 0
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
<CardTitle className="text-sm font-medium">{data.title}</CardTitle>
<Icon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{data.value}</div>
<div className="flex items-center gap-1 text-xs text-muted-foreground mt-1">
{isPositive ? (
<ArrowUpRight className="h-3 w-3 text-green-500" />
) : (
<ArrowDownRight className="h-3 w-3 text-red-500" />
)}
<span className={isPositive ? "text-green-500" : "text-red-500"}>
{Math.abs(data.change)}%
</span>
<span>{data.changeLabel}</span>
</div>
</CardContent>
</Card>
)
}
// Recent Activity Card Component
function RecentActivityCard({ activities }: { activities: RecentActivity[] }) {
return (
<Card className="flex flex-col">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Recent Activity</CardTitle>
<CardDescription>Latest user actions and events</CardDescription>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>View all</DropdownMenuItem>
<DropdownMenuItem>Export</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Settings</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardHeader>
<CardContent className="flex-1">
<div className="space-y-4">
{activities.map((activity, index) => (
<div key={activity.id}>
<div className="flex items-start gap-3">
<Avatar className="h-9 w-9">
<AvatarImage src={activity.avatar} alt={activity.user} />
<AvatarFallback>
{activity.user.split(" ").map((n) => n[0]).join("")}
</AvatarFallback>
</Avatar>
<div className="flex-1 space-y-1">
<p className="text-sm font-medium">{activity.user}</p>
<p className="text-sm text-muted-foreground">
{activity.action}
</p>
<p className="text-xs text-muted-foreground">
{activity.timestamp}
</p>
</div>
</div>
{index < activities.length - 1 && (
<Separator className="mt-4" />
)}
</div>
))}
</div>
</CardContent>
<CardFooter>
<Button variant="ghost" className="w-full">
View all activity
<ArrowUpRight className="ml-2 h-4 w-4" />
</Button>
</CardFooter>
</Card>
)
}
// Goals Card Component
function GoalsCard({ goals }: { goals: Goal[] }) {
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Goals Progress</CardTitle>
<CardDescription>Track your monthly objectives</CardDescription>
</div>
<Button variant="outline" size="sm">
<Download className="mr-2 h-4 w-4" />
Export
</Button>
</div>
</CardHeader>
<CardContent className="space-y-6">
{goals.map((goal) => (
<div key={goal.id} className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="font-medium">{goal.title}</span>
<span className="text-muted-foreground">{goal.progress}%</span>
</div>
<Progress value={goal.progress} className="h-2" />
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>{goal.current}</span>
<span>Target: {goal.target}</span>
</div>
</div>
))}
</CardContent>
</Card>
)
}
// Sales Chart Placeholder Card Component
function SalesChartCard() {
return (
<Card className="flex flex-col">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Sales Overview</CardTitle>
<CardDescription>Monthly sales performance</CardDescription>
</div>
<Badge variant="secondary">This Month</Badge>
</div>
</CardHeader>
<CardContent className="flex-1">
{/* Placeholder for chart - integrate with recharts, chart.js, etc. */}
<div className="h-[200px] md:h-[250px] flex items-center justify-center rounded-lg border border-dashed">
<div className="text-center">
<TrendingUp className="h-8 w-8 mx-auto mb-2 text-muted-foreground" />
<p className="text-sm text-muted-foreground">
Chart placeholder
</p>
<p className="text-xs text-muted-foreground mt-1">
Integrate with your preferred charting library
</p>
</div>
</div>
</CardContent>
<CardFooter className="flex-col items-start gap-2">
<div className="flex w-full items-center justify-between text-sm">
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-primary" />
<span className="text-muted-foreground">Revenue</span>
</div>
<span className="font-medium">$45,231</span>
</div>
<div className="flex w-full items-center justify-between text-sm">
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-muted" />
<span className="text-muted-foreground">Expenses</span>
</div>
<span className="font-medium">$18,942</span>
</div>
</CardFooter>
</Card>
)
}
// Main Dashboard Component
export function ResponsiveDashboard({
stats = defaultStats,
recentActivity = defaultActivity,
goals = defaultGoals,
}: ResponsiveDashboardProps) {
return (
<div className="w-full space-y-4 md:space-y-6">
{/* Dashboard Header */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Dashboard</h1>
<p className="text-muted-foreground mt-1">
Welcome back! Here's your business overview.
</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline">
<Download className="mr-2 h-4 w-4" />
Export
</Button>
<Button>View Report</Button>
</div>
</div>
{/* Stats Grid - Responsive columns */}
{/* Mobile: 1 col, Tablet: 2 cols, Desktop: 4 cols */}
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
{stats.map((stat, index) => (
<StatCard key={index} data={stat} />
))}
</div>
{/* Main Content Grid - Responsive layout */}
{/* Mobile: 1 col, Tablet: 2 cols, Desktop: 3 cols with span variations */}
<div className="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
{/* Sales Chart - Spans 2 columns on tablet+, 1 on mobile */}
<div className="md:col-span-2 lg:col-span-2">
<SalesChartCard />
</div>
{/* Recent Activity - Spans 1 column on all breakpoints */}
<div className="md:col-span-2 lg:col-span-1">
<RecentActivityCard activities={recentActivity} />
</div>
{/* Goals - Spans full width on mobile, 1 col on tablet+ */}
<div className="md:col-span-2 lg:col-span-1">
<GoalsCard goals={goals} />
</div>
{/* Additional cards can be added here with responsive spanning */}
<div className="md:col-span-2 lg:col-span-2">
<Card>
<CardHeader>
<CardTitle>Additional Metrics</CardTitle>
<CardDescription>
Space for more dashboard components
</CardDescription>
</CardHeader>
<CardContent>
<div className="h-[200px] flex items-center justify-center rounded-lg border border-dashed">
<p className="text-sm text-muted-foreground">
Add more cards as needed
</p>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
)
}
/**
* Usage Example:
*
* ```tsx
* import { ResponsiveDashboard } from "@/components/responsive-dashboard"
*
* export default function DashboardPage() {
* // Fetch your real data here
* const stats = await fetchDashboardStats()
* const activity = await fetchRecentActivity()
* const goals = await fetchGoals()
*
* return (
* <div className="container py-6">
* <ResponsiveDashboard
* stats={stats}
* recentActivity={activity}
* goals={goals}
* />
* </div>
* )
* }
* ```
*
* Key Responsive Patterns:
*
* 1. Mobile (< 640px):
* - Single column layout (grid-cols-1)
* - Stats cards stacked vertically
* - All content full width
* - Compact spacing (space-y-4)
* - Touch-friendly buttons
*
* 2. Tablet (640px - 1024px):
* - 2-column stats grid (sm:grid-cols-2)
* - 2-column main grid (md:grid-cols-2)
* - Sales chart spans 2 columns (md:col-span-2)
* - Better use of horizontal space
* - Increased spacing (md:space-y-6)
*
* 3. Desktop (>= 1024px):
* - 4-column stats grid (lg:grid-cols-4)
* - 3-column main grid (lg:grid-cols-3)
* - Dynamic column spanning:
* - Sales chart: 2/3 width (lg:col-span-2)
* - Recent activity: 1/3 width (lg:col-span-1)
* - Goals: 1/3 width (lg:col-span-1)
* - Optimal information density
*
* Grid Spanning Strategies:
*
* Use col-span to create asymmetric, visually interesting layouts:
* ```tsx
* // Wide chart on desktop, full width on mobile
* <div className="md:col-span-2 lg:col-span-2">
*
* // Sidebar card on desktop, full width on tablet
* <div className="md:col-span-2 lg:col-span-1">
*
* // Full width on all breakpoints
* <div className="col-span-full">
* ```
*
* Chart Integration:
*
* Replace the SalesChartCard placeholder with real charts:
* - Recharts (recommended for shadcn/ui)
* - Chart.js with react-chartjs-2
* - Victory charts
* - D3.js with custom React wrapper
*
* Example with Recharts:
* ```tsx
* import { LineChart, Line, ResponsiveContainer } from "recharts"
*
* <ResponsiveContainer width="100%" height={250}>
* <LineChart data={salesData}>
* <Line
* type="monotone"
* dataKey="revenue"
* stroke="hsl(var(--primary))"
* strokeWidth={2}
* />
* </LineChart>
* </ResponsiveContainer>
* ```
*
* Accessibility Features:
* - Semantic HTML structure
* - ARIA labels for screen readers (sr-only)
* - Keyboard navigation support
* - Focus visible states
* - Proper heading hierarchy (h1, CardTitle)
* - Descriptive CardDescription elements
*
* shadcn/ui Best Practices:
* - All colors use CSS variables (bg-primary, text-muted-foreground)
* - No hardcoded values
* - Proper component composition (Card, Button, Badge, Progress)
* - Lucide React icons only
* - cn() utility for conditional classes
* - Responsive Tailwind utilities (sm:, md:, lg:)
* - Consistent spacing scale (gap-4, space-y-4)
*
* Performance Considerations:
* - Use React.memo() for StatCard if stats update frequently
* - Lazy load chart libraries with dynamic imports
* - Virtualize long activity lists if needed
* - Consider skeleton loading states
* - Optimize avatar images with Next.js Image component
*/
/**
* Responsive Data Table Component
*
* Demonstrates mobile-first responsive data table patterns:
* - Mobile (< 768px): Card-based layout with key information
* - Tablet (768px - 1024px): Scrollable table with priority columns
* - Desktop (>= 1024px): Full table with sorting, filtering, pagination
*
* shadcn/ui components used: Table, Card, Button, Badge, Input, Select, Avatar
* Best practices: Mobile-first design, touch-friendly targets, responsive overflow
*/
"use client"
import * as React from "react"
import {
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
Search,
ArrowUpDown,
ArrowUp,
ArrowDown,
MoreHorizontal,
Mail,
Phone,
} from "lucide-react"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { cn } from "@/lib/utils"
// User data type
interface User {
id: string
name: string
email: string
phone: string
role: "admin" | "user" | "manager"
status: "active" | "inactive" | "pending"
avatar?: string
joinedAt: string
lastActive: string
}
// Sort configuration
type SortKey = keyof User
type SortDirection = "asc" | "desc" | null
interface ResponsiveDataTableProps {
data: User[]
itemsPerPage?: number
}
// Status badge variant mapping
const statusVariant = {
active: "default" as const,
inactive: "secondary" as const,
pending: "outline" as const,
}
// Role badge variant mapping
const roleVariant = {
admin: "destructive" as const,
manager: "default" as const,
user: "secondary" as const,
}
export function ResponsiveDataTable({
data,
itemsPerPage = 10,
}: ResponsiveDataTableProps) {
const [searchQuery, setSearchQuery] = React.useState("")
const [currentPage, setCurrentPage] = React.useState(1)
const [sortKey, setSortKey] = React.useState<SortKey | null>(null)
const [sortDirection, setSortDirection] = React.useState<SortDirection>(null)
// Filter data based on search query
const filteredData = React.useMemo(() => {
if (!searchQuery) return data
const query = searchQuery.toLowerCase()
return data.filter(
(user) =>
user.name.toLowerCase().includes(query) ||
user.email.toLowerCase().includes(query) ||
user.role.toLowerCase().includes(query)
)
}, [data, searchQuery])
// Sort data
const sortedData = React.useMemo(() => {
if (!sortKey || !sortDirection) return filteredData
return [...filteredData].sort((a, b) => {
const aValue = a[sortKey]
const bValue = b[sortKey]
if (aValue < bValue) return sortDirection === "asc" ? -1 : 1
if (aValue > bValue) return sortDirection === "asc" ? 1 : -1
return 0
})
}, [filteredData, sortKey, sortDirection])
// Paginate data
const paginatedData = React.useMemo(() => {
const startIndex = (currentPage - 1) * itemsPerPage
return sortedData.slice(startIndex, startIndex + itemsPerPage)
}, [sortedData, currentPage, itemsPerPage])
const totalPages = Math.ceil(sortedData.length / itemsPerPage)
// Handle sort
const handleSort = (key: SortKey) => {
if (sortKey === key) {
if (sortDirection === "asc") {
setSortDirection("desc")
} else if (sortDirection === "desc") {
setSortKey(null)
setSortDirection(null)
}
} else {
setSortKey(key)
setSortDirection("asc")
}
}
// Render sort icon
const renderSortIcon = (key: SortKey) => {
if (sortKey !== key) {
return <ArrowUpDown className="ml-2 h-4 w-4 opacity-50" />
}
if (sortDirection === "asc") {
return <ArrowUp className="ml-2 h-4 w-4" />
}
return <ArrowDown className="ml-2 h-4 w-4" />
}
return (
<div className="w-full space-y-4">
{/* Search and filter controls */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search users..."
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value)
setCurrentPage(1) // Reset to first page on search
}}
className="pl-8"
/>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground whitespace-nowrap">
{sortedData.length} users
</span>
</div>
</div>
{/* Mobile Card View (< 768px) */}
<div className="md:hidden space-y-3">
{paginatedData.length === 0 ? (
<Card>
<CardContent className="py-8 text-center text-sm text-muted-foreground">
No users found
</CardContent>
</Card>
) : (
paginatedData.map((user) => (
<Card key={user.id} className="overflow-hidden">
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-3">
<Avatar className="h-10 w-10">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback>
{user.name.split(" ").map((n) => n[0]).join("")}
</AvatarFallback>
</Avatar>
<div>
<CardTitle className="text-base">{user.name}</CardTitle>
<p className="text-sm text-muted-foreground">{user.email}</p>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>View profile</DropdownMenuItem>
<DropdownMenuItem>Send message</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive">
Deactivate
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardHeader>
<CardContent className="space-y-2">
<div className="flex items-center gap-2">
<Phone className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-sm">{user.phone}</span>
</div>
<div className="flex flex-wrap gap-2">
<Badge variant={roleVariant[user.role]}>{user.role}</Badge>
<Badge variant={statusVariant[user.status]}>{user.status}</Badge>
</div>
<div className="pt-2 text-xs text-muted-foreground">
<p>Joined: {new Date(user.joinedAt).toLocaleDateString()}</p>
<p>Last active: {new Date(user.lastActive).toLocaleDateString()}</p>
</div>
</CardContent>
</Card>
))
)}
</div>
{/* Tablet/Desktop Table View (>= 768px) */}
<div className="hidden md:block rounded-md border">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[250px]">
<Button
variant="ghost"
size="sm"
className="h-8 px-2"
onClick={() => handleSort("name")}
>
User
{renderSortIcon("name")}
</Button>
</TableHead>
<TableHead className="hidden lg:table-cell">
<Button
variant="ghost"
size="sm"
className="h-8 px-2"
onClick={() => handleSort("email")}
>
Contact
{renderSortIcon("email")}
</Button>
</TableHead>
<TableHead>
<Button
variant="ghost"
size="sm"
className="h-8 px-2"
onClick={() => handleSort("role")}
>
Role
{renderSortIcon("role")}
</Button>
</TableHead>
<TableHead>
<Button
variant="ghost"
size="sm"
className="h-8 px-2"
onClick={() => handleSort("status")}
>
Status
{renderSortIcon("status")}
</Button>
</TableHead>
<TableHead className="hidden xl:table-cell">
<Button
variant="ghost"
size="sm"
className="h-8 px-2"
onClick={() => handleSort("joinedAt")}
>
Joined
{renderSortIcon("joinedAt")}
</Button>
</TableHead>
<TableHead className="hidden xl:table-cell">
<Button
variant="ghost"
size="sm"
className="h-8 px-2"
onClick={() => handleSort("lastActive")}
>
Last Active
{renderSortIcon("lastActive")}
</Button>
</TableHead>
<TableHead className="w-[50px]">
<span className="sr-only">Actions</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{paginatedData.length === 0 ? (
<TableRow>
<TableCell
colSpan={7}
className="h-24 text-center text-muted-foreground"
>
No users found
</TableCell>
</TableRow>
) : (
paginatedData.map((user) => (
<TableRow key={user.id}>
<TableCell>
<div className="flex items-center gap-3">
<Avatar className="h-8 w-8">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback>
{user.name.split(" ").map((n) => n[0]).join("")}
</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<span className="font-medium">{user.name}</span>
<span className="text-sm text-muted-foreground lg:hidden">
{user.email}
</span>
</div>
</div>
</TableCell>
<TableCell className="hidden lg:table-cell">
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2 text-sm">
<Mail className="h-3.5 w-3.5 text-muted-foreground" />
{user.email}
</div>
<div className="flex items-center gap-2 text-sm">
<Phone className="h-3.5 w-3.5 text-muted-foreground" />
{user.phone}
</div>
</div>
</TableCell>
<TableCell>
<Badge variant={roleVariant[user.role]}>{user.role}</Badge>
</TableCell>
<TableCell>
<Badge variant={statusVariant[user.status]}>
{user.status}
</Badge>
</TableCell>
<TableCell className="hidden xl:table-cell text-sm">
{new Date(user.joinedAt).toLocaleDateString()}
</TableCell>
<TableCell className="hidden xl:table-cell text-sm">
{new Date(user.lastActive).toLocaleDateString()}
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>View profile</DropdownMenuItem>
<DropdownMenuItem>Send message</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive">
Deactivate
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="text-sm text-muted-foreground">
Showing{" "}
<span className="font-medium">
{(currentPage - 1) * itemsPerPage + 1}
</span>{" "}
to{" "}
<span className="font-medium">
{Math.min(currentPage * itemsPerPage, sortedData.length)}
</span>{" "}
of <span className="font-medium">{sortedData.length}</span> results
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
>
<ChevronsLeft className="h-4 w-4" />
<span className="sr-only">First page</span>
</Button>
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
disabled={currentPage === 1}
>
<ChevronLeft className="h-4 w-4" />
<span className="sr-only">Previous page</span>
</Button>
<div className="flex items-center gap-2">
<span className="text-sm">
Page {currentPage} of {totalPages}
</span>
</div>
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={() =>
setCurrentPage((prev) => Math.min(totalPages, prev + 1))
}
disabled={currentPage === totalPages}
>
<ChevronRight className="h-4 w-4" />
<span className="sr-only">Next page</span>
</Button>
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages}
>
<ChevronsRight className="h-4 w-4" />
<span className="sr-only">Last page</span>
</Button>
</div>
</div>
)}
</div>
)
}
/**
* Usage Example:
*
* ```tsx
* import { ResponsiveDataTable } from "@/components/responsive-data-table"
*
* const users = [
* {
* id: "1",
* name: "John Doe",
* email: "john@example.com",
* phone: "+1 (555) 123-4567",
* role: "admin",
* status: "active",
* avatar: "https://avatar.example.com/john.jpg",
* joinedAt: "2024-01-15T10:00:00Z",
* lastActive: "2024-03-20T14:30:00Z",
* },
* // ... more users
* ]
*
* export default function UsersPage() {
* return (
* <div className="container py-6">
* <h1 className="text-3xl font-bold mb-6">Users</h1>
* <ResponsiveDataTable data={users} itemsPerPage={10} />
* </div>
* )
* }
* ```
*
* Key Responsive Patterns:
*
* 1. Mobile (< 768px):
* - Card-based layout for better mobile UX
* - Avatar + name as card header
* - Key information prominently displayed
* - Dropdown menu for actions
* - Touch-friendly tap targets (minimum 44px)
* - Vertical stacking of information
*
* 2. Tablet (768px - 1024px):
* - Traditional table layout
* - Priority columns visible (User, Role, Status, Actions)
* - Contact info hidden (shown in user column)
* - Horizontal scrolling if needed
* - Sortable column headers
*
* 3. Desktop (>= 1024px):
* - Full table with all columns
* - Separate Contact column (lg:table-cell)
* - Date columns visible (xl:table-cell)
* - Better spacing and readability
* - Hover states on rows
*
* Features:
* - Client-side search filtering
* - Column sorting (asc/desc/none)
* - Pagination with page controls
* - Responsive column visibility
* - Dropdown menus for row actions
* - Badge components for status/role
* - Avatar components with fallbacks
*
* Accessibility Features:
* - Semantic table structure
* - Screen reader labels (sr-only)
* - Keyboard navigation support
* - ARIA labels for icon buttons
* - Focus visible states
* - Proper heading hierarchy
*
* shadcn/ui Best Practices:
* - All colors use CSS variables
* - No hardcoded spacing or colors
* - Proper component composition
* - Lucide React icons only
* - cn() utility for conditional classes
* - Tailwind utility classes throughout
* - Responsive utility classes (md:, lg:, xl:)
*/
/**
* Example Theme using OKLCH Color Space
*
* OKLCH (OKLab Lightness Chroma Hue) is a perceptually uniform color space
* that provides better color accuracy and consistency than HSL.
*
* Benefits of OKLCH:
* - Perceptually uniform (equal changes = equal perceived differences)
* - Better gradient interpolation
* - More predictable lightness
* - Better for accessibility (more accurate contrast)
*
* Format: oklch(L C H / alpha)
* - L (Lightness): 0-1 (0 = black, 1 = white)
* - C (Chroma): 0-0.4+ (0 = grayscale, higher = more saturated)
* - H (Hue): 0-360 degrees
* - alpha: 0-1 or percentage (optional)
*/
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* Border radius for components */
--radius: 0.65rem;
/* Base colors */
--background: oklch(1 0 0); /* Pure white */
--foreground: oklch(0.141 0.005 285.823); /* Very dark blue-gray */
/* Card component colors */
--card: oklch(1 0 0); /* White */
--card-foreground: oklch(0.141 0.005 285.823); /* Dark blue-gray */
/* Popover colors */
--popover: oklch(1 0 0); /* White */
--popover-foreground: oklch(0.141 0.005 285.823); /* Dark blue-gray */
/* Primary brand color (orange/amber) */
--primary: oklch(0.646 0.222 41.116); /* Warm orange */
--primary-foreground: oklch(0.98 0.016 73.684); /* Nearly white with slight warmth */
/* Secondary colors (light purple-gray) */
--secondary: oklch(0.967 0.001 286.375); /* Very light purple-gray */
--secondary-foreground: oklch(0.21 0.006 285.885); /* Dark purple-gray */
/* Muted colors for subtle elements */
--muted: oklch(0.967 0.001 286.375); /* Very light purple-gray */
--muted-foreground: oklch(0.552 0.016 285.938); /* Medium gray */
/* Accent colors */
--accent: oklch(0.967 0.001 286.375); /* Very light purple-gray */
--accent-foreground: oklch(0.21 0.006 285.885); /* Dark purple-gray */
/* Destructive/danger colors (red) */
--destructive: oklch(0.577 0.245 27.325); /* Red */
/* Border and input colors */
--border: oklch(0.92 0.004 286.32); /* Light gray border */
--input: oklch(0.92 0.004 286.32); /* Light gray input border */
/* Focus ring color */
--ring: oklch(0.75 0.183 55.934); /* Yellowish focus ring */
/* Chart colors (warm palette) */
--chart-1: oklch(0.837 0.128 66.29); /* Light yellow */
--chart-2: oklch(0.705 0.213 47.604); /* Medium orange */
--chart-3: oklch(0.646 0.222 41.116); /* Primary orange */
--chart-4: oklch(0.553 0.195 38.402); /* Darker orange */
--chart-5: oklch(0.47 0.157 37.304); /* Deep orange */
/* Sidebar specific colors */
--sidebar: oklch(0.985 0 0); /* Off-white sidebar background */
--sidebar-foreground: oklch(0.141 0.005 285.823); /* Dark text */
--sidebar-primary: oklch(0.646 0.222 41.116); /* Orange for active items */
--sidebar-primary-foreground: oklch(0.98 0.016 73.684); /* Light text on orange */
--sidebar-accent: oklch(0.967 0.001 286.375); /* Light gray for hover states */
--sidebar-accent-foreground: oklch(0.21 0.006 285.885); /* Dark text */
--sidebar-border: oklch(0.92 0.004 286.32); /* Border color */
--sidebar-ring: oklch(0.75 0.183 55.934); /* Focus ring */
}
.dark {
/* Dark mode base colors */
--background: oklch(0.141 0.005 285.823); /* Very dark blue-gray */
--foreground: oklch(0.985 0 0); /* Off-white */
/* Dark mode card colors */
--card: oklch(0.21 0.006 285.885); /* Dark purple-gray */
--card-foreground: oklch(0.985 0 0); /* Off-white */
/* Dark mode popover colors */
--popover: oklch(0.21 0.006 285.885); /* Dark purple-gray */
--popover-foreground: oklch(0.985 0 0); /* Off-white */
/* Dark mode primary (brighter orange for contrast) */
--primary: oklch(0.705 0.213 47.604); /* Brighter orange */
--primary-foreground: oklch(0.98 0.016 73.684); /* Nearly white */
/* Dark mode secondary */
--secondary: oklch(0.274 0.006 286.033); /* Medium dark gray */
--secondary-foreground: oklch(0.985 0 0); /* Off-white */
/* Dark mode muted */
--muted: oklch(0.274 0.006 286.033); /* Medium dark gray */
--muted-foreground: oklch(0.705 0.015 286.067); /* Light gray */
/* Dark mode accent */
--accent: oklch(0.274 0.006 286.033); /* Medium dark gray */
--accent-foreground: oklch(0.985 0 0); /* Off-white */
/* Dark mode destructive (slightly darker red) */
--destructive: oklch(0.704 0.191 22.216); /* Dark mode red */
/* Dark mode borders (with alpha for transparency) */
--border: oklch(1 0 0 / 10%); /* 10% white overlay */
--input: oklch(1 0 0 / 15%); /* 15% white overlay */
/* Dark mode focus ring */
--ring: oklch(0.408 0.123 38.172); /* Darker orange ring */
/* Chart colors (same in both modes for consistency) */
--chart-1: oklch(0.837 0.128 66.29);
--chart-2: oklch(0.705 0.213 47.604);
--chart-3: oklch(0.646 0.222 41.116);
--chart-4: oklch(0.553 0.195 38.402);
--chart-5: oklch(0.47 0.157 37.304);
/* Dark mode sidebar */
--sidebar: oklch(0.21 0.006 285.885); /* Dark sidebar */
--sidebar-foreground: oklch(0.985 0 0); /* Light text */
--sidebar-primary: oklch(0.705 0.213 47.604); /* Brighter orange */
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
--sidebar-accent: oklch(0.274 0.006 286.033); /* Medium dark for hover */
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%); /* 10% white border */
--sidebar-ring: oklch(0.408 0.123 38.172); /* Darker orange ring */
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
/**
* Usage Examples:
*
* 1. Background colors:
* <div className="bg-background">Default background</div>
* <div className="bg-card">Card background</div>
* <div className="bg-primary">Primary brand color</div>
*
* 2. Text colors:
* <p className="text-foreground">Default text</p>
* <p className="text-muted-foreground">Muted text</p>
* <p className="text-primary-foreground">Text on primary</p>
*
* 3. Borders:
* <div className="border border-border">With border</div>
* <input className="border-input" />
*
* 4. Using in custom CSS:
* .custom-class {
* background-color: oklch(var(--primary));
* color: oklch(var(--primary-foreground));
* }
*
* 5. Chart colors:
* <AreaChart
* data={data}
* categories={["sales", "revenue"]}
* colors={["chart-1", "chart-2"]}
* />
*/
/**
* Converting from hex to OKLCH:
*
* Use online tools or color libraries:
* - https://oklch.com
* - https://colorjs.io
* - culori (npm package)
*
* Example conversion:
* #FF6B35 (orange) → oklch(0.646 0.222 41.116)
* #3B82F6 (blue) → oklch(0.630 0.213 255.5)
* #10B981 (green) → oklch(0.710 0.180 165.4)
*
* OKLCH provides better perceptual uniformity than HSL,
* especially important for gradients and color scales.
*/
Related skills
How it compares
Choose this over generic Next.js skills when you need shadcn/ui enforcement and multi-framework migration automation.
FAQ
What frontend frameworks can nextjs-shadcn-builder migrate?
nextjs-shadcn-builder migrates React, Vue, Angular, and vanilla JavaScript frontends to Next.js 15+ with shadcn/ui. It runs automated codebase analysis, builds a migration plan, and converts components in batched phases with verification.
What design rules does nextjs-shadcn-builder enforce?
nextjs-shadcn-builder enforces CSS variables for theming, standard shadcn/ui components only, no hardcoded color or spacing values, mobile-first responsive layouts, and WCAG 2.1 AA accessibility checks during conversion.