Mid-level 7 min · April 14, 2026

shadcn Dark Mode Broken by AI-Generated Hardcoded Colors

Dark mode unreadable from AI-generated shadcn components? Prevent design token drift with automated compliance checks and avoid white-on-white bugs..

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Every example here is drawn from a real system.

Follow
Production
production tested
May 24, 2026
last updated
1,510
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Combine v0.dev (generation + Design Mode) with Cursor (Agent mode + .cursorrules) to scaffold shadcn/ui components in minutes instead of hours
  • Biggest risk: AI defaults to hardcoded Tailwind classes that break dark mode and custom themes — enforce semantic tokens at every step
  • Treat every AI output as a first draft, not a finished component
✦ Definition~90s read
What is shadcn Dark Mode Broken by AI-Generated Hardcoded Colors?

shadcn/ui is a collection of React components built on top of Radix UI primitives and styled with Tailwind CSS. Unlike traditional component libraries that ship precompiled CSS with hardcoded color values, shadcn components use CSS custom properties (CSS variables) for theming.

Think of it like a factory assembly line.

This design choice is what enables seamless dark mode toggling: when you switch themes, Tailwind's dark: variant or a class-based toggle updates the CSS variable values, and every component re-renders with the new colors automatically. The entire system depends on colors being defined as variables like --primary or --background rather than literal hex codes like #3b82f6.

When AI code generators like v0.dev or Cursor produce shadcn components, they frequently bypass this variable system and inject hardcoded Tailwind utility classes with literal color values (e.g., bg-blue-500 or text-gray-900). This breaks dark mode because those utilities don't reference the CSS variables that change with theme toggling.

The result is a component that looks correct in light mode but retains its light-mode colors when the user switches to dark mode — a common failure point that's invisible during initial development but immediately obvious in production.

The fix requires enforcing a strict specification: every color in every component must use CSS variable-based Tailwind classes (like bg-background or text-foreground) or custom utility classes that map to theme variables. For a project with 50+ components, this means building a specification system that AI tools can follow, combined with automated quality gates — typically a linting rule or a visual regression test — that rejects any component containing hardcoded color values.

Without this discipline, AI-generated shadcn components are effectively single-theme components that will fail in production environments where dark mode is expected.

Plain-English First

Think of it like a factory assembly line. v0.dev is the machine that stamps out the raw part from a blueprint — and lets you tweak it visually in Design Mode before it leaves the press. Cursor is the engineer who reshapes and wires that part to fit the specific machine it's going into, using Agent mode and project rules to handle structural adaptation, not cosmetic touch-ups. Neither alone produces a finished component. Together they scale production from one component per hour to eight or more.

Building shadcn components by hand for a 50-component design system is slow and error-prone. AI generation promises speed, but it consistently breaks dark mode, misaligns with your theme tokens, and creates inconsistent code across dozens of files. This article walks you through a two-tool workflow—v0.dev for rapid generation and Cursor for surgical customization—so you can ship scalable, dark-mode-safe shadcn components without rewriting everything from scratch.

Why AI-Generated shadcn Components Break Dark Mode

AI code generators often produce shadcn components with hardcoded color values like #ffffff or bg-white instead of leveraging CSS variables such as bg-background or text-foreground. This bypasses the theming system that shadcn/ui relies on to toggle between light and dark modes. The result: components that look correct in one mode but become unreadable or visually broken when the user switches themes.

When you generate a shadcn component via an AI tool, the model typically outputs inline Tailwind classes or raw hex codes based on its training data. It does not understand that shadcn's architecture uses CSS custom properties defined in globals.css — variables like --background, --foreground, --card, etc. — which are dynamically swapped by a dark-mode class on <html>. Hardcoded colors ignore these variables, so they never respond to theme changes. This is a structural mismatch, not a cosmetic bug.

Use AI generation for shadcn components only when you explicitly instruct the model to reference theme variables (e.g., "use bg-background instead of bg-white") and then manually audit every color-related class. In production, this matters because dark mode is not optional — it's an accessibility and user preference requirement. A single hardcoded color in a critical component (like a modal or toast) can make your app unusable for dark-mode users, eroding trust and increasing support tickets.

AI Doesn't Know Your Theme
AI models treat shadcn as a generic Tailwind project — they don't understand that shadcn's CSS variables are the only way to support dark mode correctly.
Production Insight
A team shipped a dashboard with AI-generated charts that used fill="#1f2937" — the chart was invisible in dark mode because the fill matched the background.
Symptom: users reported "missing charts" only at night; QA had only tested in light mode.
Rule: never trust AI-generated color values in a themed component — always replace with CSS variable references.
Key Takeaway
AI-generated shadcn components will hardcode colors unless explicitly told not to.
Always replace bg-white, text-black, and hex codes with shadcn's CSS variable classes.
Audit every generated component for theme-variable usage before merging to production.

The Two-Tool Workflow: v0.dev and Cursor

This workflow uses each tool for the phase where it excels. Trying to do everything in one tool produces worse results and slower output.

v0.dev handles initial generation. It translates structured text prompts into functional React components using shadcn/ui primitives and Tailwind CSS. In 2026 it also offers Design Mode — a visual editor that lets you tweak layout, spacing, and color directly in the interface before exporting code. This removes a category of small fixes that previously required a Cursor round-trip.

