v0's Opinionated Styling: When to Override and When to Accept
v0 ships with shadcn defaults. Sometimes you fight them and lose. Sometimes accepting them is the right call. The decision guide we use with design-forward clients.
Every v0 project arrives with the same aesthetic. Slate greys, subtle borders, half-rem radius, Inter as the implied type, a primary colour that leans toward neutral black or the generic v0 indigo. You can spot a v0 landing page from across the room. For a weekend hack that is fine. For a client with a brand book, it is the exact reason they came to you. They want it to stop looking like a template.
What most people do next is what costs them two weeks. They open the component files and start editing. They change Button padding inside Button.tsx, rewrite focus rings inside Input.tsx, delete the shadcn dark-mode classes because the brand is light only. Every edit is a fork of a component that came from a well-maintained registry, and every fork is a future merge conflict waiting to happen. There is a cheaper, more reversible way to make a v0 site look like a real brand, and a specific decision rule that tells you when to reach for the expensive hammer and when to leave things alone.
Why v0 feels so opinionated
v0 generates components on top of shadcn/ui, which is not a component library in the traditional npm sense. You do not install shadcn and import from it. The CLI copies component source files directly into your repository under components/ui, and from that moment on you own them. Every Button.tsx, Dialog.tsx, and Card.tsx in your project is yours to edit. There is no upstream to pull from later.
The upside is total control. The downside is that the control is a trap. Because the components look editable, your instinct is to edit them. But the shadcn design system is not carried in those files. It is carried by three layers working together. First, CSS variables defined in globals.css that every component references for colour, radius, and ring tokens. Second, the Tailwind configuration that defines scale, spacing, typography, and animation. Third, the Radix primitives underneath that handle focus management, keyboard navigation, and accessibility semantics. When you open Button.tsx and start hacking, you are poking at the thin wrapper around those three layers. The real levers are elsewhere, and knowing which lever is cheap and which is permanent is the entire skill.
The three layers of override
Every change you want to make falls into one of three layers. Picking the right one is the single most important styling decision you will make on a v0 project.
The cheapest layer is CSS variable theming. You edit the root block and the dark block in globals.css and change the values of tokens like primary, background, foreground, muted, border, accent, destructive, and radius. No component file is touched. Every component in the project picks up the new values automatically because every shadcn component was written to reference these tokens rather than hard-coded colours. A five-line change in globals.css can shift the entire visual identity of the app, and a five-line change can be reverted with a git revert and the project looks exactly as it did before.
The middle layer is Tailwind configuration. You extend the theme object in tailwind.config.ts and add a custom font family, extra spacing stops, brand-specific shadows, or new animation keyframes. This layer is additive. Existing components keep working because you are not removing anything they depend on, you are extending the vocabulary they draw from. Reverting is straightforward, although if you have rewritten components to use your new utilities you will need to unwind those too.
The most expensive layer is component-level override. You open Button.tsx and change the variants. You open Dialog.tsx and rewire the overlay animation. You open Input.tsx and replace the focus ring. Every change at this layer is a permanent fork of a component whose canonical source lives in someone else's registry. You now maintain that component for the life of the project. If shadcn publishes an accessibility fix six months from now, you will not automatically benefit from it because your component has diverged.
Almost every mistake we see in the wild is caused by someone reaching for Layer Three when the right answer was in Layer One. The instinct makes sense, because when you want a button to look different you open the button file. But the button file does not hold the design. The globals.css file does.
Layer one, the CSS variable block
Open globals.css in a v0 project and look for a block that defines variables against the root selector, followed by another block that defines the same variables scoped to the dark class. These are your brand identity controls. You will typically see tokens for background, foreground, card, popover, primary, secondary, muted, accent, destructive, border, input, ring, and radius, each with a paired foreground variant for text.
If your brand is a specific shade of deep green, you change the primary variable to that green expressed in HSL. Every button with the default variant, every focus ring, every active link, every selected row in a data table updates in the same render pass. You do not touch Button.tsx. You do not touch Link.tsx. You change one line and the whole system responds. Corner radius works the same way. The radius variable drives every rounded corner in every component because shadcn components use classes like rounded-md and rounded-lg, which resolve through Tailwind to calculations based on your radius token. This is the reversibility property. A late-cycle revision ("actually can we try slightly rounder") costs nothing to honour.
Semantic colours deserve a sentence too. Destructive, muted, and accent are not arbitrary names. Components reference them in specific contexts. Destructive shows up on delete confirmations and error states. Muted shows up on secondary text, disabled inputs, and placeholder backgrounds. Accent shows up on hover states for menu items and command palette rows. Overriding these tokens is how you tune the feel of the app without rewriting components.
Layer two, the Tailwind config
Some changes genuinely cannot be expressed in CSS variables because the token surface does not cover them. Typography is the obvious case. v0 ships with Inter as the default sans font and uses the standard Tailwind type scale. If your brand uses a custom display face for headings and a different reading face for body, you add those in tailwind.config.ts under theme.extend.fontFamily. You can then update the base layer in globals.css so that h1 through h6 and body pick up the right families by default.
Spacing scale is another candidate. If your design system insists on an 8-point grid and your Tailwind config uses a 4-point grid, either switch to a stricter spacing scale or add the 8-point stops you need. Adding is almost always cheaper than replacing. If the components you already have are using the 4-point utilities, replacing the scale will break them. Extending the scale adds capability without removing anything.
Animation keyframes sit here too. tailwindcss-animate, which ships with shadcn, exposes accordion-down, accordion-up, and a handful of others. If your brand identity includes specific motion, add new keyframes and utility classes in the Tailwind config rather than editing the existing components' class lists. A component that currently animates with data-state-open triggers a well-tested Radix behaviour. Breaking that behaviour to change timing is how you end up with accessibility regressions you did not intend.
Layer three, and the decision rule
There are legitimate reasons to edit a shadcn component file. If you need a variant the registry does not ship, you add it to the variants object in Button.tsx. If you need a composite component your product repeats across pages, you build it on top of the primitives in a new file, not by editing the primitives themselves. If you are adapting a component to a multi-framework setup or changing how it integrates with your form library, those edits live here by necessity.
The illegitimate reasons are the ones to watch for. Changing focus ring colour inside the component file is wrong because the ring variable in globals.css exists for exactly that purpose. Changing default padding inside the component is wrong because the same look can be achieved by restyling the calling site or adjusting the radius token. Removing the dark-mode classes because your product is light-only is wrong because those classes cost nothing when the dark class is never applied to the root element, and they preserve the option of a dark mode later.
The test we apply is short enough to put on a sticky note. Brand identity goes in CSS variables. Scale and motion go in the Tailwind config. Interaction behaviour stays shadcn default. Component files get edited only when no lower layer can produce the outcome. Brand identity means colour, corner radius, type family, and the semantic tone of muted, accent, and destructive. Scale means spacing steps, typography steps, breakpoint stops, custom shadows, and custom animation keyframes. Interaction behaviour means focus management, keyboard navigation, ARIA semantics, portal mounting, and the choreography of open and close states. Those are Radix concerns and you want them left alone because Radix has already solved them better than you will reinvent them in a week.
The rule has a pleasing side effect. A codebase built this way can absorb a brand refresh without a rewrite. When the client returns six months later with a new palette, the work is a globals.css edit. When they return with a new type system, the work is a tailwind.config edit plus a base layer adjustment. The components themselves have not moved, so they still work, and the behaviour users have adapted to is unchanged.
The fights you should not pick
Some overrides feel like they should be easy and are not. We see the same four battles lost repeatedly.
The typography scale fight starts when a designer hands over a modular scale with specific ratios. shadcn uses Tailwind's discrete scale. Aligning them is either a wholesale override of theme.fontSize in the Tailwind config or a per-component rewrite. The wholesale override is reasonable work. The per-component rewrite is how you end up editing twenty files to change how one page renders. If you are not willing to do the config override, accept the default scale and spend the design capital elsewhere.
The animation timing fight looks innocent. Someone decides the dropdown opens too slowly, so they edit the transition classes on the dropdown component. Next week the dialog feels inconsistent, so they edit that too. Three weeks later the motion system has five different easings and nothing feels unified. Motion is a system. Override it in the Tailwind config once or accept the defaults. Do not tune it per component.
The focus ring fight is the dangerous one. Focus rings exist for keyboard users and for accessibility compliance. Removing them because they do not match the brand is a regression, not a style choice. Changing their colour via the ring variable is fine. Removing outline, setting box-shadow to none, or gating focus styles behind :focus-visible in ways that break keyboard navigation is not. If the brand is at odds with the focus system, adjust the ring colour and accept that the ring stays visible.
The dark-mode fight happens when a product has no dark mode in the design but v0 ships dark classes everywhere. The wrong answer is to strip the dark classes out of every component. The right answer is to either leave the class toggle off at the root, or, if you want the option for later, override the dark variable block in globals.css to produce the light palette. Stripping classes is permanent. Gating the toggle is reversible.
Planning for the design-system handoff
The work that makes a v0 project survive contact with a real design team is the work that keeps all styling at the token layer. When the design team arrives, they will bring tokens. They might arrive as Figma variables, as a Style Dictionary export, as a JSON file, or as a written spec. Those tokens map onto the CSS variables in globals.css almost one to one. If your overrides lived in Layer One and Layer Two, the handoff is a mapping exercise measured in days. If your overrides live in Layer Three, the handoff is a reconciliation project measured in weeks, because every forked component must be diffed against the canonical registry, merged with the new token system, and retested.
The same logic applies when the product adds a second theme, a white-label skin for an enterprise customer, or a holiday marketing variant. All of these are trivial when styling is expressed as tokens and painful when it is embedded in component code.
This is where most of our v0 engagements begin. The client did not come asking for design-system harmonisation. They came asking why their components do not match the Figma file, or why a designer quoted six weeks to fix the marketing site. The work underneath both questions is the same. Pull styling up to the token layer, document the mapping, revert the unnecessary component forks, and leave the codebase in a shape that absorbs brand and design changes cheaply. If that is the state your v0 project needs to reach, that is exactly what a WitsCode design-system harmonisation engagement delivers. >
Accept v0's defaults for interaction behaviour, focus management, keyboard handling, and the quiet choreography of open and close states. Override colour, radius, and semantic tone in globals.css. Override typography, spacing, and motion in tailwind.config.ts. Edit component files only when no other layer produces the outcome you need, and when you do, document the fork and accept the maintenance cost. Do that and a v0 project stops looking like every other v0 project without becoming a tangle of component rewrites that nobody wants to inherit.
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.