DEV Community

우병수
우병수

Posted on • Originally published at techdigestor.com

AI-Generated UIs vs. Traditional Design: I Ran Both on the Same Project to Find Out Which One Ships Faster

TL;DR: Three days to ship a dashboard. The designer was unavailable, the Figma file had maybe 40% of the screens done, and the PM wanted something demo-ready for a client call.

📖 Reading time: ~29 min

What's in this article

  1. The Situation That Made Me Actually Compare These
  2. What I Mean by 'AI-Generated UI' (and What I Don't)
  3. Setting Up the AI UI Side: v0.dev in Practice
  4. The Traditional Design Path: Figma to Code
  5. The Exact Moment v0 Won (and the Exact Moment It Lost)
  6. Honest Pros and Cons Breakdown
  7. Head-to-Head Comparison Table
  8. When to Pick What — Actual Decision Rules I Use

The Situation That Made Me Actually Compare These

Three days to ship a dashboard. The designer was unavailable, the Figma file had maybe 40% of the screens done, and the PM wanted something demo-ready for a client call. That's not a hypothetical setup — that's the actual pressure that made me stop treating AI-generated UI as a curiosity and start treating it as a real option worth measuring against my normal workflow.

I made a deliberate call to split the work down the middle. Six components would go through the traditional route: take the existing Figma specs, extract tokens, build with shadcn/ui on top of Tailwind, review with the designer async over Slack. The other six I'd hand to v0.dev — describe what I needed in plain English, iterate on the output, drop the generated code into the project. Same codebase, same component library baseline, same deadline pressure. The only variable was how the UI got designed and structured.

What I was testing wasn't really "AI vs humans" — that framing is lazy. I was testing whether the feedback loop was fast enough to matter. Traditional handoff has a specific cost: you block on Figma completion, you block on designer review cycles, you sometimes block on decisions that should take five minutes but stretch to two days because nobody's in the same timezone. I wanted to see where AI generation actually cuts that cost and where it quietly introduces different costs that don't show up until you're knee-deep in the component.

The findings surprised me on both sides. The AI path was faster in ways I expected and slower in ways I didn't. The traditional path held up better in specific scenarios — particularly anything with dense data tables and non-obvious interaction states — but dragged exactly where you'd predict. If you're trying to figure out which tools actually belong in a lean product workflow, the broader guide on Essential SaaS Tools for Small Business in 2026 is worth a look alongside this. What follows is the specific breakdown of what happened on each side of my split experiment.

What I Mean by 'AI-Generated UI' (and What I Don't)

The term gets thrown around loosely, so I want to be specific about what I actually benchmarked here. The tools I spent real time with: v0.dev (Vercel's prompt-to-component generator), Galileo AI (which thinks more in screens and flows than individual components), and GitHub Copilot's inline suggestions while writing component code directly in VS Code. These three sit at meaningfully different points on the automation spectrum, and they produce genuinely usable output — not mockups, not wireframes, actual code you can paste into a Next.js project.

I'm deliberately not talking about asking ChatGPT to "write me a login form in HTML." That approach has its own chaotic energy — you get inline styles, class names like container2, and no design system awareness whatsoever. The tools I tested know what Tailwind utility classes exist, know the shadcn/ui component API, and produce output that slots into a real codebase without making you feel like you need a shower afterward. The distinction matters because the failure modes are completely different.

What v0.dev specifically outputs is React components using either Tailwind or shadcn/ui primitives. Galileo AI outputs Figma frames plus some React — the code side is rougher, but the layout thinking is stronger. Copilot doesn't generate whole screens; it completes what you're already writing, which makes it the least dramatic but often the most practically useful of the three. Here's what a v0.dev-generated card component actually looks like when it lands in your repo:

// v0.dev output — notice it uses shadcn Card, not a div soup
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"

export function ProductCard({ title, price, tag }: ProductCardProps) {
  return (
    <Card className="w-[350px] shadow-sm">
      <CardHeader>
        <CardTitle className="text-lg font-semibold">{title}</CardTitle>
        <Badge variant="secondary">{tag}</Badge>
      </CardHeader>
      <CardContent>
        <p className="text-2xl font-bold">${price}</p>
      </CardContent>
    </Card>
  )
}
Enter fullscreen mode Exit fullscreen mode

The traditional path I'm comparing this against is the one most product teams I know actually use: Figma for wireframes and high-fidelity mockups, design tokens exported via Token Studio or manually maintained in a tailwind.config.ts, then a developer building shadcn/ui components by hand to match the spec. This isn't a strawman "old way" — it's a deliberate, auditable workflow where a designer controls spacing, color, and interaction states before a single line of component code gets written. The handoff is usually a Figma link plus a Notion doc explaining the edge cases the mockup doesn't show.

Why does this distinction matter for the comparison? Because the AI tools aren't replacing Photoshop — they're replacing the implementation stage of a workflow that already speaks React and Tailwind. The question isn't "AI art vs. real design." It's whether generating that ProductCard above from a text prompt is faster and better than a developer building it from a Figma frame. That's a much more interesting and honest question.

Setting Up the AI UI Side: v0.dev in Practice

The thing that surprised me most about v0.dev the first time I used it: you don't install anything to evaluate whether it's useful. Go to v0.dev, type a prompt, and within 10-15 seconds you're looking at rendered JSX with a live preview. I prompted it with "a data table with filters, pagination, and a sidebar nav using shadcn/ui" and got back something genuinely usable — not a skeleton, not Lorem Ipsum chaos, but a structured component with ColumnDef types, a working <Select> for filter dropdowns, and a sidebar using Sheet for mobile breakpoints.

The copy-to-project flow is cleaner than I expected. Once v0 generates a component, it gives you a shareable URL and a one-liner to pull it into an existing Next.js project:

# v0 gives you a URL like v0.dev/chat/b/abc123
# you run this in your project root:
npx shadcn@latest add https://v0.dev/chat/b/abc123

# it drops the component into components/ui/ and installs
# any shadcn primitives it depends on — no manual copy-paste
Enter fullscreen mode Exit fullscreen mode

I've run this on Next.js 14 with App Router and it works without drama. The CLI resolves dependencies automatically — if your project is missing @radix-ui/react-select or cmdk, it installs them. What it doesn't handle is the theme mismatch, and that's where you start losing time.

Here's the real gotcha: v0 generates components against its own internal shadcn theme assumptions. If your project has custom brand tokens in tailwind.config.ts, specifically inside the extend.colors block, the generated components will reference CSS variables like --primary or --muted-foreground that may resolve to completely different values in your setup. Your sidebar might render with a gray background when your brand color is #1a1a2e. You won't see this in v0's preview — it only shows you its theme.

// your tailwind.config.ts — the block v0 ignores
extend: {
  colors: {
    primary: {
      DEFAULT: "hsl(var(--primary))",  // your --primary might be 230 80% 30%
      foreground: "hsl(var(--primary-foreground))",
    },
    // v0 assumes DEFAULT shadcn values here, not yours
    brand: {
      navy: "#1a1a2e",
      accent: "#e94560",
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The reconciliation work is tedious but mechanical. You open the generated file, grep for any hardcoded bg- or text- classes that don't map to your design tokens, then cross-reference against your components.json to confirm the cssVariables flag is set to true — if it's false, v0 and your project are speaking entirely different theming dialects. My honest time estimate: a simple component takes 5 minutes to reconcile, a complex page layout with a sidebar and data table took me closer to 40 minutes the first time I tried to integrate it into a project with an established token system.

Where v0 actually earns its place is prototyping before you've committed to a design system, or spinning up internal tools where brand precision isn't the priority. If you're building a customer-facing product with a tight Figma spec, budget time for that theme reconciliation pass — it's not a dealbreaker, but pretending it doesn't exist will bite you on your first real deadline.

The Traditional Design Path: Figma to Code

The honest surprise with the traditional path isn't the design work — it's how much the shadcn/ui Figma Community kit closes the handoff gap. Search "shadcn/ui" in Figma Community, drop the file into your project, and your designer is now working with the exact components your codebase will render. When they spec a DataTable with a sort icon in the header, you're not guessing at implementation — you already know it maps to @tanstack/react-table under the hood because that's what shadcn ships.

Design tokens are where this gets fiddly. Figma's dev mode can export variable collections, but I've never seen a clean automatic bridge to tailwind.config.ts. What actually works is manual mapping: take your Figma color variables (--primary: hsl(240 5.9% 10%) style) and mirror them in your CSS custom properties file, then reference those in Tailwind. It takes maybe 90 minutes once per project to do this right, and it means your designer can change a token in Figma and you have a concrete, named thing to update in code.

// tailwind.config.ts
// Map Figma token names 1:1 so designers and devs speak the same language
export default {
  theme: {
    extend: {
      colors: {
        primary: "hsl(var(--primary))",
        "primary-foreground": "hsl(var(--primary-foreground))",
        muted: "hsl(var(--muted))",
        "muted-foreground": "hsl(var(--muted-foreground))",
      },
    },
  },
}
Enter fullscreen mode Exit fullscreen mode

The actual build loop goes: pull the Figma frame up on one monitor, run npx shadcn@latest add table, then open the generated file at components/ui/table.tsx and start wiring. A data table with server-side sorting isn't just that one command — you're also adding npx shadcn@latest add button for the sort headers, writing the useSearchParams hook to persist sort state in the URL, and building the column definition array with typed accessors. That's the realistic scope. My honest timeline for a single data table — Figma frame to working component with column sorting, loading skeleton, and empty state — was about 4 hours. Not a complaint, just the real number you should put in your sprint.

// Every column definition traces back to a Figma spec'd header label
const columns: ColumnDef<Invoice>[] = [
  {
    accessorKey: "status",
    header: ({ column }) => (
      <Button
        variant="ghost"
        onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
      >
        Status <ArrowUpDown className="ml-2 h-4 w-4" />
      </Button>
    ),
  },
  // ... rest of columns
]
Enter fullscreen mode Exit fullscreen mode

Where this path genuinely wins is alignment. Before a single npx command runs, a designer has already answered: what happens on mobile, what does the empty state look like, what's the sort indicator icon, does the row hover state use muted or a border change? Those decisions are locked in Figma. With AI-generated UI, those same questions get answered by a language model at generation time — sometimes well, often inconsistently across components, and almost never in a way that reflects your actual brand decisions. Every pixel being intentional isn't just aesthetics; it means QA has a spec to test against and stakeholders approved a visual before engineering time was spent.

The Exact Moment v0 Won (and the Exact Moment It Lost)

The 4-minute stats card grid is what sold me. It was 10pm, I needed a dashboard mockup for a standup the next morning, and I typed something like "stats card grid with sparklines, 4 metrics, subtle shadow, dark text on white" into v0. Four minutes later I had JSX I could actually drop into the repo. The same component built manually — finding the right recharts config, getting the grid gap right, tweaking typography scale — would've eaten 90 minutes minimum. That's not hype, that's a genuine productivity unlock for a very specific situation.

The other place v0 genuinely earns its keep is client pre-approval work. Before anyone has signed off on a design direction, before Figma files exist, you need something that moves in a browser and looks real enough to get feedback. A Loom recording of an AI-generated mockup gets stakeholder reactions that a static wireframe never does. I've used this pattern three times now — prompt a rough layout, record a 2-minute Loom, get real feedback within an hour. The alternative is a designer spending two days building something that might get immediately rejected anyway.

Here's where it fell apart on me: the generated sidebar nav came back with hardcoded hex values baked directly into the JSX style attributes.

// What v0 gave me
<nav style={{ backgroundColor: '#1a1a2e', color: '#e0e0e0' }}>

// What the codebase expected
<nav className="bg-surface text-on-surface">
// or with CSS vars:
// background-color: var(--color-surface);
Enter fullscreen mode Exit fullscreen mode

The moment dark mode toggled, the sidebar stayed hardcoded navy. Fixing it wasn't hard, but it also wasn't free — you have to audit every generated component for this, and v0 does it consistently. It doesn't know your token system, your CSS variable naming convention, or your Tailwind config. It generates visually plausible output that is architecturally foreign to your codebase.

The accessibility failures were the ones that actually concerned me. Missing aria-label on icon buttons. Heading hierarchy that jumped from h2 to h4 because the type sizes looked right visually. Zero visible focus states — the :focus-visible styles weren't there at all. None of this is surprising if you think about how these models are trained, but it's easy to forget when the component looks polished on screen. Shipping AI-generated UI without a dedicated accessibility pass is how you end up with a product that fails WCAG 2.1 AA on components you thought were done.

The rewriting problem is the quiet killer for anything beyond mockup work. The stats card grid was fine because it was a display component — data in, render out. The moment I tried to generate a form with conditional field visibility based on user role plus local validation state plus an optimistic update pattern, the output gave me a clean-looking component that was structurally wrong for our use case. I spent 45 minutes stripping out the generated state management and replacing it with our actual patterns. At that point I would've been faster writing it fresh with a reference component open in a split pane. v0 works brilliantly when the component is self-contained. The second business logic threads through it, you're editing someone else's code with unfamiliar assumptions baked in.

Honest Pros and Cons Breakdown

The thing that actually surprised me after three months of mixing both approaches: AI-generated UI is a prototyping tool that occasionally ships to production, while traditional design is a production tool that's occasionally used for prototyping. That distinction changes how you weigh every trade-off below.

Where AI-Generated UI genuinely wins

Speed on greenfield screens is real and measurable. A new admin dashboard page that would take a designer half a day in Figma plus a developer another half day to implement? I've had v1s running in 20 minutes with v0 or Bolt. Not pixel-perfect v1s — but working, clickable, close-enough v1s that I can put in front of a stakeholder. The other win that doesn't get talked about enough: no context-switching. Jumping from VS Code to Figma to Storybook and back is a productivity killer, and AI UI generation collapses that loop. For solo developers or small teams without a dedicated designer, this matters more than any feature comparison.

Where AI-Generated UI consistently fails you

Accessibility is the hard blocker. I'm not talking about missing alt text — I mean generated components that use div and onClick where a button belongs, missing aria-expanded on dropdowns, focus traps that don't trap. Every major AI UI tool I've tested ships with these issues by default. Dark mode is similarly unreliable — you'll get hardcoded text-gray-900 classes that look fine in light mode and become invisible on dark backgrounds, because the model doesn't understand your Tailwind config's darkMode: 'class' setup unless you spell it out explicitly in the prompt. And output quality variance is brutal. A vague prompt like "make a settings page" produces garbage. A specific prompt like "settings page with sidebar nav, 3 sections (account, notifications, billing), each section uses a card with a gray-50 background, form fields use shadcn/ui Input and Label components, save button is right-aligned" produces something actually usable. Most teams don't write prompts at the specific end of that spectrum consistently.

What traditional design still does better

Pixel-perfect control isn't vanity — it matters when your design system has 8px spacing grids, specific elevation levels, and a component library that took six months to build. AI has no idea your Button component accepts a variant="destructive" prop and uses a specific red from your brand tokens. Traditional design's real advantage is that accessibility can be decided at the design system level, once, and then inherited everywhere. When your Figma component has a built-in focus ring spec and your engineering handoff includes ARIA roles, every screen built from that system starts from a better baseline than any AI output I've seen. QA is also dramatically easier — you're diffing against a spec that exists, not trying to reverse-engineer intent from generated code.

The real cost of traditional design

A full Figma-to-code cycle for a single new screen eats a full day minimum — sometimes two. That's designer time for the mockup, review time, handoff time, developer time to implement, and back-and-forth for inevitable tweaks. This is fine when you're building something that'll be seen by thousands of users. It's genuinely painful when you're iterating on an internal tool or a feature that might get cut. The other cost is dependency on designer availability. If your designer is heads-down on a quarterly launch, that new screen you need for a sales demo next week either gets hacked together or waits. Neither outcome is great.

The hidden cost nobody mentions about AI UI

You still need a developer who knows React and Tailwind well enough to debug the generated output. I've watched junior developers take AI-generated component code, paste it into a project, hit an error they don't understand, and spend two hours chasing it — longer than if they'd just written the component from scratch. Generated code often imports from libraries that aren't installed, uses hooks incorrectly in subtle ways, or produces className strings that conflict with existing styles. The dirty secret is that AI UI generation has a skill floor. Someone on your team needs to be able to read the output critically, identify what's wrong, and either fix it or re-prompt effectively. If you have that person, the speed gains are real. If you don't, you're trading one bottleneck for another.

<!-- AI generated this. Looks fine visually. Ships with 3 a11y violations. -->
<div
  className="flex items-center gap-2 cursor-pointer"
  onClick={handleToggle}
  style={{ userSelect: 'none' }}
>
  <div className={`w-4 h-4 border ${checked ? 'bg-blue-600' : 'bg-white'}`} />
  {label}
</div>

<!-- What it should be -->
<label className="flex items-center gap-2 cursor-pointer select-none">
  <input
    type="checkbox"
    checked={checked}
    onChange={handleToggle}
    className="sr-only"
  />
  <div
    aria-hidden="true"
    className={`w-4 h-4 border rounded ${checked ? 'bg-blue-600' : 'bg-white'}`}
  />
  {label}
</label>
Enter fullscreen mode Exit fullscreen mode

That example isn't hypothetical — I pulled the pattern from an actual v0 output last month. The visual result is identical. The accessible result is not. This is why "AI generated my UI" and "this UI is production-ready" are two separate claims that need separate verification.

Head-to-Head Comparison Table

Head-to-Head: v0.dev vs Figma + shadcn/ui

The accessibility gap is what kills AI-generated UI in enterprise and government projects. I spent an afternoon running axe DevTools against a v0.dev-generated dashboard and found missing aria-labelledby on modal dialogs, buttons with no accessible name, and focus trap logic that simply wasn't there. v0 ships visually plausible components — they look like they work — but WCAG 2.1 AA compliance requires intentional implementation decisions that a prompt can't reliably encode. You will need a manual audit pass before shipping anything to a regulated industry.

The flip side is equally real: a solo developer doing product design and frontend work in Figma + shadcn/ui will spend 3–4 hours on a single reasonably complex screen. Token decisions, responsive breakpoints, state variants in Figma, then re-implementing all of that manually in React. v0 collapses that to 10–20 minutes for a working starting point. That time delta compounds fast across a 30-screen product.

Category

AI-Generated (v0.dev)

Traditional (Figma + shadcn/ui manual)

Time to first working component

2–10 minutes with a decent prompt

45–90 minutes including Figma spec + implementation

Accessibility out of the box

Inconsistent. Basic ARIA sometimes present, complex patterns often missing

Radix-based shadcn/ui primitives are WCAG-aware by default

Dark mode reliability

Hit or miss — inline styles and hardcoded hex values appear regularly

Reliable if your CSS variable tokens are set up correctly

Design consistency across screens

Drifts — spacing, border-radius, shadow depth varies between generations

High — design tokens enforce consistency at the system level

Customizability

Fast for broad changes, painful for surgical edits to generated code

Full control — you wrote it, you know where every class lives

Cost

Free tier has limited monthly generations — check v0.dev/pricing for current limits; Pro is paid

Figma free tier is limited to 3 projects; Dev Mode (needed for real handoff) requires a paid seat

Dark mode is where v0 output quietly breaks at 11pm the night before a demo. The model generates components that look correct in the preview — which renders in a controlled environment — but once you drop the code into a project with next-themes and CSS variables, you find hardcoded bg-white or text-gray-900 Tailwind classes that don't respond to the dark class on the html element. Always grep generated output for non-semantic color classes before committing.

# Quick check for hardcoded color classes in v0-generated output
# Run this before committing any generated component
grep -E "bg-(white|black|gray-[0-9]+|slate-[0-9]+)|text-(white|black|gray-[0-9]+)" \
  src/components/generated/*.tsx

# Anything that shows up here needs to be replaced with semantic tokens:
# bg-white → bg-background
# text-gray-900 → text-foreground
Enter fullscreen mode Exit fullscreen mode

The honest verdict: these tools are solving different problems for different team configurations. A 3-person startup without a dedicated designer should default to v0 for speed and budget, then do a focused accessibility pass before any public launch. A team with a design system already in place — existing tokens, component library, Storybook — gets almost no benefit from v0 because the generated code will fight your existing system rather than extend it. The consistency drift across screens alone will cost you more cleanup time than the initial generation saved.

When to Pick What — Actual Decision Rules I Use

The mistake most teams make is treating this as an either/or religious debate. I spent too long in that camp. The real question isn't "AI or traditional?" — it's "what am I building, for whom, and in the next how many hours?" Once you frame it that way, the answer usually gets obvious fast.

Use AI-Generated UI When These Three Conditions Are True

First: you're prototyping and the prototype is going to get thrown away or heavily modified anyway. Second: it's an internal tool — admin dashboards, ops consoles, internal reporting views — where your users are employees who will tolerate a slightly inconsistent button radius. Third: someone in a meeting said "can we see what this would look like?" and you have 45 minutes. In all three cases, spinning up v0.dev or using Cursor with a UI prompt gets you a clickable thing faster than any Figma file would. The thing that surprised me early on was how good the first draft looks — it fools stakeholders into thinking more work has been done than actually has, which can be both useful and dangerous.

Use Traditional Design When the Stakes Are Customer-Facing

If real users — paying customers, public visitors, anyone outside your org — are going to interact with the UI, the calculus flips. AI-generated components fail in three specific ways at this level: inconsistent spacing at edge cases (a label that's two words too long breaks the whole card), accessibility gaps that only show up with a screen reader, and zero alignment with your brand tokens. If you have a designer on the team, the traditional handoff process (Figma → Storybook → component library) isn't bureaucracy — it's the mechanism that keeps the codebase consistent after the third engineer joins. Also, if you're building a design system that needs to scale across multiple products or teams, AI scaffolding creates technical debt at the token level that's genuinely painful to undo. I've seen teams spend two sprints normalizing color values that v0 just invented on its own.

The Hybrid Approach I Actually Use Now

Generate with v0, then do a three-pass cleanup before anything ships:

  1. Accessibility pass: Run axe-core in the browser, fix contrast ratios, add real aria-label attributes where v0 left placeholders, check keyboard nav flow.
  2. Dark mode pass: AI tools almost always hardcode hex values instead of using CSS custom properties. Replace every hardcoded color with your design token. This is tedious but non-negotiable.
  3. Token alignment pass: Swap out the Tailwind classes that don't map to your design system — font sizes, border radii, shadow values — so the generated component doesn't look alien next to your handwritten ones.
/* What v0 generates (bad for a real codebase) */
.card {
  background: #ffffff;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

/* What you replace it with (actually maintainable) */
.card {
  background: var(--color-surface-primary);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
}
Enter fullscreen mode Exit fullscreen mode

If You're a Solo Dev With No Designer

AI UI is the right call — with one non-negotiable caveat. Budget a dedicated cleanup sprint before any public launch. Not a few hours. A sprint. The gap between "this looks fine in Chrome on my MacBook" and "this is actually accessible, responsive, and consistent on Android Chrome with system font scaling enabled" is larger than it appears at 1am when you're shipping. I'd suggest generating freely during build, keeping a running list of every component that was AI-scaffolded, then going through that list systematically before launch. Also: run Lighthouse accessibility audit (lighthouse --only-categories=accessibility https://yourapp.com) and fix everything below 90. That alone catches the most embarrassing issues.

If You're on a Product Team With a Designer

Don't introduce AI UI tools into the handoff without talking to your designer first. The traditional Figma → code workflow works because both sides have a shared source of truth. When you start generating components with v0 and then backfilling Figma to match (yes, people do this), you break that contract. The designer's annotations stop being authoritative, and you end up in a situation where nobody's sure which version is canonical. I've watched this create real tension on teams. AI tools help the solo developer close the design gap; they help the team developer open a collaboration gap. The one exception: use AI generation in your own local branch to understand what a component might look like, then hand that direction to the designer as a reference. Idea generation, not code generation.

The Accessibility Problem Isn't Going Away

The thing that should embarrass every AI UI tool vendor is that their output fails accessibility checks that we solved, systematically, years ago. I ran npx axe-core@latest against a v0-generated dashboard component last month and got 23 violations on a single screen. Not warnings — violations. Things a screen reader user would hit as a hard wall.

# Run this against anything AI-generated before you ship it
npx axe-core@latest http://localhost:3000

# Expect output like:
# Violation: color-contrast (7 elements affected)
# Violation: button-name (3 elements affected)
# Violation: label (5 elements affected)
# Violation: region (1 element affected)
Enter fullscreen mode Exit fullscreen mode

The pattern I keep seeing across v0, Galileo, and Copilot suggestions is the same missing layer every time. Interactive <div> elements with click handlers and no role="button". Form fields with visual labels that aren't wired to their inputs via htmlFor / id or aria-labelledby. Focus states that got stripped because the AI learned from codebases where developers wrote outline: none and never replaced it with anything. Skip navigation links — which take about four lines of CSS to implement — simply absent. These aren't edge cases. They're the minimum.

Traditional design backed by a solid component system sidesteps most of this without extra effort. Radix UI — the headless primitive layer underneath shadcn/ui — ships with focus management, keyboard navigation, and ARIA roles baked into every component. A Dialog traps focus correctly. A Select announces its state. You get that for free because a human made deliberate architecture decisions upstream. When you pull a <Button> from shadcn, the accessible version is the default version. AI tools generate the visual output and leave the semantics as an exercise for whoever inherits the code.

The real cost here isn't legal risk, though that's real too — it's the hidden refactor debt. AI-generated UIs look done. They pass a visual review. They go into PR and get merged because nobody ran axe-core, and three sprints later you have a junior dev trying to audit 40 screens of output that was never accessible to begin with. I've watched this happen. The fix isn't dramatic — add aria-label here, wire a label there — but the volume is brutal when you're retrofitting instead of building correctly the first time.

  • Missing role attributes: AI generates <div onClick> constantly. Needs role="button" + tabIndex={0} + keyboard handler, or just needs to be a <button>.
  • Broken focus-visible: Generated CSS often nukes the browser default without adding a custom ring. Radix components manage this via the :focus-visible pseudo-class correctly.
  • Orphaned form labels: Visual labels that are purely presentational <span> or <p> elements instead of proper <label htmlFor="input-id"> associations.
  • No skip links: <a href="#main-content" className="sr-only focus:not-sr-only">Skip to content</a> — four tokens, never generated automatically.

My current workflow: AI generates the skeleton, axe-core runs in CI via @axe-core/playwright against every Playwright test, and anything that fails blocks the merge. That catches the worst of it, but it's still more remediation work than if I'd started from shadcn components directly. For internal tools where the user base is controlled, you might tolerate the debt. For anything public-facing, the traditional approach with Radix primitives underneath genuinely wins — not because it's more principled, but because it ships accessible by default and AI-generated output doesn't.

My Current Workflow After All This

The thing that changed my whole approach: I stopped fighting about whether AI-generated UI is "good enough" and started treating it like I treat any other unreviewed contribution. v0.dev generates the initial scaffold, and I open that code expecting to rewrite maybe 40% of it. Not because v0 is bad — it's genuinely impressive — but because no automated tool knows my design system, my accessibility requirements, or why that one button needs a 48px touch target. The output is a rough draft from a junior dev who's fast and never argues back. Useful, not finished.

First thing I run before anything else hits the PR, no exceptions:

# install if you haven't
npm install --save-dev axe-core @axe-core/react

# then in your dev entry point
import React from 'react';
import ReactDOM from 'react-dom/client';

if (process.env.NODE_ENV !== 'production') {
  const axe = require('@axe-core/react');
  // 1000ms delay lets the component tree settle before analysis
  axe(React, ReactDOM, 1000);
}
Enter fullscreen mode Exit fullscreen mode

v0 frequently generates div soup with click handlers instead of buttons, missing aria-label on icon-only controls, and color contrast ratios that look fine in Figma light mode but fail WCAG AA. I've caught contrast violations at 3.8:1 that only show up under certain Tailwind color combinations. Fix every axe violation before anything else merges — I made this a CI gate using jest-axe so nobody can quietly skip it.

The Tailwind config reconciliation is tedious but non-negotiable. v0 spits out hardcoded values like bg-blue-600 and text-gray-900 that have nothing to do with my actual brand tokens. My real config uses CSS custom properties mapped through extend.colors:

// tailwind.config.ts
export default {
  theme: {
    extend: {
      colors: {
        // maps to --color-brand-primary in globals.css
        // so dark mode switching works via [data-theme="dark"]
        'brand-primary': 'rgb(var(--color-brand-primary) / )',
        'surface-default': 'rgb(var(--color-surface-default) / )',
        'text-primary': 'rgb(var(--color-text-primary) / )',
      },
    },
  },
}
Enter fullscreen mode Exit fullscreen mode

If I let v0's hardcoded Tailwind values stay in the codebase, dark mode breaks in exactly the places users will notice first — hero sections, cards, modals. The find-and-replace pass from v0's color classes to my semantic tokens takes maybe 20 minutes per component, and it's the kind of mechanical work where I've started considering a custom codemod.

QA gate is manual and stays manual: any screen that ships gets a dark mode toggle check and a full keyboard navigation pass — Tab order, Enter/Space on interactive elements, Escape to dismiss overlays, focus visible on every focusable element. I tried to automate this fully with Playwright and it catches maybe 60% of the real issues. The other 40% are things like focus getting trapped inside a modal that only appears after a 500ms animation, or a tooltip that renders off-screen in dark mode because someone hardcoded bg-white. Eyes still beat scripts for this.

Figma never fully left my workflow, which surprised me. For internal tooling, I'll sometimes ship straight from a v0 draft after cleanup. But anything customer-facing — onboarding flows, pricing pages, anything a non-technical stakeholder will have opinions about — goes through Figma even if v0 generated the first draft. The reason is practical: Figma is where product and design leave comments, and "we changed the CTA color last Tuesday" conversations need to live somewhere that isn't a GitHub PR. I use v0 to collapse the blank-canvas problem, then import components into Figma as a starting point using the Figma REST API to at least get rough frames. It's not a clean pipeline, but it's honest about where the friction actually lives.


Disclaimer: This article is for informational purposes only. The views and opinions expressed are those of the author(s) and do not necessarily reflect the official policy or position of Sonic Rocket or its affiliates. Always consult with a certified professional before making any financial or technical decisions based on this content.


Originally published on techdigestor.com. Follow for more developer-focused tooling reviews and productivity guides.

Top comments (0)