Cursor handles contextualization. Its AI features — Chat with @codebase context, Agent and Composer mode for multi-file autonomous edits, inline Cmd+K transformations, and .cursorrules for project-wide rule enforcement — adapt generic v0.dev output to your project's design tokens, existing hooks, type definitions, and coding conventions.

The developer's role is quality control. You write the spec, review the generated scaffold, direct the refactoring, and sign off before merge. The AI handles the mechanical labor; you handle the judgment calls.

src/lib/component-pipeline.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
// Conceptual pipeline — illustrates the workflow stages
// v0.dev and Cursor do not expose programmatic APIs; this is a process diagram in code form

interface ComponentSpec {
  name: string;          // PascalCase: UserAvatar, MetricCard, DataTable
  description: string;   // One sentence: what it does, not how it looks
  props: PropSpec[];
  variants: VariantSpec[];
  states: string[];      // Always include: loading, error, empty
  isServerComponent: boolean; // React 19: explicit decision required
}

interface PipelineStage {
  tool: 'v0.dev' | 'cursor' | 'developer';
  input: string;
  output: string;
  qualityCheck: string;
}

const pipeline: PipelineStage[] = [
  {
    tool: 'developer',
    input: 'Product requirement or design file',
    output: 'ComponentSpec — structured definition of props, variants, states',
    qualityCheck: 'Does the spec describe one cohesive component, or should it be split?',
  },
  {
    tool: 'v0.dev',
    input: 'ComponentSpec converted to a structured prompt',
    output: 'React component scaffold with Tailwind classes and TypeScript types',
    qualityCheck: 'Does it render? Are hardcoded colors present? Check Design Mode for layout issues.',
  },
  {
    tool: 'cursor',
    input: 'v0.dev scaffold pasted into project',
    output: 'Project-native component using design tokens, existing hooks, and proper types',
    qualityCheck: 'Does tsc --noEmit pass? Does it render correctly in light and dark mode?',
  },
  {
    tool: 'developer',
    input: 'Cursor-adapted component',
    output: 'Reviewed, tested, and merged component with Storybook story',
    qualityCheck: 'All four quality gates passed. PR approved.',
  },
];
The Draft-Refine Mental Model
  • v0.dev output is a first draft — assume 30 to 50 percent of it needs modification even after Design Mode adjustments.
  • Cursor is the structural adaptation tool — it aligns generic output to project-specific context via Agent mode and .cursorrules.
  • The developer is the quality gate — no AI output ships without human review of every line.
  • Speed comes from repeating the loop efficiently, not from skipping review steps.
Production Insight
v0.dev does not know your project's token definitions, existing hooks, or type interfaces. It produces a plausible generic implementation. Cursor's job is to replace plausible with correct.
Key Takeaway
Two tools with distinct roles outperform one tool used for everything. v0.dev generates; Cursor contextualizes; the developer ships.

Phase 1: Generation with v0.dev

v0.dev translates structured UI descriptions into functional React components. Prompt quality directly determines output quality — a vague prompt produces a vague component that requires extensive rework.

A strong v0.dev prompt includes: the component name, one sentence describing its core function, the key props it accepts, the variants it supports, the states it must handle, the specific shadcn/ui primitives to use, and explicit token requirements.

After initial generation, use Design Mode to fix obvious visual issues — padding, spacing, color, layout — before exporting. This takes two to three minutes and removes a round of Cursor work.

v0.dev output is complete enough to run but not complete enough to ship. It will have hardcoded colors, generic types, and no connection to your project's hooks or utilities. That is expected. That is what Phase 2 addresses.

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 with the following requirements:

Core function: {ONE_SENTENCE — what it does, not how it looks}

Props:
- {PROP_NAME}: {TYPE} — {ONE_LINE_DESCRIPTION}
- {PROP_NAME}: {TYPE} — {ONE_LINE_DESCRIPTION}
- {PROP_NAME}: {TYPE} (optional) — {ONE_LINE_DESCRIPTION}

Variants:
- {VARIANT_NAME}: {OPTION_1} | {OPTION_2} | {OPTION_3}
- size: sm | md | lg

States to handle:
- loading: show a Skeleton placeholder
- error: show an inline error message with retry option
- empty: show an empty state with a descriptive message

Technical requirements:
- Use these shadcn/ui primitives: {LIST — e.g., Card, Button, Badge, Skeleton}
- Style exclusively with semantic color tokens: bg-primary, text-muted-foreground,
  border-border, text-foreground, bg-muted (Tailwind v4 @theme tokens — no hardcoded scales)
- All props must have explicit TypeScript types — no any
- Export the component as a named export
- Include a Props interface above the component definition
- {IF CLIENT COMPONENT}: Add 'use client' directive at top
- {IF SERVER COMPONENT}: No useState or useEffect — accept data as props
Prompt Specificity Saves Refactoring Time
A 10-line prompt that names exact primitives, variant options, and token requirements saves 15 to 20 minutes of Cursor refactoring. The investment is in the spec, not in post-generation cleanup. The more specific the prompt, the closer v0.dev gets to project-ready on the first attempt.
Production Insight
Generic prompts produce generic components that need 80 percent rework. Specific prompts with named variants, states, and token requirements reduce integration time to minutes. Spend the extra two minutes on the prompt.
Key Takeaway
Prompt quality is the primary lever in this workflow. Vague prompts cost more time in Cursor than they save in v0.dev.

Phase 2: Customization with Cursor

Cursor transforms the v0.dev scaffold into a project-native component through three steps: contextualize, refactor, and validate.

