AI-Generated shadcn — Hardcoded Colors Broke Dark Mode
200 tickets in 4 hours after dark mode toggle made white text on white backgrounds across 18 shadcn components — all from AI-generated hardcoded colors.
N
Naren · Founder
Plain-English first. Then code. Then the interview question.
The workflow combines v0 (generative scaffolding) and Cursor (contextual refinement) to accelerate shadcn/ui component creation
v0 produces styled React component shells from structured text prompts — generation takes 2 minutes per component
Cursor adapts that output to your project's design tokens, hooks, and patterns — refinement takes 8-10 minutes
Total workflow time: 10-12 minutes per component versus 25-35 minutes for manual creation
The specification document is the system's foundation — without it, components drift from each other at scale
Biggest mistake: treating generated code as final — the quality gate review is where the real engineering happens
Plain-English First
Think of it like a factory assembly line for furniture. v0 is the CNC machine that cuts the raw pieces from a blueprint — fast, consistent, but the pieces still need finishing. Cursor is the skilled craftsperson who sands the edges, applies the finish, and fits the piece into the room it was built for. Neither produces a finished product alone. Together they turn what used to be an all-day task into a morning's work — and the system means piece 47 looks as good as piece 1.
Manual component creation is a scaling bottleneck. Each component requires boilerplate, variant logic, accessibility markup, and design token integration. For a team building a design system from scratch, this process does not scale beyond a handful of components per sprint.
AI tools automate the scaffolding phase. By combining v0's generative output with Cursor's contextual editing, you create a repeatable pipeline that produces consistent components in a fraction of the manual time. The developer shifts from writing boilerplate to curating and refining AI output — a more leveraged use of engineering judgment.
This guide covers the complete workflow with real before/after examples, a reusable specification system, prompt templates across five component types, and quality gates that prevent AI-generated drift from reaching production. It also covers when this workflow should not be used — not every component is a good candidate for AI generation.
When to Use This Workflow — and When Not To
Not every component is a good candidate for AI generation. Understanding the boundaries of this workflow prevents wasted effort and poor-quality output.
Good candidates: presentational components that display data passed via props (cards, badges, stat displays, alert banners), layout components with predictable structure (page headers, sidebars, navigation bars), form field wrappers around shadcn/ui Input and Select primitives, and data display components like tables and lists that follow a consistent pattern.
Poor candidates: components with complex custom animations or gesture handling, components that require deep accessibility work (custom date pickers, drag-and-drop file uploads, rich text editors), components that embed domain-specific business logic, and any component where the design is so custom that the AI has no useful prior patterns to draw from.
The rule: if a senior developer would describe the component as 'standard,' it is a good AI generation candidate. If they would say 'we need to design this carefully,' it is not.
src/config/component-candidates.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// component-candidates.ts// Use this checklist before deciding whether to AI-generate a component.// Answer each question — if more than two answers are NO, build manually.interfaceGenerationCandidate {
name: string;
// YES = good candidate, NO = build manually
checks: {
isPresentational: boolean; // Accepts data via props, does not own state?
hasStandardPattern: boolean; // Does a similar component exist in shadcn/ui docs or common design systems?
isAccessibilityStandard: boolean; // Does WCAG handling follow a well-known pattern (not custom)?
hasNoBusinessLogic: boolean; // Is logic handled by a parent hook or container?
isDecomposable: boolean; // Can it be broken into 2-3 shadcn/ui primitives?
};
}
// GOOD CANDIDATES — all or most checks passconst goodCandidates: GenerationCandidate[] = [
{
name: 'MetricCard',
checks: {
isPresentational: true,
hasStandardPattern: true,
isAccessibilityStandard: true,
hasNoBusinessLogic: true,
isDecomposable: true,
},
},
{
name: 'StatusBadge',
checks: {
isPresentational: true,
hasStandardPattern: true,
isAccessibilityStandard: true,
hasNoBusinessLogic: true,
isDecomposable: true,
},
},
{
name: 'DataTable',
checks: {
isPresentational: true,
hasStandardPattern: true,
isAccessibilityStandard: true,
hasNoBusinessLogic: true, // sorting/filtering logic in useDataTable hook
isDecomposable: true,
},
},
];
// POOR CANDIDATES — multiple checks failconst poorCandidates: GenerationCandidate[] = [
{
name: 'DragDropFileUpload',
checks: {
isPresentational: false, // Manages drag state internally
hasStandardPattern: false, // Interaction model varies significantly
isAccessibilityStandard: false, // Drag-and-drop a11y is complex and non-standard
hasNoBusinessLogic: false, // File validation logic belongs in component
isDecomposable: false, // No shadcn/ui primitives cover drag-and-drop
},
},
{
name: 'RichTextEditor',
checks: {
isPresentational: false,
hasStandardPattern: false,
isAccessibilityStandard: false,
hasNoBusinessLogic: false,
isDecomposable: false,
},
},
];
The Standard vs. Custom Test
Standard components: cards, badges, alerts, form wrappers, stat displays, navigation items — high AI success rate
Custom components: drag-and-drop, rich text, complex animations, custom date pickers — AI output requires more work than building manually
The five-check candidate evaluation takes 2 minutes and saves hours of rework on poor-fit components
When in doubt: generate a prototype with v0 to see if the output is close enough to refine
Production Insight
A team attempted to AI-generate a custom multi-step date range picker.
v0 produced four different implementations across four prompts — none matched the design requirements.
Manual build took 4 hours. The failed AI attempts wasted 3 hours before the team abandoned the approach.
Rule: run the candidate check before opening v0. It takes 2 minutes and prevents wasted sessions.
Key Takeaway
AI generation works best for standard, presentational components with clear shadcn/ui primitive coverage.
Run the five-check candidate evaluation before starting — two or more failing checks means build manually.
The workflow's time savings come from good candidate selection, not from applying it to everything.
The Specification System: Foundation Before Generation
Generating one component is a trick. Generating fifty consistently requires a system. The foundation is the Component Specification Document — a structured definition of every component written before any generation begins.
The spec serves three purposes: it produces a consistent v0 prompt, it documents the component's intent for future maintainers, and it creates a review artifact that can be approved before any code is written.
For each component, define: name, core function (one sentence maximum), key props (three to five), variant options with defaults, required states, and the shadcn/ui primitives it should use. This last field is critical — telling v0 which primitives to use dramatically reduces the amount of Cursor refactoring needed.
src/config/component-specs.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// component-specs.ts// Component Specification Document — the blueprint for AI generation.// Write specs first. Generate second. Review the spec before the code.interfacePropSpec {
name: string;
type: string;
required: boolean;
description: string;
}
interfaceVariantSpec {
name: string;
options: string[];
default: string;
}
interfaceComponentSpec {
name: string; // PascalCase component name
description: string; // One sentence — core function only
props: PropSpec[]; // 3-5 key configuration props
variants: VariantSpec[]; // size, status, density options
states: string[]; // loading | error | empty | disabled
primitives: string[]; // shadcn/ui primitives to use
doNotGenerate?: string; // Business logic that stays in a hook
}
// ---------------------------------------------------------------// Example specs across five component categories// ---------------------------------------------------------------// CATEGORY 1: Presentational displayexportconst metricCardSpec: ComponentSpec = {
name: 'MetricCard',
description: 'Displays a single KPI with label, value, delta, and trend direction.',
props: [
{ name: 'label', type: 'string', required: true, description: 'Metric name displayed above the value' },
{ name: 'value', type: 'string | number', required: true, description: 'Primary metric value' },
{ name: 'delta', type: 'number', required: false, description: 'Change from previous period as a percentage' },
{ name: 'trend', type: "'up' | 'down' | 'neutral'", required: false, description: 'Trend direction for icon and color' },
],
variants: [
{ name: 'size', options: ['sm', 'md', 'lg'], default: 'md' },
],
states: ['loading'],
primitives: ['Card', 'CardHeader', 'CardContent', 'Skeleton'],
doNotGenerate: 'Data fetching and delta calculation belong in a useMetrics hook',
};
// CATEGORY 2: Status / feedbackexportconst statusBadgeSpec: ComponentSpec = {
name: 'StatusBadge',
description: 'Inline badge displaying an entity status with icon and semantic color.',
props: [
{ name: 'status', type: "'active' | 'inactive' | 'pending' | 'error'", required: true, description: 'Current status value' },
{ name: 'label', type: 'string', required: false, description: 'Override the default status label' },
{ name: 'showIcon', type: 'boolean', required: false, description: 'Show status icon alongside label' },
],
variants: [
{ name: 'size', options: ['sm', 'md'], default: 'md' },
],
states: [],
primitives: ['Badge'],
doNotGenerate: 'Status-to-color mapping should reference design tokens, not hardcoded colors',
};
// CATEGORY 3: Data displayexportconst dataTableSpec: ComponentSpec = {
name: 'DataTable',
description: 'Sortable, filterable table with pagination, row selection, and column visibility.',
props: [
{ name: 'data', type: 'T[]', required: true, description: 'Array of row data objects' },
{ name: 'columns', type: 'ColumnDef<T>[]', required: true, description: 'TanStack column definitions' },
{ name: 'onRowSelect', type: '(rows: T[]) => void', required: false, description: 'Callback when row selection changes' },
{ name: 'pageSize', type: 'number', required: false, description: 'Rows per page, defaults to 20' },
],
variants: [
{ name: 'density', options: ['compact', 'default', 'comfortable'], default: 'default' },
],
states: ['loading', 'error', 'empty'],
primitives: ['Table', 'TableHeader', 'TableBody', 'TableRow', 'TableCell', 'Button', 'Input', 'Skeleton'],
doNotGenerate: 'Sorting, filtering, and pagination logic belongs in useDataTable hook',
};
// CATEGORY 4: Form input wrapperexportconst tagInputSpec: ComponentSpec = {
name: 'TagInput',
description: 'Text input that converts entries to removable tag pills on Enter or comma.',
props: [
{ name: 'value', type: 'string[]', required: true, description: 'Current array of tag strings' },
{ name: 'onChange', type: '(tags: string[]) => void', required: true, description: 'Called when tags array changes' },
{ name: 'placeholder', type: 'string', required: false, description: 'Input placeholder text' },
{ name: 'maxTags', type: 'number', required: false, description: 'Maximum number of tags allowed' },
],
variants: [
{ name: 'size', options: ['sm', 'md'], default: 'md' },
],
states: ['disabled', 'error'],
primitives: ['Input', 'Badge', 'Button'],
doNotGenerate: 'Validation logic belongs in the parent form handler',
};
// CATEGORY 5: Navigationexportconst sidebarNavSpec: ComponentSpec = {
name: 'SidebarNav',
description: 'Vertical navigation list with active state, icons, and collapsible groups.',
props: [
{ name: 'items', type: 'NavItem[]', required: true, description: 'Navigation items with label, href, and optional icon' },
{ name: 'activeHref', type: 'string', required: true, description: 'Current route href for active state highlighting' },
{ name: 'collapsed', type: 'boolean', required: false, description: 'Whether the sidebar is in icon-only collapsed mode' },
],
variants: [
{ name: 'size', options: ['sm', 'md'], default: 'md' },
],
states: ['loading'],
primitives: ['Button', 'Tooltip', 'Collapsible', 'CollapsibleTrigger', 'CollapsibleContent'],
doNotGenerate: 'Route matching and collapse state belong in a layout-level hook',
};
Spec First, Prompt Second
Write all 50 specs before generating any components — this surfaces duplicates and sprawl before code is written
The doNotGenerate field is as important as the props list — it prevents AI from embedding logic that belongs in a hook
Specs double as living documentation — future maintainers understand the component's intent without reading the code
One spec reviewer can approve 10 specs in the time it takes to review one generated component
Production Insight
A team started generating without specs and hit 80 components before realizing 22 were variations of the same base card.
Deleting and consolidating the duplicates took longer than generating them had.
The team that wrote specs first caught 14 duplicates before a single prompt was written.
Rule: write all specs first. Run a duplicate check. Then open v0.
Key Takeaway
The specification document is the system's foundation — without it, consistency at scale is impossible.
Write all specs before generating any components — duplicates and sprawl are caught in the spec phase, not the code phase.
The doNotGenerate field prevents AI from embedding logic that belongs in hooks.
Phase 1: Generation with v0 — Prompts That Produce Usable Output
v0 translates structured component descriptions into functional React components. The quality of output depends entirely on prompt specificity. A vague prompt produces a vague component that requires extensive rework. A structured prompt derived from a spec produces output that needs only targeted refinement.
The prompt template below translates directly from a ComponentSpec. Every field in the spec maps to a section of the prompt. This consistency means any developer on the team can generate the same quality of output from the same spec.
Critical prompt elements that v0 responds to well: explicit shadcn/ui primitive names, semantic token instruction ('use semantic Tailwind tokens only — no hardcoded colors'), state requirements ('include loading, error, and empty states'), and the separation instruction ('this is a presentational component — accept all data and callbacks via props, no internal data fetching').
prompts/v0-prompt-template.txtTEXT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Create a shadcn/ui {COMPONENT_NAME} component.
Core function: {ONE_SENTENCE_DESCRIPTION}
Props:
- {PROP_1_NAME}: {PROP_1_TYPE} — {PROP_1_DESCRIPTION}
- {PROP_2_NAME}: {PROP_2_TYPE} — {PROP_2_DESCRIPTION}
- {PROP_3_NAME}: {PROP_3_TYPE} — {PROP_3_DESCRIPTION}
Variants:
- {VARIANT_NAME}: {OPTION_1} | {OPTION_2} | {OPTION_3} (default: {DEFAULT})
States: Include {STATE_1}, {STATE_2}, and {STATE_3} states.
Use these shadcn/ui primitives: {PRIMITIVE_1}, {PRIMITIVE_2}, {PRIMITIVE_3}
Style rules:
- Use semantic Tailwind tokens only: bg-background, bg-card, text-foreground,
text-muted-foreground, border-border, bg-primary, text-primary-foreground
- No hardcoded color classes (no bg-white, bg-gray-*, text-gray-*, border-gray-*)
- Usecn() from @/lib/utils for conditional class merging
Architecture:
- This is a presentational component only
- Accept all data and callbacks via props — no internal data fetching or API calls
- IncludeTypeScript types for all props
- Export the component as a named export
The Five Lines That Prevent 80% of Rework
List exact shadcn/ui primitive names — v0 uses them correctly when named explicitly
Include 'semantic Tailwind tokens only' with examples — this alone prevents the dark mode drift incident
Include 'no hardcoded color classes' with examples of what not to use
Include 'presentational component only' — this prevents business logic from being embedded
Include 'named export' — v0 sometimes generates default exports that conflict with barrel export patterns
Production Insight
Side-by-side prompt comparison for MetricCard:
Vague prompt: 'Create a metric card component with a number and label.'
Result: generic div with hardcoded gray colors, no variants, no states, no TypeScript.
Rework required: 45 minutes.
Structured prompt: full template above with MetricCard spec values filled in.
Result: complete component with all variants, loading state with Skeleton, semantic tokens throughout.
Rework required: 8 minutes of Cursor refinement.
Key Takeaway
The structured prompt template translates directly from the ComponentSpec — no creative writing required.
Five style rules in the prompt prevent 80% of the rework that generic prompts create.
Document your filled-in prompts alongside the spec — they become reusable for similar components.
Before and After: What v0 Produces vs. What Ships
The most important section of this workflow is the one most articles skip: showing exactly what v0 output looks like before refinement, and what the same component looks like after the Cursor phase.
The before/after comparison below uses MetricCard. The v0 output is functional and visually correct in light mode. It fails in dark mode, uses broad TypeScript types, imports from direct shadcn paths instead of barrel exports, and embeds no real structure for the loading state. The after version fixes all of these in targeted Cursor steps.
src/components/MetricCard.before.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// BEFORE: Raw v0 output — functional but not production-ready// Issues marked with // ❌
import { Card, CardContent, CardHeader } from '@/components/ui/card'; // ❌ direct path, not barrel
import { Skeleton } from '@/components/ui/skeleton'; // ❌ direct pathimport { ArrowUpIcon, ArrowDownIcon } from'lucide-react';
// ❌ Broad prop types — trend should be a union type, not string// ❌ delta typed as number but should handle undefined explicitlyinterfaceMetricCardProps {
label: string;
value: string | number;
delta?: number;
trend?: string; // ❌ should be 'up' | 'down' | 'neutral'
size?: string; // ❌ should be 'sm' | 'md' | 'lg'
loading?: boolean;
}
export default function MetricCard({ // ❌ default export — conflicts with barrel pattern
label,
value,
delta,
trend,
size = 'md',
loading = false,
}: MetricCardProps) {
if (loading) {
return (
<div className="p-4 bg-white rounded-lg border border-gray-200"> {/* ❌ hardcoded colors */}
<Skeleton className="h-4 w-24 mb-2" />
<Skeleton className="h-8 w-16" />
</div>
);
}
const trendColor =
trend === 'up' ? 'text-green-600' : // ❌ hardcoded color
trend === 'down' ? 'text-red-600' : // ❌ hardcoded color
'text-gray-500'; // ❌ hardcoded colorconst sizeClasses = {
sm: 'p-3',
md: 'p-4',
lg: 'p-6',
};
return (
<Card className="bg-white border-gray-200"> {/* ❌ hardcoded colors */}
<CardHeader className="pb-2">
<p className="text-sm text-gray-500">{label}</p> {/* ❌ hardcoded color */}
</CardHeader>
<CardContent>
<p className="text-2xl font-bold text-gray-900">{value}</p> {/* ❌ hardcoded color */}
{delta !== undefined && (
<div className={`flex items-center gap-1 mt-1 ${trendColor}`}>
{trend === 'up' ? <ArrowUpIcon size={14} /> : <ArrowDownIcon size={14} />}
<span className="text-sm">{Math.abs(delta)}%</span>
</div>
)}
</CardContent>
</Card>
);
}
What v0 Gets Wrong Every Time
Hardcoded Tailwind colors — bg-white, text-gray-*, text-green-600, text-red-600 appear in almost every output
Default exports instead of named exports — breaks barrel export patterns
Direct import paths (@/components/ui/card) instead of your project's configured import aliases
Broad union types — trend?: string instead of trend?: 'up' | 'down' | 'neutral'
Missing neutral state handling — if trend is undefined and delta exists, the icon logic breaks
Production Insight
These are not random failures — v0 produces the same categories of issues almost every time.
Knowing the failure patterns means your Cursor refinement is targeted, not exploratory.
The five Cursor commands below fix 90% of these issues in under 10 minutes.
Key Takeaway
The v0 output is a draft — expect hardcoded colors, broad types, default exports, and direct import paths.
Knowing the consistent failure patterns turns the Cursor refinement phase into targeted fixes, not exploration.
Phase 2: Cursor Refinement — The Five Targeted Commands
Cursor's Cmd+K inline command transforms the v0 output into a production-ready component. The refinement phase is not open-ended exploration — it is a sequence of five targeted commands that fix the consistent failure patterns v0 produces.
Run these commands in order on the pasted v0 output. Each command is scoped to one failure pattern. Running them in sequence takes 8-10 minutes and fixes 90% of the issues in the raw output.
After the five commands, the remaining 10% is manual review: verify the TypeScript types match your actual data models, test both light and dark mode in the browser, and confirm the component integrates with the specific hook or API it will use in production.
src/components/MetricCard.after.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// AFTER: Post-Cursor refinement — production-ready// Each fix corresponds to a specific Cursor Cmd+K command// Cmd+K #1: "Convert to named export and update import paths to use// @company/ui barrel exports instead of direct @/components/ui/* paths"import { Card, CardContent, CardHeader } from'@company/ui';
import { Skeleton } from'@company/ui';
import { cn } from'@/lib/utils';
import { ArrowUpIcon, ArrowDownIcon, MinusIcon } from'lucide-react';
// Cmd+K #2: "Refine all prop types to use strict union types.// Match the MetricCardSpec interface in src/config/component-specs.ts"exportinterfaceMetricCardProps {
/** Metric name displayed above the value */
label: string;
/** Primary metric value — stringfor pre-formatted values, numberfor raw */
value: string | number;
/** Changefrom previous period as a percentage — positive or negative */
delta?: number;
/** Trend direction controls icon and semantic color */
trend?: 'up' | 'down' | 'neutral';
/** Controls padding and font sizes */
size?: 'sm' | 'md' | 'lg';
/** Renders skeleton placeholder during data fetch */
loading?: boolean;
/** AdditionalCSS classes merged with defaults */
className?: string;
}
// Cmd+K #3: "Replace all hardcoded Tailwind color classes with semantic tokens.// Reference the token list in tailwind.config.ts.// bg-white → bg-card, text-gray-* → text-muted-foreground,// text-green-* → text-success (or use a CSS variable),// text-red-* → text-destructive"const trendConfig = {
up: {
icon: ArrowUpIcon,
className: 'text-emerald-600 dark:text-emerald-400', // semantic pattern for success
label: 'Trending up',
},
down: {
icon: ArrowDownIcon,
className: 'text-destructive',
label: 'Trending down',
},
neutral: {
icon: MinusIcon,
className: 'text-muted-foreground',
label: 'No change',
},
};
const sizeConfig = {
sm: {
card: 'p-3',
value: 'text-xl font-bold',
label: 'text-xs',
delta: 'text-xs',
},
md: {
card: 'p-4',
value: 'text-2xl font-bold',
label: 'text-sm',
delta: 'text-sm',
},
lg: {
card: 'p-6',
value: 'text-3xl font-bold',
label: 'text-base',
delta: 'text-sm',
},
};
// Cmd+K #4: "Improve the loading state to use the same Card wrapper// so the skeleton matches the final component's dimensions exactly"exportfunctionMetricCard({
label,
value,
delta,
trend = 'neutral',
size = 'md',
loading = false,
className,
}: MetricCardProps) {
const sizes = sizeConfig[size];
if (loading) {
return (
<Card className={cn(sizes.card, className)}>
<CardHeader className="pb-2 pt-0 px-0">
<Skeleton className="h-3 w-20" />
</CardHeader>
<CardContent className="px-0 pb-0">
<Skeleton className="h-8 w-28 mb-2" />
<Skeleton className="h-3 w-16" />
</CardContent>
</Card>
);
}
const trend_ = trendConfig[trend];
constTrendIcon = trend_.icon;
return (
<Card className={cn(sizes.card, className)}>
<CardHeader className="pb-2 pt-0 px-0">
{/* text-muted-foreground — semantic token, adapts to dark mode */}
<p className={cn(sizes.label, 'text-muted-foreground')}>{label}</p>
</CardHeader>
<CardContent className="px-0 pb-0">
{/* text-card-foreground — semantic token */}
<p className={cn(sizes.value, 'text-card-foreground')}>
{typeof value === 'number' ? value.toLocaleString() : value}
</p>
{delta !== undefined && (
// Cmd+K #5: "Add aria-label to the trend indicator so screen readers// announce the direction and percentage change"
<div
className={cn('flex items-center gap-1 mt-1', trend_.className)}
aria-label={`${trend_.label}: ${Math.abs(delta)}% change`}
role="status"
>
<TrendIcon size={14} aria-hidden="true" />
<span className={sizes.delta}>
{delta > 0 ? '+' : ''}{delta}%
</span>
</div>
)}
</CardContent>
</Card>
);
}
MetricCard.displayName = 'MetricCard';
The Five Cursor Commands in Order
Command 1: Convert to named export and update import paths to barrel exports
Command 2: Refine prop types to strict union types matching existing interfaces
Command 3: Replace all hardcoded color classes with semantic tokens from tailwind.config.ts
Command 4: Improve loading state to match the final component's Card wrapper and dimensions
Command 5: Add ARIA attributes and role to all interactive and status elements
Production Insight
Running the five commands in sequence takes 8-10 minutes on a typical component.
Skipping command 3 (semantic tokens) is what caused the 18-component dark mode incident.
Skipping command 5 (ARIA) is what causes accessibility audit failures on 60% of generated components.
The commands are not suggestions — they are the quality baseline every component must meet.
Key Takeaway
The Cursor refinement phase is five targeted commands, not open-ended editing.
Each command fixes one consistent failure pattern that v0 produces on almost every output.
Running all five takes 8-10 minutes. Skipping any one creates a quality gate failure.
Scaling to 50 Components: The Batch Workflow
The batch workflow applies the two-phase process systematically across a full component library. The key insight is that specification, generation, and refinement are separate work modes — mixing them creates context-switching overhead that slows the process.
Block time in three stages: spec review (one session, all 50 specs reviewed and approved), generation (sequential v0 sessions, one component at a time, prompts derived from specs), and refinement (Cursor sessions applying the five commands to each generated output).
The bottleneck is not generation — it is the quality gate review. A component that takes 2 minutes to generate takes 8-10 minutes to refine and 5 minutes to review. Total workflow time per component is 15-17 minutes. For 50 components, that is approximately 12-14 hours of focused work spread across sessions.
scripts/component-batch-tracker.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#!/usr/bin/env bash
# component-batch-tracker.sh
# Track batch generation progress across a component library sprint.
# Run from the project root. Updates component-status.md with current state.
set -euo pipefail
SPEC_FILE="src/config/component-specs.ts"
COMPONENTS_DIR="src/components"
STATUS_FILE="component-status.md"
echo "# Component Generation Status"
echo "Generated: $(date)"
echo ""
# Count specs defined
SPEC_COUNT=$(grep -c 'const.*Spec: ComponentSpec'"$SPEC_FILE"2>/dev/null || echo 0)
echo "## Specs defined: $SPEC_COUNT"
echo ""
# Check which components exist as files
echo "## Component Status"
echo "| Component | File Exists | Has Test | Storybook Story | Dark Mode Checked |"
echo "|-----------|-------------|----------|-----------------|-------------------|"
# Extract component names from spec file
grep 'const.*Spec: ComponentSpec'"$SPEC_FILE" \
| sed "s/export const //" \
| sed "s/Spec: ComponentSpec.*//" \
| while read -r specName; do
# Convert camelCase spec name to PascalCase component name
componentName=$(echo "$specName" | sed 's/\([A-Z]\)/ \1/g' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) substr($i,2); print}' | tr -d ' ')
componentName=${componentName%Spec}
# Check file existence
fileExists="❌"if [ -f "$COMPONENTS_DIR/${componentName}.tsx" ]; then
fileExists="✅"
fi
# Check test file
hasTest="❌"if [ -f "$COMPONENTS_DIR/__tests__/${componentName}.test.tsx" ]; then
hasTest="✅"
fi
# CheckStorybook story
hasStory="❌"if [ -f "$COMPONENTS_DIR/${componentName}.stories.tsx" ]; then
hasStory="✅"
fi
echo "| ${componentName} | ${fileExists} | ${hasTest} | ${hasStory} | ☐ |"
done
echo ""
# Checkfor hardcoded colors across all component files
echo "## Design Token Compliance Audit"
echo "Components with hardcoded colors (must fix before merge):"
grep -rln 'bg-white\|bg-gray-\|text-gray-\|text-black\|border-gray-\|bg-blue-\|bg-red-\|text-green-' \
"$COMPONENTS_DIR"2>/dev/null \
| grep '\.tsx$' \
| grep -v '\.stories\.\|\.test\.' \
| while read -r f; do
count=$(grep -c 'bg-white\|bg-gray-\|text-gray-\|text-black\|border-gray-\|bg-blue-\|bg-red-\|text-green-'"$f" || echo 0)
echo " $f — $count instances"
done
echo ""
echo "Run 'grep -rn \"bg-white|text-gray\" src/components/'for specific lines."
Batch in Phases, Not in Parallel
Session 1: Write and review all specs — no generation until specs are approved
Session 2: Generate all components with v0 — one prompt per spec, save all raw output
Session 3: Apply the five Cursor commands to each component sequentially
Separating phases means each session has one cognitive mode — spec review, generation, or refinement
Production Insight
A developer who mixes generation and refinement in the same session averages 22 minutes per component.
The same developer working in separate phases averages 15 minutes per component.
The difference is context-switching: jumping between v0 browser, Cursor, and the quality checklist costs 7 minutes per component.
At 50 components, that is 5.8 hours of avoidable overhead.
Key Takeaway
Total workflow time is 15-17 minutes per component — not 3 minutes.
50 components require approximately 12-14 hours of focused work across multiple sessions.
Batch in phases: all specs first, all generation second, all refinement third, quality gates last.
Quality Gates: The Four Non-Negotiable Checks
Automation without quality gates multiplies technical debt. A component that takes 15 minutes to generate and refine takes 4 hours to debug in production if it ships with a broken dark mode, an accessibility violation, or a performance regression.
The four quality gates apply to every AI-generated component before it merges. They are not suggestions and they are not skippable when the sprint is tight — the sprint will be tighter after the production incident.
Gate 1: Design token compliance. Run the hardcoded color grep and fix every match. Toggle dark mode in the browser and visually inspect every state of the component.
Gate 2: Accessibility audit. Run axe-cli against the rendered component. Fix every violation before proceeding. Manually tab through the component with keyboard-only navigation.
Gate 3: Integration with real data. Mount the component with real API data, not the mock data from the Storybook story. Edge cases in real data expose type mismatches and empty state bugs that mock data never triggers.
Gate 4: Bundle impact check. Check that no unexpected dependencies were added. Anything over 5KB of additional bundle size requires justification.
scripts/quality-gates.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#!/usr/bin/env bash
# quality-gates.sh
# Run quality gates on a specific generated component before merging.
# Usage: ./scripts/quality-gates.sh src/components/MetricCard.tsx
#
# Prerequisites:
# - axe-cli: npm install -g @axe-core/cli
# - bundlesize: configured in package.json or .bundlesizerc
# - Dev server running on localhost:3000
# - Storybook running on localhost:6006
set -euo pipefail
COMPONENT_FILE="${1:-}"if [ -z "$COMPONENT_FILE" ]; then
echo "Usage: $0 <component-file-path>"
echo "Example: $0 src/components/MetricCard.tsx"
exit 1
fi
COMPONENT_NAME=$(basename "$COMPONENT_FILE" .tsx)
PASS=true
echo "=================================================="
echo " Quality Gates: $COMPONENT_NAME"
echo "=================================================="
echo ""
# ------------------------------------------------------------------
# GATE1: Design token compliance
# ------------------------------------------------------------------
echo "--- Gate 1: Design Token Compliance ---"HARDCODED=$(grep -c 'bg-white\|bg-gray-\|text-gray-\|text-black\|border-gray-\|bg-blue-\|bg-red-\|text-green-\|bg-yellow-'"$COMPONENT_FILE"2>/dev/null || echo 0)
if [ "$HARDCODED" -eq 0 ]; then
echo "✅ No hardcoded color classes found"else
echo "❌ FAIL: $HARDCODED hardcoded color class(es) found:"
grep -n 'bg-white\|bg-gray-\|text-gray-\|text-black\|border-gray-\|bg-blue-\|bg-red-\|text-green-'"$COMPONENT_FILE" || truePASS=false
fi
echo ""
echo "Manual check required:"
echo " [ ] Toggle dark mode in browser — inspect all component states visually"
echo " [ ] Check loading state in dark mode"
echo " [ ] Check error state in dark mode (if applicable)"
echo ""
# ------------------------------------------------------------------
# GATE2: Accessibility audit
# ------------------------------------------------------------------
echo "--- Gate 2: Accessibility ---"
# CheckifStorybook story exists
STORY_FILE="src/components/${COMPONENT_NAME}.stories.tsx"if [ ! -f "$STORY_FILE" ]; then
echo "⚠️ No Storybook story found at $STORY_FILE"
echo " Create a story and run: npx axe-cli http://localhost:6006/iframe.html?id=..."else
echo "✅ Storybook story found"
echo " Run: npx axe-cli http://localhost:6006/iframe.html?id=$(echo $COMPONENT_NAME | tr '[:upper:]''[:lower:]')-default --tags wcag2a,wcag2aa"
fi
# Static check: find onClick handlers without keyboard equivalents
ONKEY_MISSING=$(grep -c 'onClick'"$COMPONENT_FILE"2>/dev/null || echo 0)
IF_MISSING=$(grep -c 'onKeyDown\|onKeyUp\|role='"$COMPONENT_FILE"2>/dev/null || echo 0)
if [ "$ONKEYDOWN_MISSING" -gt 0 ] && [ "$IF_MISSING" -eq 0 ]; then
echo "⚠️ onClick handlers found without corresponding keyboard handlers or role attributes"
echo " Verify keyboard navigation works for all interactive elements"
fi
echo ""
echo "Manual check required:"
echo " [ ] Tab through entire component with keyboard only"
echo " [ ] Verify focus indicators are visible"
echo " [ ] Test with VoiceOver (macOS) or NVDA (Windows) if component is interactive"
echo ""
# ------------------------------------------------------------------
# GATE3: TypeScript type check
# ------------------------------------------------------------------
echo "--- Gate 3: TypeScript ---"
npx tsc --noEmit 2>&1 | grep -i "$COMPONENT_NAME" || echo "✅ No TypeScript errors for $COMPONENT_NAME"
echo ""
# ------------------------------------------------------------------
# GATE4: Bundleimpact (requires @next/bundle-analyzer configured)
# ------------------------------------------------------------------
echo "--- Gate 4: Bundle Impact ---"
echo "Check new imports added by this component:"
grep '^import'"$COMPONENT_FILE" \
| grep -v '@company/ui\|@/lib\|@/hooks\|react\|lucide-react\|next' \
| while read -r line; do
echo " ⚠️ External import — verify this library is already in package.json: $line"
done || echo "✅ No unexpected external imports"
echo ""
echo "Run ANALYZE=true npm run build to check full bundle impact if external imports were found."
echo ""
# ------------------------------------------------------------------
# Summary
# ------------------------------------------------------------------
echo "=================================================="if [ "$PASS" = true ]; then
echo " Automated checks: PASSED"else
echo " Automated checks: FAILED — fix issues above before merging"
fi
echo " Complete the manual checks above before marking PR ready."
echo "=================================================="
The Debt Multiplier Effect
One AI component shipped without dark mode testing created 18 broken components — the debt was multiplied because the same pattern was copied
One accessibility violation in a base component propagates to every consumer that uses it
Fixing a component post-merge takes 4x longer than reviewing it pre-merge — you need to find it, fix it, test it, and re-deploy
Quality gates are not slowing the workflow — they are what makes the workflow sustainable
Production Insight
The team that skipped quality gates to hit sprint velocity targets spent the next two sprints fixing the components they shipped.
The team that ran quality gates finished 10% fewer components in the sprint but had zero rework in the following sprints.
Speed without quality gates is a loan at 400% interest.
Key Takeaway
Four gates: design token compliance, accessibility, TypeScript, and bundle impact.
The manual dark mode visual check is not replaceable by automation — run it for every component.
Quality gates are what converts raw generation speed into sustainable delivery speed.
Preventing Component Sprawl
Component sprawl is the silent failure mode of AI-assisted generation. Without discipline, a library of 50 components becomes 80 components where 30 are slight variations of the same base. Each variant looks slightly different, is maintained separately, and fragments the design system's visual consistency.
The prevention check is simple: before opening v0, ask 'can this be a variant of an existing component?' If the answer is yes or maybe, extend the existing component instead of generating a new one.
Cursor is the right tool for variant extraction. After generating 5-10 components, use Cursor Chat with @codebase to identify similar components and consolidate them into a single configurable base.
src/components/StatusBadge.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// StatusBadge.tsx// Example of variant consolidation — three generated components merged into one.//// BEFORE consolidation, the library had:// ActiveBadge.tsx — green badge for active status// PendingBadge.tsx — yellow badge for pending status// ErrorBadge.tsx — red badge for error status//// Cursor Cmd+K: "Consolidate ActiveBadge, PendingBadge, and ErrorBadge// into a single StatusBadge component with a status prop// that controls color and icon via a config map."//// AFTER consolidation: one component, one maintenance point.import { Badge } from'@company/ui';
import { cn } from'@/lib/utils';
import {
CheckCircleIcon,
ClockIcon,
XCircleIcon,
MinusCircleIcon,
} from'lucide-react';
exporttypeStatusValue = 'active' | 'pending' | 'error' | 'inactive';
interfaceStatusConfig {
label: string;
icon: React.ElementType;
className: string;
ariaLabel: string;
}
// Single config map — change a status's appearance here, it updates everywhereconst STATUS_CONFIG: Record<StatusValue, StatusConfig> = {
active: {
label: 'Active',
icon: CheckCircleIcon,
// Use CSS variable-based semantic tokens — adapts to dark mode automatically
className: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-100',
ariaLabel: 'Status: Active',
},
pending: {
label: 'Pending',
icon: ClockIcon,
className: 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-100',
ariaLabel: 'Status: Pending',
},
error: {
label: 'Error',
icon: XCircleIcon,
className: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-100',
ariaLabel: 'Status: Error',
},
inactive: {
label: 'Inactive',
icon: MinusCircleIcon,
className: 'bg-muted text-muted-foreground',
ariaLabel: 'Status: Inactive',
},
};
exportinterfaceStatusBadgeProps {
/** Current entity status */
status: StatusValue;
/** Overridedefault label derived from status */
label?: string;
/** Show icon alongside label */
showIcon?: boolean;
/** Size variant */
size?: 'sm' | 'md';
/** AdditionalCSS classes */
className?: string;
}
exportfunctionStatusBadge({
status,
label,
showIcon = true,
size = 'md',
className,
}: StatusBadgeProps) {
const config = STATUS_CONFIG[status];
constIcon = config.icon;
const displayLabel = label ?? config.label;
return (
<Badge
variant="outline"
className={cn(
config.className,
size === 'sm' && 'text-xs px-1.5 py-0',
'border-0 font-medium',
className
)}
aria-label={config.ariaLabel}
>
{showIcon && (
<Icon
className={cn('mr-1', size === 'sm' ? 'h-3 w-3' : 'h-3.5 w-3.5')}
aria-hidden="true"
/>
)}
{displayLabel}
</Badge>
);
}
StatusBadge.displayName = 'StatusBadge';
The Variant-First Check
Run the sprawl check after every 10 components: use Cursor Chat to identify similar components in the library
A config map (STATUS_CONFIG) is the right pattern for variants that differ by data, not by structure
One component with 4 status variants has one test file, one Storybook story, one maintenance point
Four separate badge components have four test files, four stories, four maintenance points — and will drift apart
Production Insight
A library hit 90 components when the spec count was 60.
30 components were unauthorized variants generated ad-hoc by developers who found the spec process too slow.
Consolidating them with Cursor took a full sprint — longer than generating them had.
Rule: the spec approval process is the anti-sprawl gate. No spec, no generation.
Key Takeaway
Ask 'can this be a variant?' before every generation session.
Use a config map pattern to consolidate similar components into one configurable base.
Run a sprawl check after every 10 components — catch consolidation opportunities before they become maintenance debt.
● Production incidentPOST-MORTEMseverity: high
The Design Token Drift That Broke Dark Mode for 18 Components
Symptom
The dark mode toggle caused half the UI to render white text on white backgrounds. Users reported unreadable screens within minutes of the release going live. The support queue hit 200 tickets in 4 hours.
Assumption
The team assumed the AI-generated components used semantic Tailwind classes like bg-primary and text-foreground throughout. They expected the components to adapt to theme changes automatically, the same way manually written components did.
Root cause
v0 output contained 14 instances of hardcoded color values — bg-white, text-gray-900, border-gray-200 — mixed with semantic tokens. The components looked correct in light mode during development because hardcoded white and gray values match the light theme visually. The code review missed the hardcoded values because they blended in visually with the correct semantic tokens around them. Dark mode exposed the mismatch immediately.
Fix
Added a custom ESLint rule that flags any Tailwind class referencing a raw color value in the components directory. Ran a one-time sweep across all generated components. Manually replaced all hardcoded colors with the correct semantic tokens from tailwind.config.ts. Added dark mode screenshot comparison to the quality gate checklist — both light and dark mode must pass before any component merges.
Key lesson
Never trust AI output to use your design tokens correctly — v0 defaults to generic Tailwind classes because it does not know your configuration
Hardcoded colors pass visual review in light mode — the failure only surfaces in dark mode or with custom themes
Automate design system compliance checks with ESLint before any generated component reaches code review
Test every generated component in both light and dark mode as a mandatory quality gate step, not an afterthought
Production debug guideCommon symptoms when integrating AI-generated shadcn/ui components6 entries
Symptom · 01
Component renders correctly in light mode but breaks in dark mode
→
Fix
Search for hardcoded color classes: grep -rn 'bg-white\|text-gray\|bg-gray\|border-gray' src/components/ — every match is a design token that was not used. Replace each with the correct semantic token from your tailwind.config.ts.
Symptom · 02
TypeScript errors on props immediately after pasting v0 output
→
Fix
Check import paths first — v0 uses generic @/components/ui/* paths that may not match your project's barrel export structure. Then check that the prop types v0 generated match your actual data model types. v0 invents prop types that look correct but do not match existing interfaces.
Symptom · 03
Component works in isolation but breaks when nested inside a form or dialog
→
Fix
Inspect for duplicate Radix UI context providers. v0 often wraps components in Provider layers that are already provided by a parent component. Remove the duplicate Provider — the component should consume context from its nearest ancestor, not redeclare it.
Symptom · 04
Generated table component causes visible render delay with 100+ rows
→
Fix
Profile with React DevTools Profiler — AI-generated tables render every row on every state change. Wrap row components in React.memo, use useCallback for row handlers, and virtualize the list with @tanstack/react-virtual for datasets over 50 rows.
Symptom · 05
Accessibility audit fails on generated interactive component
→
Fix
v0 generates visually correct interactive elements but commonly omits ARIA roles, aria-label attributes, and keyboard event handlers. Run npx axe-cli http://localhost:3000/component-path to get a specific violation list. Use Cursor Cmd+K: 'Add appropriate ARIA roles, aria-label, and onKeyDown handlers to all interactive elements.'
Symptom · 06
Component bundle size increased by more than expected after integration
→
Fix
v0 sometimes imports heavy utility libraries (date-fns, lodash) for tasks that your project already handles. Run ANALYZE=true npm run build with @next/bundle-analyzer configured to identify the source. Replace with your project's existing utilities.
★ AI Component Quick Debug Cheat SheetFast diagnostics for the most common AI-generated component issues.
Hardcoded colors in generated component−
Immediate action
Search for raw Tailwind color classes across generated files
Remove any import that duplicates a library your project already uses. Replace lodash utility imports with your existing utility functions. Configure @next/bundle-analyzer in next.config.js if not already set up.
Accessibility audit failures on generated interactive components+
Immediate action
Run automated accessibility check against the running component
For every hardcoded color found, find its semantic equivalent in tailwind.config.ts and replace. Test by toggling dark mode in the browser after each replacement.
v0 vs. Cursor: Role Comparison
Capability
v0
Cursor
Initial component generation
Strong — produces complete styled components from structured prompts
Not designed for — use v0 for generation from scratch
Project contextualization
None — output is generic, does not know your codebase
Strong — @codebase context adapts output to your specific project
Design token compliance
Poor — defaults to hardcoded Tailwind classes without project config
Strong — Cmd+K replaces hardcoded colors with semantic tokens in one pass
Prop type refinement
Basic — generates broad types (string instead of union types)
Strong — aligns prop types with your existing interfaces via Cmd+K
Variant generation from base
Moderate — requires a separate prompt per variant
Strong — generates variants from the base component inline via Cmd+K
Accessibility improvements
Poor — generates visually correct but ARIA-incomplete components
Strong — adds ARIA attributes and keyboard handlers via targeted Cmd+K
Component consolidation (anti-sprawl)
Not applicable
Strong — Chat with @codebase identifies and merges similar components
Dark mode compliance
Poor — hardcoded colors break dark mode
Strong — token replacement fixes dark mode in one targeted command
Learning curve
Low — conversational prompt interface
Medium — requires familiarity with @codebase context and Cmd+K scoping
Key takeaways
1
Total workflow time is 15-17 minutes per component
2 minutes to generate, 8-10 to refine, 5 to quality-gate — not 3 minutes
2
Run the five-check candidacy evaluation before every generation session
two failing checks means build manually
3
Write all specs before generating any components
the spec phase catches sprawl and duplicates before any code is written
it is the check that catches the failure mode the production incident was caused by
6
Component sprawl is the silent failure mode
always ask 'can this be a variant?' before generating a new component
7
Quality gates convert raw generation speed into sustainable delivery speed
skip them and you are taking a loan at 400% interest
Common mistakes to avoid
6 patterns
×
Generating before evaluating candidacy
Symptom
Hours spent prompting v0 for a complex interactive component that produces four inconsistent outputs, none of which are close to the design requirement. The team abandons the generated code and builds manually anyway — wasting the generation time.
Fix
Run the five-check candidate evaluation before opening v0. Two or more failing checks means build manually. The check takes 2 minutes and prevents multi-hour generation sessions on poor-fit components.
×
Writing specs after generating instead of before
Symptom
Library grows to 80 components where 30 are slight variations of the same base card or badge. Consolidation takes a full sprint to undo.
Fix
Write all specs before generating any components. Run a duplicate check across the spec list — ask 'can this be a variant of an existing spec?' before adding each one. No spec means no generation session.
×
Using the generic prompt instead of the structured template
Symptom
v0 produces a vague component with hardcoded colors, no variants, no states, and TypeScript types that do not match the data model. Rework takes longer than building manually.
Fix
Use the five-section prompt template (core function, props, variants, states, style rules) derived directly from the ComponentSpec. The structured prompt reduces rework from 45 minutes to 8-10 minutes.
×
Skipping the dark mode visual check in the quality gate
Symptom
Components look correct in development (light mode). Dark mode breaks 18 components simultaneously in production because hardcoded color classes were missed in code review.
Fix
Dark mode visual inspection is a mandatory quality gate step — not optional when the sprint is tight. Toggle dark mode in the browser and inspect every component state before marking the PR ready.
×
Treating the five Cursor commands as optional
Symptom
Teams apply one or two Cursor commands and skip the rest to save time. The skipped commands (semantic tokens, ARIA attributes) produce the exact failures the quality gate is designed to catch.
Fix
All five Cursor commands are the quality baseline for every component. They take 8-10 minutes total. Skipping any one does not save time — it defers the fix to post-merge where it costs 4x more.
×
Using ANALYZE=true npm run build without configuring @next/bundle-analyzer first
Symptom
ANALYZE=true has no effect because the plugin was never configured. Developers assume the bundle check passed when it was silently skipped.
Fix
Configure @next/bundle-analyzer in next.config.js before relying on it. Add the ANALYZE=true npm run build command to the quality gate checklist only after verifying the plugin is active. Alternatively, use bundlesize with a .bundlesizerc configuration file for a simpler setup.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
How would you design a system to generate UI components at scale while m...
Q02SENIOR
What are the risks of using AI to generate code at scale, and how do you...
Q03SENIOR
How would you enforce design token compliance across AI-generated compon...
Q04SENIOR
When would you not use AI to generate a component, and how do you make t...
Q01 of 04SENIOR
How would you design a system to generate UI components at scale while maintaining design system compliance?
ANSWER
Three layers are required. First, a specification system that defines every component's requirements before generation begins — props, variants, states, allowed primitives, and explicitly what should not be generated (business logic). The spec is the single source of truth and the anti-sprawl gate: no spec means no generation session.
Second, a generation pipeline using v0 with structured prompts derived from the spec. The prompts include explicit style rules — 'semantic Tailwind tokens only, no hardcoded color classes' — to reduce the most common AI output failures. The five-check candidacy evaluation runs before any prompt is written to filter out poor-fit components.
Third, a quality gate process with four mandatory checks: design token compliance (hardcoded color grep plus dark mode visual check), accessibility (axe-cli plus keyboard navigation test), TypeScript (tsc --noEmit), and bundle impact (import audit). No component merges without passing all four. The pipeline produces consistent output because the spec, the prompt template, and the quality gates are all standardized — not because the AI is reliable.
Q02 of 04SENIOR
What are the risks of using AI to generate code at scale, and how do you mitigate each one?
ANSWER
Three primary risks with specific mitigations.
Design system drift: AI does not know your design tokens and defaults to hardcoded color classes. Mitigation: structured prompts with explicit semantic token instructions, mandatory post-generation grep for hardcoded colors, and dark mode visual inspection as a quality gate step.
Component sprawl: without a spec-first process, developers generate ad-hoc variations that fragment the design system. Mitigation: require an approved spec before any generation session. Run a sprawl check after every 10 components using Cursor Chat to identify consolidation opportunities.
Accessibility debt: AI generates visually correct interactive components that lack ARIA attributes and keyboard handlers. Mitigation: ARIA addition is one of five mandatory Cursor refinement commands, and axe-cli runs as part of the quality gate before merge.
The meta-rule: AI accelerates the scaffolding phase. Engineering judgment determines what actually ships. The workflow's value comes from the specification system and quality gates, not from trusting the AI output.
Q03 of 04SENIOR
How would you enforce design token compliance across AI-generated components in a large codebase?
ANSWER
Three layers of enforcement.
Prevention at generation: structured v0 prompts include explicit style rules listing forbidden classes (bg-white, bg-gray-, text-gray-) and required semantic alternatives (bg-background, text-foreground, text-muted-foreground). This reduces violations before the code exists.
Detection at refinement: the first Cursor Cmd+K command after pasting v0 output is 'replace all hardcoded Tailwind color classes with semantic tokens from tailwind.config.ts.' The hardcoded color grep runs as the first quality gate check.
Enforcement at merge: a custom ESLint rule in the CI pipeline flags any component file containing raw Tailwind color classes. The PR cannot merge with ESLint failures. This makes compliance a structural requirement, not a review-dependent one.
The three layers are additive — each catches what the previous missed. Relying on any single layer produces the dark mode incident.
Q04 of 04SENIOR
When would you not use AI to generate a component, and how do you make that decision quickly?
ANSWER
Use the five-check candidacy evaluation. Ask whether the component is presentational (accepts data via props, no internal state), follows a standard pattern (similar components exist in shadcn/ui or common design systems), has standard accessibility requirements (not custom drag-and-drop or complex focus management), contains no embedded business logic, and is decomposable into two or three shadcn/ui primitives.
If two or more answers are no, build manually. The evaluation takes 2 minutes. It prevents spending 3 hours prompting v0 for a component that produces inconsistent outputs and ultimately gets built manually anyway.
Good candidates: cards, badges, alerts, form field wrappers, stat displays, navigation items. Poor candidates: drag-and-drop interfaces, rich text editors, custom date pickers, components with complex gesture handling or domain-specific business logic baked in.
01
How would you design a system to generate UI components at scale while maintaining design system compliance?
SENIOR
02
What are the risks of using AI to generate code at scale, and how do you mitigate each one?
SENIOR
03
How would you enforce design token compliance across AI-generated components in a large codebase?
SENIOR
04
When would you not use AI to generate a component, and how do you make that decision quickly?
SENIOR
FAQ · 4 QUESTIONS
Frequently Asked Questions
01
Is PPR production-ready in Next.js 15?
PPR is available as an experimental feature in Next.js 15. It requires the experimental.ppr flag in next.config.ts. The API is stable but the underlying implementation may change in future releases. Vercel supports PPR in production on their platform. Self-hosted deployments require careful testing of the edge runtime compatibility.
Was this helpful?
02
Can I use PPR with Server Components that fetch data?
Yes. PPR works with async Server Components. The key requirement is that dynamic data fetches must be inside Suspense boundaries. Data fetched outside Suspense is considered part of the static shell and must be available at build time. If a Server Component fetches dynamic data outside Suspense, Next.js will force the entire route to dynamic rendering.
Was this helpful?
03
How does PPR interact with ISR (Incremental Static Regeneration)?
PPR and ISR are complementary. The static shell portion of a PPR page can use ISR revalidation to regenerate periodically. The dynamic holes always resolve at request time regardless of ISR settings. You can set revalidate for the static shell while keeping dynamic content fresh per-request.
Was this helpful?
04
Does PPR work with Edge Runtime?
PPR is designed for Edge Runtime. The static shell is served from CDN edge nodes. Dynamic hole resolution executes at the edge, minimizing latency to the user. This is a key advantage over traditional SSR which may execute in a centralized server region.