Can v0 by Vercel produce a shippable frontend?
Learn where v0 by Vercel saves frontend time, where generated UI breaks down, and how to budget design-system integration, testing, and cleanup.

Table of Contents
v0 can produce a convincing interface in minutes. It cannot decide whether that interface belongs in your product, obeys your component contracts, handles ugly data, or deserves an approval from the engineer on call. A polished preview proves that the happy path renders. Shipping requires much more evidence.
I use generated UI as an accelerated first implementation, not as a replacement for frontend engineering. That distinction protects the speed advantage. Teams get into trouble when they spend an hour generating a page, call it 90 percent done, then discover during review that the remaining work touches tokens, accessibility, state ownership, analytics, tests, and half a dozen existing components. The last 10 percent was never 10 percent. It was the integration work that the preview hid.
The practical goal is to turn v0 output into a small, reviewable pull request with known cleanup tasks. That means controlling the input, judging the code separately from the picture, and estimating the work that begins after generation.
v0 output is a branch, not a finished frontend
Treat every generation as code from a fast contractor who has seen your brief but has not lived with your repository. The contractor can make a strong first pass. Your team still owns architecture, behavior, security, accessibility, and maintenance.
The current v0 documentation says an imported GitHub repository becomes the source of truth. A chat gets its own branch, code changes create commits, and v0 does not push directly to main. That is the right boundary. It places generated work inside the same review mechanism as human work instead of creating a side door around it. A branch and preview are delivery mechanisms, though, not quality gates.
A preview answers a narrow question: does this version build and display in the preview environment? It does not prove that the component API matches your conventions, the route works with production authorization, the layout survives translated copy, or keyboard users can complete the flow. Even a production-like preview cannot contain all the data and failure conditions your application will meet.
Keep three states separate in planning:
- Generated means v0 produced runnable code.
- Integrated means the code uses the repository's components, data boundaries, and conventions.
- Shippable means the team has tested the required behavior and accepts the operational risk.
Blurring these states causes the familiar estimate failure. A founder sees the generated screen and thinks the feature exists. An engineer sees placeholder data, duplicated primitives, missing loading behavior, and a component that owns state it should receive from the route. Both observations are accurate. They refer to different states of the work.
Define the acceptance boundary before prompting. For a pricing page, it might be: uses the existing PlanCard and Button, reads plans from the current loader, preserves analytics event names, works at the team's supported breakpoints, passes keyboard checks, and includes tests for selection and checkout navigation. With that contract, the team can judge the generation. Without it, aesthetic approval quietly becomes technical approval.
Component quality starts with a written contract
A generated component is good when another engineer can understand its responsibility, reuse it without copying, and change it without triggering unrelated behavior. A clean screenshot says almost nothing about those properties.
v0 tends to fill missing context with plausible choices. That is useful during exploration and risky during integration. If the prompt says "make a plan card," the generator must decide where labels live, which component owns selection, how currency is formatted, what a disabled plan means, and whether the click target is a link or button. Those choices can look reasonable while contradicting the application.
Give it the contract that a human engineer would need. Name the existing primitives, the data shape, the owner of state, required variants, event callbacks, and forbidden substitutions. A compact prompt attachment can look like this:
export type PlanCardProps = {
plan: {
id: string
name: string
priceCents: number
description: string
features: string[]
}
selected: boolean
disabled?: boolean
onSelect: (planId: string) => void
}
// Constraints:
// - Use the existing Button and Price components.
// - The parent owns selection and currency.
// - Do not fetch data or write analytics inside PlanCard.
// - Keep DOM order meaningful without CSS.
This fragment prevents several expensive mistakes. It stops the card from inventing a second money formatter, burying a fetch in a presentational component, and keeping a private selected state that drifts away from checkout state. It also gives reviewers an objective target. They can compare the output with the contract instead of arguing about taste.
Look for boundaries, not file length. One generated file with 250 lines is not automatically bad, and five tiny files are not automatically good. Split code when parts have independent behavior or a stable reuse case. Keep markup together when extraction would merely replace readable HTML with a vague wrapper and a long prop list. Generators often overextract repeated visual fragments while underextracting actual domain concepts. Correct that based on responsibility.
Delete fake flexibility. Generated components often accept optional icons, optional subtitles, several alignments, arbitrary class names, and callbacks that no current caller uses. Every unused option expands the test surface. Build the variants the product needs now. The source is cheap to regenerate later, while a public component API is expensive to retract after other code depends on it.
Also inspect HTML before styling. A clickable div with keyboard handlers is still a poor substitute for a button. A heading chosen for font size damages the document outline. A list of benefits built from unrelated containers gives assistive software less information. Correct elements reduce code because the browser already supplies semantics and interaction behavior.
Your design system must be executable context
Design-system integration works when v0 can consume real tokens and components, not when the prompt contains a paragraph about the brand. Prose such as "use our clean visual style" invites approximation. Source code, component examples, and allowed token names constrain the output.
Vercel's v0 design-system documentation makes this concrete. v0 defaults to shadcn/ui, while a custom registry can expose a team's own primitives, blocks, CSS variables, styles, and dependencies. The newer Design Systems documentation describes the system skill as an adapter that tells v0 where the source lives, which components and props are safe, and how to wire them into an app. That is more useful than copying a documentation site into a prompt.
The shadcn registry specification also distinguishes package dependencies from registryDependencies. The latter point to other registry items, so a product block can declare the exact button, input, or data table it needs. The CLI resolves those items and validates registry resources against its schema. This gives generated work a repeatable installation path instead of relying on copied snippets.
A small registry entry for a product block might declare the relationship like this:
{
"name": "billing-plan-grid",
"type": "registry:block",
"registryDependencies": [
"@company/button",
"@company/price",
"@company/feature-list"
],
"files": [
{
"path": "blocks/billing-plan-grid.tsx",
"type": "registry:component"
}
]
}
The point is not the JSON itself. The point is that billing-plan-grid now has named dependencies. If the generator replaces @company/button with a hand-styled element, the deviation is visible in review.
Do not publish the entire design system to the generator on day one. Start with the primitives and blocks used by the feature: button, field, modal, table, typography, spacing tokens, and one or two proven compositions. Include examples that demonstrate correct behavior, not a museum of every historical variant. More context can produce worse choices when obsolete and current patterns sit beside each other without status labels.
Document escape rules. Sometimes the design system lacks a needed component. Tell v0 whether it may compose primitives, add a local component, or must stop and mark the gap. Otherwise it may create a near copy of an existing control. That duplicate will drift in focus styles, disabled behavior, and theming long after the generated page ships. A visible TODO(design-system-gap) is cheaper than an accidental second system.
Review the generation as an unfamiliar pull request
Review generated code with the same suspicion and courtesy you would apply to code from a new teammate. Do not punish it for being generated, and do not grant it a pass because the preview looks expensive. Start with behavior and ownership, then move toward details.
I use this review order because it exposes costly errors early:
- Trace data from the route or server boundary to the rendered component. Identify placeholder arrays, duplicated types, hidden fetches, and client state that shadows server state.
- Trace every user action. Confirm the correct element handles it, pending and disabled states exist, errors remain recoverable, and analytics fire from the established layer.
- Compare imports and tokens with the repository's approved paths. Search for raw colors, arbitrary spacing, copied primitives, and new packages.
- Read the DOM structure and accessible names without looking at the visual preview. Then test keyboard order and focus after dialogs, errors, and navigation.
- Run the repository's formatter, type checker, tests, and production build. Treat every suppression or configuration change as a separate review decision.
Generated diffs deserve a size limit. A prompt that adds a page, rewrites shared primitives, changes dependency versions, and edits global CSS has produced several decisions in one commit. Split them. First land any intentional design-system addition, then the feature that consumes it. Reviewers can then see whether a shared change is necessary and which snapshot or visual changes it causes.
Inspect package changes before running the app locally. A generator may add a library for a task your repository already solves, choose a package with an incompatible version, or alter a lockfile far beyond the intended dependency. The presence of valid npm code does not make the dependency an acceptable operational choice. Your team owns updates, licenses, bundle cost, and security response after merge.
Watch for comments that assert behavior without implementing it. // Handle error above a console call is not an error state. // Accessible label beside an icon does not give the control a name. // Responsive table does not explain what happens to six columns on a narrow screen. Generated comments can sound like completed requirements, so reviewers should verify the DOM and execution path.
Do not ask v0 to review a huge generation and accept its verdict. It can help locate raw colors, duplicate markup, or missing states, but the same context gap that caused an error can also hide it during self-review. Feed it a concrete acceptance list and ask for evidence by file and line. A response that cannot point to implemented behavior becomes a new task, not proof.
The states outside the screenshot consume the cleanup budget
Production UI spends much of its life outside the ideal screenshot. Data is late, absent, long, translated, unauthorized, stale, or malformed. Users double click, go back, resize, zoom, and return to an expired tab. Generated UI looks fast because it usually begins with the one state that photographs well.
For every data region, require at least the states that your product can actually reach. A dashboard query may need loading, empty, partial, error, and success behavior. A form may need untouched, invalid, submitting, server rejection, and success behavior. Do not demand a ceremonial state matrix for static content, but do not pretend a networked component has only one state.
Responsive review must test content pressure, not just viewport width. Replace a six-letter customer name with a long legal entity. Add the maximum number of navigation items the product allows. Increase browser zoom. Use a translated string that wraps. Put a validation message under the last field in a modal. These checks reveal fixed heights, clipped controls, and grids that only work with the generator's sample copy.
Accessibility needs executable checks and human judgment. Automated tooling catches missing names, invalid relationships, and some contrast failures. It cannot decide whether focus lands in a useful place after a failed submission or whether a screen reader receives a sensible status update while data refreshes. A reviewer should complete the main flow with a keyboard, inspect the accessibility tree for unfamiliar controls, and verify focus after every layer opens or closes.
Turn the main behavior into a test before polishing minor spacing. This example proves more than a screenshot because it names the action and the route transition:
import { test, expect } from "@playwright/test"
test("a customer can choose the Team plan", async ({ page }) => {
await page.goto("/pricing")
const plan = page.getByRole("article", { name: "Team plan" })
await expect(plan.getByText("For growing teams")).toBeVisible()
await plan.getByRole("button", { name: "Choose Team" }).click()
await expect(page).toHaveURL(/\/checkout\?plan=team$/)
})
Adapt the locator and copy to your application rather than pasting this test blindly. Its useful property is the contract: a named region contains the expected plan, a user activates a semantic control, and the application reaches a specific state. Add a rejection or retry test when the flow crosses a network boundary.
Visual regression tests help when the repository already maintains stable fixtures and review discipline. Adding snapshots to an unstable generated page can freeze accidental details and bury reviewers in noise. First settle the component contract and test data. Then capture the states whose appearance carries product risk, such as an overflowing plan name, an inline validation error, or a destructive confirmation dialog.
Estimate cleanup from integration surfaces
Estimate generated frontend work by counting integration surfaces, not by comparing the screenshot with a mockup. Each surface connects the new code to a contract that existed before the prompt: components, tokens, data, routing, authorization, analytics, localization, accessibility, tests, and deployment.
Use a worksheet during planning. For each surface, record three things: the existing contract, the evidence needed for approval, and the engineer-hours your team assigns after inspecting the generated diff. Do not use a universal percentage. A marketing section with static copy and established blocks can require little cleanup. A billing form can look equally polished while touching money formatting, server validation, permissions, analytics, and recovery behavior.
Consider a constructed estimate for one pricing selection flow. The numbers below are a planning decision, not an industry benchmark:
| Work item | Evidence for completion | Budget |
|---|---|---|
| Generate and choose a direction | Accepted desktop and narrow layouts | 4 hours |
| Replace local primitives and tokens | Approved imports with no raw visual values | 5 hours |
| Connect plan data and selection | Types and route behavior match the application | 4 hours |
| Add reachable states and accessibility | Keyboard review and state checks pass | 4 hours |
| Add tests and finish the pull request | Required checks pass with a readable diff | 5 hours |
The total is 22 hours. Generation occupies four of them. Calling the page 80 percent complete after the first four hours would reverse the estimate. The picture is mostly present, while most engineering evidence is still absent.
Teams should replace these figures with their own history after a few pull requests. Track time under stable categories: prompting, visual iteration, system alignment, behavior, tests, and review fixes. Do not combine everything after generation into "cleanup." That label makes necessary product work sound like waste and hides which inputs need improvement. If system alignment repeatedly costs more than expected, invest in the registry and examples. If behavior dominates, tighten feature contracts before prompting. If review fixes dominate, shrink diffs and strengthen acceptance checks.
Set a stop rule for bad generations. If the output chooses the wrong architecture, duplicates central components, and introduces broad global changes, regeneration with better context may cost less than repair. If the structure is sound and the gaps are local, keep it and edit normally. Engineers lose time when they preserve a poor generation merely because producing it felt productive. Generated code has no sunk cost worth defending.
Include reviewer time. A large diff produced in minutes can consume a senior engineer's afternoon because generation speed does not reduce the number of decisions in the code. Smaller prompts, explicit file boundaries, and separate commits turn that load into reviewable units. The useful metric is elapsed time from accepted requirement to merged, verified change, not time from blank prompt to attractive preview.
Prompt for constraints and evidence, not decoration
A good implementation prompt reads closer to a pull request brief than an art direction exercise. It states the user outcome, repository boundary, components to reuse, data source, reachable states, and checks that must pass. Visual references still matter, but they should not carry behavioral requirements by implication.
Give v0 one coherent slice at a time. Ask for the pricing grid against existing components before asking it to connect checkout, edit global navigation, and redesign the mobile menu. Once the grid contract is accepted, add the action and states. This sequence reduces collateral changes and gives each prompt a testable result.
Attach examples selectively. One current component with good props and one complete usage often teach more than twenty disconnected files. Tell the generator which example is authoritative. If the repository contains an old and new table, describe the allowed import path and forbid the old one. Models infer patterns from frequency, so duplicated legacy code can outweigh a short sentence saying not to use it.
Require an implementation note with the generation. Ask it to list files changed, design-system components used, any new dependency, state ownership, and requirements it could not satisfy. This note does not replace review. It exposes assumptions while the chat still has enough context to correct them.
A practical prompt can use this shape without becoming a giant template:
Build the pricing selection region on the existing /pricing route.
Use PlanCard, Button, Price, and Stack from the approved import paths.
Read plans from the route loader; do not add sample plan data.
The route owns selectedPlanId. PlanCard receives data and emits onSelect.
Support loading, unavailable-plan, selection-pending, and server-error states.
Preserve the current analytics event and checkout URL construction.
Do not edit shared primitives, global CSS, dependencies, or configuration.
Add the selection Playwright test and report any unmet requirement.
This prompt prevents more rework than adjectives about a modern, premium interface. It also gives v0 permission to admit a gap. That matters. A generator forced to make everything look complete will often conceal uncertainty behind placeholder behavior. An explicit unmet-requirements note keeps missing work visible.
Avoid repeated prompts that only say "make it better." Better has no stable meaning. Name the defect: spacing token is wrong, emphasis order differs from the reference, focus disappears after close, card height relies on sample copy, or mobile action falls below unrelated content. Precise feedback improves the current output and reveals whether your acceptance criteria were precise enough.
Git workflow keeps generation accountable
Connect v0 to the repository only after deciding what it may change and who reviews it. The official GitHub documentation says each connected chat works on a branch and every code change creates a commit. It also says the repository becomes the source of truth. Use those mechanics to keep human and generated changes in one history.
Start from a clean, current branch. A generation based on stale component APIs can spend its cleanup budget repairing conflicts that better timing would avoid. Keep one chat tied to one coherent feature branch. When several chats alter the same files, merge order becomes part of the implementation and reviewers lose a clear account of intent.
Protect shared files through process even if the tool can edit them. Changes to tokens, primitives, package manifests, authorization helpers, and global styles should trigger the owners or checks your team already uses. Generated code should not bypass code ownership because a tool created the branch.
Ask for small commits at decision boundaries where possible, then inspect the final diff locally or in your normal pull request interface. Run the exact commands the repository requires. A typical gate might include formatting, lint, type checking, unit tests, the focused browser test, and a production build. The command names vary, so keep them in repository instructions rather than making the generator guess.
Do not merge from the preview alone. Preview deployment is excellent for product and design feedback because it gives reviewers a real URL and realistic rendering. Technical approval still belongs to the diff and checks. A layout can feel finished while a shared component was copied, an authorization condition moved into the browser, or a dependency quietly expanded the client bundle.
After merge, keep ownership ordinary. Put generated files in the same directories, tests, and maintenance rotation as human files. Avoid banners that warn future engineers not to edit AI output. Code that needs special handling is already a maintenance problem. If the team cannot explain and safely modify a component, it was not ready to merge.
v0 is worth using when the team preserves judgment
v0 is a strong fit when visual iteration is the bottleneck and the surrounding contracts are known. It can turn a written requirement, a component set, and a reference into a working branch quickly enough that product, design, and engineering discuss real behavior instead of static intent. It is less useful when the hard part is deciding data ownership, permissions, domain rules, or a missing design-system primitive. The generator can implement those decisions after the team makes them.
Use it aggressively for bounded pages, known interaction patterns, internal tools with clear risk, and alternate visual treatments built on stable components. Use tighter supervision for payments, identity, permission management, destructive actions, and flows where an accessibility failure blocks a user. The distinction is based on consequence and integration, not on whether the first preview looks simple.
The economic win comes from removing low judgment assembly while retaining high judgment review. Removing review as well creates fast output and slow recovery. Adding elaborate governance to every static section kills the advantage. Match the controls to the surface: lighter for isolated content, heavier for shared primitives and consequential flows.
A Team & AI Audit can map this workflow against your actual repository, team roles, and delivery history in five business days, including where AI can reduce engineering cost and where human review must stay. The useful outcome is not a mandate to generate more code. It is a smaller team with explicit contracts, better context, and a measured path from prompt to production.
Make the next generated feature prove itself in a pull request. Record time across generation, integration, behavior, tests, and review fixes. After three or four comparable changes, you will know whether v0 is saving delivery time, moving work into review, or exposing a weak design system. That evidence is far more useful than another perfect preview.
Frequently Asked Questions
Is v0 by Vercel production ready?
v0 can produce code that runs in production, but that does not make every generation ready to ship. Your team still needs to verify component use, data boundaries, reachable states, accessibility, tests, and repository checks.
Can v0 use an existing design system?
Yes, especially when the system is exposed through real components, tokens, examples, and a custom registry. A prose description of the brand is weak context; executable components and allowed import paths make deviations visible.
Does v0 work with an existing GitHub repository?
Yes. The current documentation says an imported repository becomes the source of truth, and connected chats work through branches and commits rather than pushing directly to main. Keep normal pull request review and required checks in place.
How much cleanup does v0 code need?
There is no honest universal percentage. Estimate the integration surfaces the feature touches, then budget system alignment, data and state behavior, accessibility, tests, and review fixes using your team's own history.
Should I replace generated shadcn/ui components with my own components?
Replace them when your product already has an approved component for that responsibility. If no component exists, decide explicitly whether to compose current primitives or add a reviewed design-system component instead of keeping a near duplicate.
How do I review AI generated frontend code?
Trace data and actions first, compare imports and tokens with approved paths, inspect semantic HTML and focus behavior, then run the repository's full checks. Review the diff independently of the visual preview.
Can v0 generate accessible interfaces?
It can generate semantic and accessible code, but the result still needs automated checks and human testing. Complete the main flow with a keyboard and verify names, status messages, error recovery, and focus after layers open or close.
What should I include in a v0 prompt for frontend work?
Include the user outcome, files or route in scope, approved components, data source, state owner, reachable states, forbidden changes, and required tests. Ask for an implementation note that lists assumptions and unmet requirements.
When should I regenerate instead of cleaning up v0 output?
Regenerate when the architecture is wrong, central components are duplicated, or broad global changes dominate the diff. Edit the result normally when its structure is sound and the remaining gaps are local and testable.
Can a small team ship faster with v0?
Yes, when the team has clear component and product contracts and keeps senior review on consequential changes. Measure time to a merged, verified change, because generation speed alone can hide work shifted into integration and review.