Step 1 — Contextualize. Paste the v0.dev output into your project at the correct file path. Open Cursor Chat and provide context using @codebase, or explicitly reference key files: @src/styles/globals.css (for Tailwind v4 @theme tokens), @src/types/user.ts, @src/hooks/useDataTable.ts. The more precise the context, the better the adaptation.

Step 2 — Refactor. Use Cmd+K for inline targeted changes or Agent mode for multi-step transformations. Common refactoring commands are shown in the code block below. If you have a .cursorrules file, it enforces project conventions automatically — semantic token usage, import patterns, naming conventions — reducing the number of manual corrections needed.

Step 3 — Validate. Run tsc --noEmit. Render the component in both light and dark mode. Check the output of the hardcoded color audit script. Do not proceed to the quality gates until these three checks pass.

src/components/DataTable.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
// Cursor refactoring sequence — use these as Cmd+K prompts or Agent mode instructions
// Run them in order for consistent results

// Step 1: Token compliance
// "Replace all hardcoded Tailwind color classes with semantic tokens.
//  Reference the @theme block in src/styles/globals.css for available token names.
//  Do not use bg-white, text-gray-*, bg-gray-*, or any raw color scale."

// Step 2: Hook integration
// "Replace the local useState and useEffect data fetching logic with our
//  custom useDataTable hook from @/hooks/useDataTable.
//  The hook accepts: { data, pageSize, sortable, filterable }.
//  It returns: { rows, pagination, sort, filter, isLoading, error }."

// Step 3: Type alignment
// "Replace the generic Row type with the TableRow interface from @/types/table.ts.
//  Replace any with the specific types from that file.
//  Run tsc --noEmit after changes to confirm no type errors."

// Step 4: State handling
// "Add loading state using our Skeleton component from @/components/ui/skeleton.
//  Add error state using our Alert component with a retry button.
//  Add empty state with an EmptyState component showing the emptyMessage prop."

// Step 5: Server/client boundary (React 19)
// "Evaluate whether this component requires 'use client'.
//  If it only receives data via props and has no browser-only APIs,
//  remove 'use client' and convert to a server component."

// Step 6: Variant extraction
// "Using the base component above as the default variant,
//  create a compact variant that reduces row padding to py-1
//  and hides the checkbox selection column.
//  Export it as DataTableCompact from the same file."

// After all steps, the component should:
// - Import from project barrel exports, not direct component paths
// - Use semantic color tokens exclusively
// - Delegate state management to useDataTable
// - Handle loading, error, and empty states
// - Pass tsc --noEmit with zero errors
// - Render correctly in light and dark mode
Context Window Limits in Cursor
  • Cursor's @codebase context has limits — large projects will not fit in a single context window.
  • Reference files explicitly rather than relying on @codebase scans: @src/styles/globals.css, @src/types/user.ts, @src/hooks/useDataTable.ts.
  • A well-configured .cursorrules file reduces context dependency because project rules are applied automatically.
  • Agent mode handles multi-file refactoring better than Chat — use it for changes that touch more than two files.
Production Insight
Explicit file references plus a .cursorrules file produce more consistent results than broad @codebase scans. The .cursorrules file is the single highest-leverage configuration in this workflow — it encodes your design system in a form that Cursor enforces automatically.
Key Takeaway
Cursor adapts generic output to your project. The .cursorrules file is the enforcement mechanism. Without it, every component requires manual correction of the same issues.

Scaling to 50+ Components: The Specification System

Generating one component is a technique. Generating fifty consistently is a system. The difference is the Component Specification Document.

Before generating any component, define every component in a structured spec. For each component: name, one-sentence description, key props (three to five), variant options, required states, and whether it is a server or client component. This document becomes your prompt source and your living documentation.

The batch process is sequential and repeatable: spec → prompt → v0.dev generation → Design Mode review → Cursor refactor → quality gate → Storybook story → merge. Each component follows the same pipeline. Variation in output quality comes from variation in spec quality — not from the tools.

In our six-hour session generating 52 components, two engineers worked in parallel on separate component groups. One handled data display components (tables, charts, stat cards); the other handled form inputs and navigation. Parallel execution is possible because each component is self-contained and the pipeline is the same for both.

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
// Component Specification Schema
// Every component is defined here before generation begins
// This file is reviewed once; the generated component is reviewed once
// Both together take less time than manual component creation

interface PropSpec {
  name: string;
  type: string;
  required: boolean;
  description: string;
  defaultValue?: string;
}

interface VariantSpec {
  name: string;      // e.g., "size", "status", "density"
  options: string[]; // e.g., ["sm", "md", "lg"]
  default: string;
}

interface ComponentSpec {
  name: string;              // PascalCase
  description: string;       // One sentence — what it does
  props: PropSpec[];
  variants: VariantSpec[];
  states: string[];          // loading, error, empty — always all three
  shadcnPrimitives: string[]; // Exact primitives to reference in the prompt
  isServerComponent: boolean; // React 19: explicit decision before generation
  storybook: boolean;        // Always true — every component gets a story
}

