Integrating v0 Components Into an Existing Codebase Without Breaking It
v0's opinionated Tailwind, shadcn, and CSS variable stack fights your existing codebase unless you harmonise. The three-step merge we run on client projects.
A v0 component looks like a gift. You type a sentence, Vercel's generator produces a pretty React component with real Tailwind classes and shadcn primitives, and you copy the code into your project. Then the buttons are the wrong shade of blue, the corners are rounder than everything else on the page, dark mode stops working, and a Sidebar import throws because the file it references does not exist. None of that is v0 being broken. It is v0 being opinionated in a way your codebase has not agreed to yet.
The fix is not to stop using v0. The fix is a short harmonisation pass before you paste the first component, repeated in smaller form for each new component you pull in. This is the playbook we run when a founder has been generating screens in v0 and needs them merged into an app with its own Tailwind setup, brand palette, and shadcn installation frozen months before v0's latest rewrite.
There are three layers where v0 fights an existing codebase. The Tailwind config is one, because v0 writes its own colour tokens, typography scale, and radius. The shadcn primitives are the second, because v0 generates against the latest registry while your project pinned an older snapshot at install time. The CSS variable cascade is the third, because v0 defines the same token names your theme file already defines, and whichever import order wins rewrites the whole visual system. Get those three aligned first, then paste.
What v0 actually ships and why it collides with your project
Before the harmonisation steps make sense, it helps to name the stack v0 assumes. A current v0 component is Next.js App Router code styled with Tailwind, composed from shadcn/ui components that in turn wrap Radix primitives, iconified with lucide-react, and typeset in Geist Sans and Geist Mono loaded through next/font. Forms use react-hook-form with zod resolvers. Dark mode is class-based. Colour tokens are written in OKLCH into app/globals.css under :root and .dark selectors. Since the late 2024 shadcn rewrite, newer v0 output uses Tailwind v4 syntax with @theme inline and @custom-variant dark directives rather than a tailwind.config.ts file at all.
Your existing project almost certainly differs on at least three of those points. Maybe you are on Tailwind v3 with a config file and HSL-triple variables. Maybe you installed shadcn nine months ago, so your button variants predate variant="link" and you never added the new Sidebar. Maybe your fonts are Inter and your dark mode runs off prefers-color-scheme instead of a class. Every mismatch becomes a silent breakage when v0 output lands without preparation.
The symptoms are predictable. Colours drift because some components inherit your --primary and others inherit v0's. Radius looks unsettled, because v0 defaults to 0.625rem while older shadcn defaulted to 0.5rem. Dark mode toggles on some components and not others, because v0 wrote its variables under .dark while your toggle flips data-theme. Fonts flash on load when two copies of Geist race. A component throws at import time because it references @/components/ui/sidebar, a primitive your project never installed. Each of these has the same cause. v0 wrote a component for a project it imagines; you pasted it into a project that exists.
Step one, the tailwind.config merge strategy
The first harmonisation step is the Tailwind config, and the rule is to merge extends rather than replace. v0 writes its colour tokens under theme.extend.colors with names that shadcn expects, such as primary, secondary, muted, accent, destructive, border, input, and ring. Your existing project almost certainly already uses at least some of those names, because shadcn chose them on purpose to sound like a brand system. The temptation is to paste v0's config block over yours and move on. That is the move that repaints your entire app to v0's palette in a single commit, because every existing component reading bg-primary or text-muted-foreground will now resolve against v0's values instead of yours.
What we do on client projects is the opposite. We keep the host project's colour tokens as the source of truth and rewrite v0's output to match, rather than rewriting the host to match v0. That means opening v0's generated config (or the @theme inline block in globals.css on newer Tailwind v4 output) and reading each token name against the host's equivalent. If the host defines primary as a particular brand orange and v0 generated slate blue, the pasted components should use the host orange. Occasionally v0's output relies on a token the host does not define, such as chart-1 through chart-5 for data visualisations. Those we add to the host config under the same names, using host-palette values that harmonise with the brand.
Tailwind v3 versus v4 is a second fork in the path. If the host is still on Tailwind v3 and v0's latest output uses @theme inline directives, those directives will silently no-op when PostCSS compiles them. The symptom is that colours you expect to be defined simply are not, so everything falls back to the defaults and the component looks washed out. There are two honest ways through. Either strip the Tailwind v4 syntax out of the v0 output and port the token definitions into the host's v3 tailwind.config.ts by hand, or upgrade the host to Tailwind v4, which is a larger decision that touches every file that extends the config. The half-measure, leaving both syntaxes in the tree, is the option that costs you two days of confused debugging later.
Font family is the third merge point and the one most people forget. v0's output often sets theme.extend.fontFamily.sans to a Geist variable. If your host config sets sans to Inter, the later import wins, and you end up with Inter on some pages and Geist on others depending on which layout wrapped the render. Pick one project-wide. If you choose to keep Inter, edit the pasted component's layout wrapper and strip its Geist variable assignment before committing.
Step two, the shadcn version pinning trap
The second step is the one teams miss most often because shadcn does not feel like a dependency. It is copy-paste. You ran npx shadcn-ui@latest init sometime in the past and files landed in components/ui/. There is no version number in your package.json that pins those files to a moment in time. But they are pinned, by file content, to whatever the registry shipped on the day you ran the command. v0 does not know that. v0 generates every new component against today's registry. If your snapshot is from nine months ago and a lot has changed upstream, v0's output will reference APIs your files do not implement.
The concrete examples are worth knowing. The Sidebar primitive did not exist in older shadcn snapshots. If v0 imports SidebarProvider and SidebarContent from @/components/ui/sidebar, the import will fail outright because the file is not there. The fix is to run npx shadcn@latest add sidebar against your project, which appends the current sidebar files. That sounds safe, but be aware you are now mixing old-registry components with new-registry components in the same folder. The Button you installed nine months ago and the Sidebar you installed today share CSS variables and, sometimes, a cn helper. If the newer component assumes a token or a variant that the older one does not know about, you get subtle rendering bugs that are invisible in isolation and only show up at composition time.
The Toast situation is a cleaner example. Older shadcn shipped a custom useToast hook and a Toaster in components/ui/toast.tsx. Newer v0 output uses Sonner, via import { toast } from "sonner", and a <Toaster /> placed in the root layout. If you paste a v0 component that calls toast.success(...) and your project still exports useToast, nothing errors at compile time because toast may be a globally available symbol from somewhere, but in practice you get two toast systems living in one app and users see different notification styles depending on which screen they are on. Standardise on one before the first paste.
The other trap is Button variants. shadcn adds and sometimes renames variants over time. If v0 generates <Button variant="link"> and your older button file's cva definition has no link variant, React renders a button with no classes at all and you end up with an unstyled element that looks like a plain browser default in the middle of a styled page. When you paste a v0 component, diff the referenced shadcn primitives against your components/ui/ files. If anything is missing, regenerate with npx shadcn@latest add and accept that you are now on a newer snapshot for that component. If the project is big enough, do a planned refresh of the whole shadcn surface rather than drift by attrition.
Radix versions sit behind shadcn and cause their own version of this problem. Each shadcn primitive wraps a @radix-ui/react-* package. Newer Radix Dialog, Dropdown, and Popover changed how they handle focus trapping and outside-click detection. If your host project pinned @radix-ui/react-dialog at, say, 1.0.4 and v0's latest output assumes 1.1.x behaviour, your dialog will close at slightly different moments than v0's preview suggested. Run npm ls @radix-ui/react-dialog and compare against what v0's generated package.json snippet recommends. Upgrade host rather than hold v0 back.
Step three, the CSS variable cascade
The third step is the one that produces the most dramatic failures, because CSS cascade is unforgiving about order and v0's variables overlap almost perfectly with a typical shadcn theme. v0 writes into :root and .dark a full set of tokens: --background, --foreground, --card, --popover, --primary, --secondary, --muted, --accent, --destructive, --border, --input, --ring, --radius, and the chart series. Your existing theme file writes the same names. Whichever CSS file is imported last wins. If your globals.css imports v0's block after the host theme, you just repainted the app. If the other way, v0's intended values never take effect and the pasted component renders against your palette by accident, which is sometimes what you wanted but is never what you would have chosen deliberately.
Variable format is the part that trips up people who are alert to cascade order. v0 in late 2024 switched to OKLCH colour space, so you see values like oklch(0.21 0.006 285.885) in the generated tokens. Older shadcn setups use HSL written as three space-separated numbers without the hsl() wrapper, like 222.2 47.4% 11.2%, because the wrapper is applied in the Tailwind config as hsl(var(--background)). If you paste OKLCH values into a setup that wraps with hsl(), the resulting hsl(oklch(...)) is invalid CSS, the browser drops the declaration, and the colour falls back to a default or to black. The component looks catastrophically broken. Before pasting tokens, check which format the host uses, and convert. Either rewrite v0's OKLCH into HSL triples using a colour tool, or upgrade the host to OKLCH, which means rewriting the Tailwind config to stop wrapping with hsl() and just use var(--background) directly.
The --radius variable deserves its own paragraph because it controls every rounded corner in your app. shadcn computes Tailwind's borderRadius.lg, md, and sm from var(--radius) by subtraction. Change that one value and the whole visual language shifts at once. v0 defaults --radius to 0.625rem. Older shadcn snapshots used 0.5rem. The 2px difference is small enough that no single component looks wrong, but across a page the mixed radii read as unsettled. Pick one. If the host is committed to 0.5rem, edit the pasted v0 globals to match. If the host is willing to move to 0.625rem, change the host once and every existing component updates for free.
Dark mode reconciliation is the last piece of the cascade. v0 writes its dark tokens under the .dark class selector and assumes darkMode: 'class' in Tailwind config, toggled by a script that adds or removes .dark on the html element. If your host project was set up with darkMode: 'media', the class selector never matches and dark tokens never apply. Worse, if your host uses a custom selector like [data-theme="dark"], v0's components stay in light mode no matter what the toggle does. Align the strategy before pasting. We default clients to class-based dark mode with a small theme provider, because it gives you a real toggle and matches what v0 expects.
Font import order is a smaller cousin of the cascade problem. v0's layout.tsx usually imports Geist from next/font and attaches its CSS variable to the body. Your existing layout does the same for Inter. If you paste v0's layout over yours, you lose Inter. If you add v0's imports alongside yours, both fonts load, and Geist briefly flashes into view before the final cascade resolves. Pick one font, remove the other's next/font import from the tree, and the FOUT goes away.
The last-mile audit we run on client handoffs
Founders send us v0 exports with a request that reads roughly, make these work inside our existing app without breaking anything. The work is usually two to four days. Day one is the alignment pass described above, applied to the host project before a single v0 file is pasted. Day two is component-by-component integration, where we diff each pasted file against the host's shadcn primitives, update Radix versions, normalise icon imports to the library the host uses, and reconcile form stacks when v0's react-hook-form output lands in a Formik codebase. Day three and sometimes day four is visual regression, rendering the new v0 screens alongside existing ones, catching radius drift, spacing mismatches, and dark mode holes, and closing them before they compound.
Each of the three layers has a non-obvious failure mode that costs hours the first time. The OKLCH versus HSL collision looks like a styling bug, not a parse error. The shadcn version drift looks like a missing prop, not a snapshot mismatch. The cascade order looks like random colour drift, not a file import order problem. Once you have seen each twice, the audit runs quickly.
If you are generating screens in v0 faster than you can merge them, or the first paste broke something you cannot get back, we do v0 integration reviews as a fixed scope engagement. The output is a merged PR with the three-step harmonisation applied, a short written handover, and a checklist for the next v0 component you pull in yourself.
Get weekly field notes.
Practical writing on shipping products, straight to your inbox. No spam.
Need help with this?
MVP Development
We design and build web apps, MVPs, and SaaS products. Talk to us about what you are working on.
Talk to usWant to discuss vibe coders for your business?
Start a project and we'll talk through where you are, what's working, and the highest-leverage moves for the next 90 days.