// Example specs
const componentSpecs: ComponentSpec[] = [
  {
    name: 'DataTable',
    description: 'Sortable, filterable table with pagination and row selection',
    props: [
      { name: 'data', type: 'T[]', required: true, description: 'Array of row data objects' },
      { name: 'columns', type: 'ColumnDef<T>[]', required: true, description: 'Column configuration array' },
      { name: 'onRowSelect', type: '(rows: T[]) => void', required: false, description: 'Callback fired when row selection changes' },
      { name: 'emptyMessage', type: 'string', required: false, defaultValue: 'No results found', description: 'Message shown in empty state' },
    ],
    variants: [
      { name: 'density', options: ['compact', 'default', 'comfortable'], default: 'default' },
    ],
    states: ['loading', 'error', 'empty'],
    shadcnPrimitives: ['Table', 'Checkbox', 'Button', 'Skeleton', 'Alert'],
    isServerComponent: false, // Requires interactivity for sorting and selection
    storybook: true,
  },
  {
    name: 'MetricCard',
    description: 'Displays a single KPI metric with label, value, trend indicator, and comparison period',
    props: [
      { name: 'label', type: 'string', required: true, description: 'Metric name' },
      { name: 'value', type: 'string | number', required: true, description: 'Current metric value' },
      { name: 'trend', type: "'up' | 'down' | 'neutral'", required: false, description: 'Trend direction vs comparison period' },
      { name: 'trendValue', type: 'string', required: false, description: 'e.g., "+12.4%" — shown next to trend indicator' },
    ],
    variants: [
      { name: 'size', options: ['sm', 'md', 'lg'], default: 'md' },
    ],
    states: ['loading', 'error', 'empty'],
    shadcnPrimitives: ['Card', 'CardHeader', 'CardContent', 'Skeleton'],
    isServerComponent: true, // Display only — no interactivity required
    storybook: true,
  },
];
Specification-First Generation
  • A structured spec produces consistent components across the entire library because each prompt follows the same pattern.
  • Without specs, each generated component drifts toward a different pattern depending on how the prompt was written.
  • Specs serve as living documentation — they answer 'why does this component have these props?' without reading the implementation.
  • The spec review is the cheapest review in the pipeline. Catch structural problems here, not after generation.
Production Insight
We reviewed the spec document as a team before generating a single component. That 30-minute review caught four components that should have been variants of existing components, saving two hours of generation and merging work.
Key Takeaway
Consistency at scale comes from the specification system, not from careful individual prompting. Write specs before you open v0.dev.

Quality Gates: The Non-Negotiable Checkpoint

Automation without quality gates multiplies technical debt at the same rate it accelerates production. Each of the four gates targets a distinct failure mode that AI generation introduces.

Gate 1 — Visual regression. Render the component in Storybook across all variants and all states (loading, error, empty, populated). Check both light and dark mode. Screenshot comparison catches layout breaks that look fine in isolation but break in composition.

Gate 2 — Accessibility audit. Run axe-core against the component in the browser or Storybook. AI-generated components miss ARIA labels, keyboard navigation, and focus management at a high rate. This gate is not optional — it is a legal requirement in many jurisdictions.

Gate 3 — Integration test with real data. Mock data hides edge cases that production data exposes: long strings, null values, empty arrays, deeply nested objects. Connect the component to your actual API or a fixture that mirrors production data shape.

Gate 4 — Bundle size check. AI sometimes suggests heavy dependencies for problems that have lightweight solutions. A generated table component should not pull in a full charting library. Measure the bundle impact of each component before merge.

For simple presentational components (cards, badges, alerts), all four gates take eight to ten minutes. For complex interactive components (data tables, multi-step forms), they take twenty to thirty minutes. That time is not optional — it is the price of sustainable speed.

scripts/quality-gates.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
// Quality gate runner — run before any generated component is merged
// Requires: Storybook running, dev server running, tsc available

interface QualityReport {
  componentName: string;
  passed: boolean;
  failures: string[];
  warnings: string[];
}

async function runQualityGates(componentPath: string, componentName: string): Promise<QualityReport> {
  const report: QualityReport = {
    componentName,
    passed: true,
    failures: [],
    warnings: [],
  };

  console.log(`\nRunning quality gates for ${componentName}...`);

  // Gate 1: TypeScript check
  // Run before visual checks — type errors indicate structural problems
  const typeCheckPassed = await runCommand(`npx tsc --noEmit --strict ${componentPath}`);
  if (!typeCheckPassed) {
    report.failures.push('Gate 1 failed: TypeScript type errors detected. Run tsc --noEmit to see full output.');
  }

  // Gate 2: Visual regression — requires Storybook
  // Checks both light and dark mode renders for all variants
  const visualPassed = await runVisualRegression(componentName);
  if (!visualPassed) {
    report.failures.push('Gate 2 failed: Visual regression detected. Check Storybook screenshots for diff.');
  }

  // Gate 3: Accessibility audit — requires dev server
  // Target the Storybook story URL for the component
  const storybookUrl = `http://localhost:6006/iframe.html?id=${componentName.toLowerCase()}--default`;
  const a11yViolations = await runCommand(
    `npx @axe-core/cli "${storybookUrl}" --tags wcag2a,wcag2aa --exit`
  );
  if (!a11yViolations) {
    report.failures.push('Gate 3 failed: Accessibility violations found. Run axe-core manually to see full report.');
  }

  // Gate 4: Hardcoded color audit
  const colorAuditPassed = await runCommand(
    `! grep -rn -e 'bg-white' -e 'bg-black' -e 'text-gray-[0-9]' -e 'bg-gray-[0-9]' ${componentPath}`
  );
  if (!colorAuditPassed) {
    report.failures.push('Gate 4 failed: Hardcoded color classes detected. Replace with semantic tokens.');
  }

  // Gate 5: Bundle size impact
  // Warning if over 5KB, failure if over 20KB for a single component
  const bundleImpactKB = await measureBundleImpact(componentPath);
  if (bundleImpactKB > 20) {
    report.failures.push(`Gate 5 failed: Component adds ${bundleImpactKB}KB to bundle. Investigate imports.`);
  } else if (bundleImpactKB > 5) {
    report.warnings.push(`Gate 5 warning: Component adds ${bundleImpactKB}KB. Review imports for tree-shaking opportunities.`);
  }

  report.passed = report.failures.length === 0;

  if (report.passed) {
    console.log(`✓ ${componentName} passed all quality gates`);
    if (report.warnings.length > 0) {
      console.log(`  Warnings: ${report.warnings.join(', ')}`);
    }
  } else {
    console.log(`✗ ${componentName} failed ${report.failures.length} gate(s):\n  ${report.failures.join('\n  ')}`);
  }

  return report;
}
The Debt Multiplier Effect
  • In our experience, one unreviewed AI component introduces three to five downstream bugs — type mismatches, token drift, missing keyboard handlers, or edge case render failures.
  • Fixing a component post-merge takes four times longer than reviewing it pre-merge because downstream code has already been written against the broken implementation.
  • Quality gates are not a slowdown — they are the mechanism that makes the speed sustainable.
Production Insight
We skipped the accessibility gate on six components in sprint two to hit a deadline. All six required rework in the next sprint. The gate would have taken forty minutes. The rework took three hours.
Key Takeaway
The four gates — TypeScript, visual, accessibility, token compliance — plus bundle check are non-negotiable. Sustainable speed requires review. Raw speed without review is just debt accrual.

Version Control and Team Workflow at Scale

Generating 50+ components creates a version control and review workflow problem. Without a clear branching and commit strategy, the PR queue becomes unmanageable and review quality drops.

We used a component-group branching strategy: one feature branch per logical group of components (data-display, form-inputs, navigation, feedback). Each branch contained six to ten related components. This kept PR diffs reviewable and allowed parallel work without merge conflicts.

Commit strategy within each branch: one commit per component, with a consistent message format. This makes bisecting straightforward if a component introduces a regression.

Review strategy: the author runs all quality gates locally before opening the PR. The reviewer checks only that the gates passed (via CI output) and does a spot-check on one component's light and dark mode rendering. With quality gates in CI, the reviewer is not re-checking mechanical compliance — they are checking judgment calls.

scripts/component-workflow.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
#!/bin/bash
# Component generation workflow — run these commands in sequence
# Assumes: main branch is clean, Storybook is configured

COMPONENT_NAME=$1
GROUP=$2 # e.g., data-display, form-inputs, navigation

if [ -z "$COMPONENT_NAME" ] || [ -z "$GROUP" ]; then
  echo "Usage: ./component-workflow.sh ComponentName group-name"
  exit 1
fi

# Step 1: Ensure you are on the correct feature branch
BRANCH="feat/components-${GROUP}"
git checkout "$BRANCH" 2>/dev/null || git checkout -b "$BRANCH"

echo "Branch: $BRANCH"
echo "Ready to generate: $COMPONENT_NAME"
echo ""
echo "Workflow:"
echo "1. Open v0.dev — paste spec prompt — review in Design Mode — copy output"
echo "2. Create file: src/components/${COMPONENT_NAME}.tsx"
echo "3. Paste v0.dev output"
echo "4. Open Cursor — run refactoring sequence (see Phase 2 prompts)"
echo "5. Run type check:"
echo "   npx tsc --noEmit"
echo "6. Run color audit:"
echo "   grep -rn -e 'bg-white' -e 'text-gray-[0-9]' src/components/${COMPONENT_NAME}.tsx"
echo "7. Generate Storybook story with Cursor Agent mode"
echo "8. Verify in Storybook: light mode, dark mode, all variants, all states"
echo "9. Commit:"
echo "   git add src/components/${COMPONENT_NAME}.tsx src/components/${COMPONENT_NAME}.stories.tsx"
echo "   git commit -m 'feat(components): add ${COMPONENT_NAME} — ${GROUP} group'"
echo "10. Next component — repeat from step 1"
Parallel Generation Strategy
Two engineers can work in parallel on separate component groups without merge conflicts as long as each works on a separate branch. Merge branches into main in sequence, not simultaneously. The spec document prevents overlap — if a component is in the spec, it belongs to exactly one group and one engineer.
Key Takeaway
A clear branching strategy (one branch per component group) and a commit-per-component convention makes 50+ components reviewable and bisectable. Without it, the PR becomes a wall of diffs nobody reviews carefully.

Common Failure Modes at Scale

After generating components in volume, specific failure patterns become predictable. These are the five most common issues we hit and have seen other teams hit.

The Sprawl Prevention Rule
  • A single Button with five variants is easier to maintain than five separate button components.
  • A single Card with size and density variants covers most display use cases without proliferation.
  • Track your component count against your spec count. If components grow faster than specs, you have sprawl.
  • Use Cursor Agent mode to refactor sprawl: 'Merge ButtonSmall, ButtonLarge, and ButtonIcon into a single Button component with size and icon variant props.'
Key Takeaway
The five failure modes — logic in presentation, token drift, loose types, sprawl, missing accessibility — are predictable and preventable. The spec phase catches sprawl. The quality gates catch the rest.

Stop Copy-Pasting: The Real Power Is AI That Understands Your Design Tokens

Most tutorials show you how to generate a shadcn button with AI. That's table stakes. The real unlock is teaching your AI to read your design tokens before it writes a single line of JSX. Your tailwind.config.ts and CSS variables are a contract. If your AI doesn't respect them, every generated component introduces drift. I've seen teams burn two weeks fixing mismatched border radii because the AI hallucinated rounded-xl while the spec said rounded-lg. The fix is brutal but effective: inject your token map into every prompt. Store it as a JSON file in your project root. Reference it like a config. When v0.dev generates a component, it should match your tokens within 95% accuracy on the first pass. The remaining 5% gets caught in Cursor. This isn't about generating faster. It's about generating correctly. One prompt with tokens beats ten prompts without.

design-tokens.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
// io.thecodeforge.ai/shadcn-tokens === 0.3.2

// Inject this into every v0.dev prompt
export const designTokens = {
  radius: {
    sm: '4px',
    md: '6px',
    lg: '8px',
    xl: '12px',
  },
  spacing: {
    xs: '4px',
    sm: '8px',
    md: '16px',
    lg: '24px',
    xl: '32px',
  },
  colors: {
    primary: {
      DEFAULT: 'hsl(222.2 47.4% 11.2%)',
      foreground: 'hsl(210 40% 98%)',
    },
    muted: {
      DEFAULT: 'hsl(210 40% 96.1%)',
      foreground: 'hsl(215.4 16.3% 46.9%)',
    },
  },
} as const;

// Expected output from AI: consistent border radius read from token, not hardcoded
Output
// AI generates: className="rounded-lg" (8px) instead of rounded-xl (12px)
// Your design system stays intact across 50+ components
Production Trap:
Never let AI hardcore arbitrary values like rounded-[5px]. That bypasses your design system entirely. If you see this in a generated component, delete it. The next developer will blame you when the UI looks like a Frankenstein project.
Key Takeaway
Design tokens in, garbage out. Feed the AI your constraints before it writes a single className.

Vague prompts produce broken components. I've watched senior engineers type 'generate a data table' and then spend an hour fixing the generated mess. The AI doesn't read minds. It reads words. If you don't specify loading states, empty states, error states, and pagination behavior, you'll get a generic table that looks good in a demo and fails in production. Here's the pattern that works: treat your prompt like a spec document. List every prop, every state, every edge case. Use bullet points. Include your TypeScript interface. Reference the exact shadcn variant you want. For a combobox, that means specifying whether it's single-select or multi-select, whether search is client-side or server-side, and what happens when the user types a value not in the list. My team reduced post-generation rework by 60% just by adopting this prompt structure. The AI generates the first draft. You generate the spec.

prompt-spec.mdMARKDOWN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- io.thecodeforge.ai/prompt-spec === 0.1.0 -->

Generate a shadcn Combobox component with:

**Props Interface:**
- options: Array<{ label: string; value: string }>
- placeholder?: string (default: 'Select...')
- onSelect: (value: string) => void
- loading?: boolean
- error?: string

**States:**
1. Default: show placeholder
2. Open dropdown: filterable search input visible
3. Empty state: 'No results found' centered in dropdown
4. Loading: spinner inside dropdown
5. Error: red text below input
6. Selected: show selected label, hide placeholder

**Behavior:**
- Close dropdown on outside click
- Keyboard navigation: arrow keys, Enter to select
- Escape closes dropdown without selecting
Output
// AI generates a Combobox with all 6 states handled
// No missing edge cases. No surprise runtime errors.
Pro Tip:
Save your best prompts as a prompt-library/ folder in your repo. Next sprint, when you need a Dialog component, you don't start from scratch. You clone the Modald spec and swap the variants. Your AI toolchain becomes a reusable asset, not a one-off party trick.
Key Takeaway
Bad prompts cost hours. Good prompts are a reusable template. Write specs, not wishes.
● Production incidentPOST-MORTEMseverity: high

The Design Token Drift Incident

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. The bug affected three separate pages and required an emergency patch.
Assumption
The AI-generated components used semantic Tailwind classes like bg-primary and text-foreground. We assumed they would adapt to theme changes automatically because they looked correct during development.
Root cause
v0.dev output contained 14 instances of hardcoded color values — bg-white, text-gray-900, border-gray-200 — mixed with semantic tokens. The components rendered correctly in light mode, which was the only mode tested during the sprint review. Dark mode was not part of the standard component review checklist at the time.
Fix
Added a custom ESLint rule that flags any Tailwind class referencing a raw color value. Ran a one-time sweep across the component directory and manually replaced all hardcoded colors with semantic design tokens. Added dark mode rendering to the mandatory review checklist for all generated components.
Key lesson
  • Never trust AI output to use your design tokens correctly — v0.dev defaults to generic Tailwind classes regardless of what you specify in your prompt.
  • Automate design system compliance checks before merging any generated component. A shell script or ESLint rule catches what code review misses.
  • Test every generated component in both light and dark mode before marking it done. Add this to your PR checklist, not your memory.
  • In Tailwind v4, your semantic tokens are defined in your CSS file under @theme — make sure your audit scripts and AI prompts reference the correct location.
Production debug guideCommon symptoms when integrating AI-generated shadcn/ui components — Tailwind v4 and React 19 aware6 entries
Symptom · 01
Component renders correctly in light mode but breaks in dark mode
Fix
Run the hardcoded color audit script above. Replace all raw color class matches with semantic tokens from your @theme definition. Check that your dark mode variant overrides the same token names.
Symptom · 02
TypeScript errors on props after pasting v0.dev output
Fix
Check import paths first — v0.dev uses generic @/components/ui/* paths. Align with your project's barrel exports or path alias configuration. Then check prop types against your actual data model interfaces.
Symptom · 03
Component works in isolation but breaks when nested inside a form or dialog
Fix
Inspect for duplicate Radix UI context providers. AI often wraps components in unnecessary Provider layers that conflict with parent context. Remove the inner provider and let the parent supply the context.
Symptom · 04
Generated table or list causes noticeable render delay with 200+ rows
Fix
Profile with React DevTools Profiler. AI-generated list components rarely include memoization. Wrap row components in React.memo, use useCallback for handlers, and virtualize with @tanstack/virtual for lists exceeding 100 items.
Symptom · 05
forwardRef TypeScript errors or deprecation warnings in React 19 project
Fix
React 19 passes ref as a standard prop. Remove forwardRef wrappers and update the component signature to include ref in the props interface directly. Use Cursor Agent mode: 'Remove forwardRef and accept ref as a standard prop per React 19.'
Symptom · 06
Component fetches data with useEffect but project uses React 19 server components
Fix
Determine if the component needs interactivity. If not, convert to an async server component. If it does, evaluate whether use() is more appropriate than useEffect for the data fetching pattern.
★ AI Component Quick Debug Cheat SheetFast diagnostics for common AI-generated component issues. Copy-paste ready. Run from project root.
Hardcoded colors in generated component
Immediate action
Scan for raw Tailwind color classes
Commands
grep -rn -e 'bg-white' -e 'bg-black' -e 'text-gray-[0-9]' -e 'bg-gray-[0-9]' src/components/ --include='*.tsx'
grep -rn -e 'bg-blue-[0-9]' -e 'bg-red-[0-9]' -e 'bg-green-[0-9]' -e 'text-black' src/components/ --include='*.tsx'
Fix now
Replace all matches with semantic tokens: bg-primary, text-muted-foreground, border-border, text-foreground. Tokens are defined in @theme in globals.css (Tailwind v4).
Component bundle size increased unexpectedly after integration+
Immediate action
Identify what was imported by the generated component
Commands
npx @next/bundle-analyzer
grep -rn "from 'lodash'" src/components/ --include='*.tsx'
Fix now
Replace lodash barrel imports with individual function imports: import debounce from 'lodash/debounce'. Consider replacing lodash entirely with native equivalents for simple operations.
Accessibility audit failures on generated interactive components+
Immediate action
Run automated a11y check against running dev server
Commands
npx @axe-core/cli http://localhost:3000/component-preview --tags wcag2a,wcag2aa
grep -rn 'onClick' src/components/ --include='*.tsx' | grep -v 'onKeyDown'
Fix now
Add keyboard handlers (onKeyDown, onKeyUp) alongside onClick for all interactive elements. Add role attributes and aria-label to elements that lack semantic meaning. Use Cursor Agent mode: 'Add WCAG 2.2 compliant keyboard handlers and ARIA attributes to all interactive elements.'
TypeScript strict mode errors after pasting generated component+
Immediate action
Run type check in isolation
Commands
npx tsc --noEmit --strict src/components/YourComponent.tsx
grep -rn ': any' src/components/YourComponent.tsx
Fix now
Use Cursor Cmd+K: 'Replace all any types with proper interfaces. Reference the User type from src/types/user.ts and the ApiResponse type from src/types/api.ts.'
v0.dev vs. Cursor: Role Comparison
Capabilityv0.devCursor
Initial generation from promptStrong — produces styled, functional React components from structured text promptsNot designed for this — use v0.dev for generation from scratch
Visual polishing before code exportStrong — Design Mode provides a visual editor for layout, spacing, and color adjustmentsNot applicable — Cursor works on code, not visual previews
Project contextualizationLimited — output is generic; does not know your hooks, types, or token definitionsStrong — @codebase context, explicit file references, Agent mode, and .cursorrules adapt output to project conventions
Design token compliancePoor — defaults to hardcoded Tailwind color scales regardless of prompt instructionsStrong — can audit and replace hardcoded colors via Cmd+K or Agent mode with explicit token references
Variant generationModerate — requires separate prompts; Design Mode helps with visual variantsStrong — Agent mode generates variants from the base component in a single instruction
TypeScript type refinementBasic — generates plausible types that may not match your data modelsStrong — aligns generated types with existing project interfaces when given explicit file references
Multi-file batch refactoringNot supported — one component output at a timeStrong — Agent mode handles changes across multiple files in a single session
Storybook story generationNot supportedStrong — Agent mode generates complete Storybook v8 story files from the component and fixture data
Accessibility remediationNot supported — no a11y audit or fix capabilityStrong — Agent mode adds ARIA attributes and keyboard handlers when given explicit WCAG instructions
Learning curveLow — prompt-based interface with visual Design Mode fallbackMedium — requires understanding of @codebase context, Agent mode workflow, and .cursorrules configuration

Key takeaways

1
AI is a scaffolding tool, not a shipping tool
it accelerates the mechanical work; engineering judgment determines what reaches production.
2
Start with a structured specification document, not a creative prompt
consistency across 50+ components comes from the spec, not from careful individual prompting.
3
v0.dev generates and Design Mode polishes; Cursor contextualizes with Agent mode
each tool has a distinct, non-overlapping role.
4
The .cursorrules file is the single highest-leverage configuration in this workflow
it enforces your design system automatically across every Cursor session.
5
Quality gates convert raw speed into sustainable speed
TypeScript, visual, accessibility, and token compliance checks are not optional steps.
6
Component sprawl is the silent killer of design system consistency
always check whether a variant suffices before generating a new component.
7
Tailwind v4 moves token definitions to @theme in your CSS file
update your prompts, audit scripts, and .cursorrules to reference the correct location.
8
React 19 makes the server/client component decision explicit
make it in the spec before you generate, not after you refactor.

Common mistakes to avoid

6 patterns
×

Prompting v0.dev for logic instead of presentation

Symptom
Generated component contains API calls, data transformation, or validation logic mixed into the render function. The component cannot be reused with different data sources.
Fix
Add to every v0.dev prompt: 'Presentational component only — accept all data and callbacks via props. No API calls, no data transformation, no validation logic inside the component.' Then use Cursor Agent mode to extract any logic that slipped through into a custom hook.
×

Ignoring design token requirements in the prompt

Symptom
Generated components use hardcoded Tailwind color scales (bg-blue-500, text-gray-900, bg-white) that do not adapt to dark mode or custom themes. This is the most common failure mode.
Fix
Include in every prompt: 'Use only semantic color tokens — bg-primary, text-muted-foreground, border-border — defined in @theme in globals.css (Tailwind v4). No hardcoded color scales.' After generation, run the color audit script before proceeding to Cursor.
×

Not validating AI-generated TypeScript types against project models

Symptom
Types are overly broad (any, Record<string, unknown>) or do not match existing data model interfaces. Causes runtime errors that TypeScript strict mode would have caught.
Fix
Run tsc --noEmit as the first quality gate — before visual review, before accessibility. Use Cursor Cmd+K: 'Refine all types in this component to match the interfaces in src/types/. Replace any with specific types. Run tsc --noEmit after changes.' Do not merge a component with type errors.
×

Creating new components when a variant would suffice

Symptom
Design system library has 80+ components where 30 to 40 are near-identical variations. Maintenance burden grows linearly with component count. Designers can not find the right component.
Fix
Before opening v0.dev, check the spec document: does an existing component cover 80 percent of this use case? If yes, add a variant prop to the existing spec. Only generate a new component when the prop API is genuinely different. Periodically run: Cursor Agent mode 'Identify components in src/components/ that could be merged into a single configurable component.'
×

Skipping accessibility review because the component looks correct

Symptom
Visual correctness and accessibility compliance are independent. A component can look perfect and fail WCAG 2.2 on keyboard navigation, focus management, and ARIA labeling.
Fix
Run npx @axe-core/cli against the Storybook story URL for every interactive component — not the homepage. Treat accessibility failures as build-breaking errors, not warnings. Use Cursor Agent mode: 'Add WCAG 2.2 compliant keyboard handlers, ARIA roles, and focus management to all interactive elements in this component.'
×

Not generating Storybook stories alongside components

Symptom
Visual review happens in the browser against real data. Edge cases (empty state, error state, long strings, null values) are not tested until they appear in production.
Fix
Generate a Storybook story immediately after the component passes type checking — before any other quality gate. Use Cursor Agent mode to generate the story file. Every story must include: Default, Loading, Error, Empty, and one story per variant. Visual gate runs against Storybook, not the browser.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How would you design a system to automatically generate UI components th...
Q02SENIOR
What are the risks of using AI to generate code at scale, and how do you...
Q03SENIOR
A team generated 30 UI components with AI and shipped them. Two weeks la...
Q04SENIOR
How do you handle the version control and review workflow when generatin...
Q05SENIOR
When would you not use AI generation for a UI component?
Q01 of 05SENIOR

How would you design a system to automatically generate UI components that adhere to a company's design system?

ANSWER
Three layers. First: a specification system — every component is defined in a structured format before generation begins, covering props, variants, states, token requirements, and whether it is a server or client component. This prevents sprawl and inconsistency. Second: a generation layer using v0.dev prompted with the spec plus the design system's token documentation. Design Mode handles visual polish. Cursor Agent mode with a .cursorrules file handles contextualization — replacing hardcoded values with tokens, aligning types with existing interfaces, integrating project hooks. Third: a validation layer with automated checks for design token compliance (no hardcoded color scales), TypeScript strict mode, accessibility (axe-core), and bundle size impact. The key insight is that AI output is a first draft — the specification and validation layers determine what actually ships.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Can I use this workflow with component libraries other than shadcn/ui?
02
How do you handle components that require complex state management?
03
What about performance? Do AI-generated components have performance issues?
04
How many components per hour can you realistically generate with this workflow?
05
How does Tailwind v4's CSS-first configuration change this workflow?
06
What is the right way to handle the React 19 server and client component decision for generated components?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Every example here is drawn from a real system.

Follow
Verified
production tested
May 24, 2026
last updated
1,510
articles · all by Naren
🔥

That's React.js. Mark it forged?

7 min read · try the examples if you haven't

Previous
Full-Stack Type Safety in 2026 – The Ultimate Guide
47 / 47 · React.js
Next
Introduction to Node.js