The whole point of the copied-source model is that you can change anything. The trap is that "anything" includes changes that make the component impossible to reason about, and the difference between a customisation that ages well and one that becomes a fork is almost always where you made it.
Four levels, cheapest first.
Level one: change the token
Most requests that sound like component changes are theme changes. A button that should be less round, a card that should be flatter, a focus ring that should be your brand colour: all of that lives in CSS variables, and changing it there changes every component at once.
That is the correct place for anything that should be true everywhere. The theming guide has the full token set, and the rule that keeps it working: never use a colour without its foreground partner.
Level two: add a variant
shadcn components are built on a variants helper, which means adding a shape is a data change rather than a code change:
Adding to the variant map is the right level for anything the product needs repeatedly. It keeps every use of the new shape identical, it is discoverable by anyone reading the component, and it does not touch the rendering logic.
The failure to avoid: passing a long className at every call site instead. Three call sites is a convention nobody agreed to; ten is a variant that was never named.
Level three: pass classes at the call site
For genuinely one-off adjustments - this card needs more padding on this screen - a className is correct.
One detail makes it work reliably: the components merge incoming classes with a utility that resolves Tailwind conflicts, so a later p-8 beats an earlier p-4 rather than both landing and the cascade deciding. Without that merge, overriding a class from outside is a coin flip.
Keep it to layout and spacing. A call site that changes colours and radii is a variant in disguise.
Level four: edit the component
The level that is available and should be deliberate. It is right when the component's behaviour is wrong for your product: a dialog that must not close on outside click, a table row that needs a different keyboard model, a select that needs multi-selection.
Two habits make it survivable. Leave a comment saying what you changed and why, because the next person will otherwise compare it to the upstream version and assume it drifted. And keep the change minimal: an edited component that still looks like its origin can be re-derived when the upstream fixes something; a rewritten one cannot.
The one thing not to do
Do not wrap it. A component that wraps Button to add one prop, in a project where you own Button, adds a layer, a file and an indirection for nothing. Wrapping makes sense around a dependency you cannot edit. Here you can edit it, so edit it.
The same applies to a second file next to the first: button-brand.tsx beside button.tsx is two buttons, and the team registry guide is about why that number never goes down on its own.
Keeping the edits visible
Because the code is yours, nothing tells you which components you have changed. Two cheap conventions solve it: a short comment at the top of any edited component naming the change, and a review habit of reading the diff on every add, which the CLI guide covers. That is what stops --overwrite quietly deleting a fix nobody remembered making.
Where else to look
The honest list, because the answer is not always us:
| Source | Best for | Trade-off |
|---|---|---|
| CSS variables | Anything that should be true everywhere | Not for one screen |
| class-variance-authority | Named shapes the product uses repeatedly | A convention the team has to keep |
| tailwind-merge | Call-site overrides that actually win | Only resolves Tailwind conflicts |
| 21st | A different component when yours is fighting you | Quality varies by author, so preview before you take it |
Taking one
Every component page has a live preview and the code. Installing goes through the shadcn CLI against our registry:
That key comes from your 21st account, and installs require a membership. Set API_KEY_21ST once in your shell and the command works for anything in the catalogue.
Frequently asked
- How do you customise a shadcn component properly?
- At the cheapest level that works. A token change for anything that should be true everywhere, a named variant for a shape the product uses repeatedly, a className at the call site for a genuine one-off, and an edit to the component only when its behaviour is wrong for your product.
- Why do my className overrides not apply?
- Because two Tailwind classes for the same property both land and the cascade decides. shadcn components merge incoming classes with a utility that resolves those conflicts, so a later p-8 beats an earlier p-4. Without that merge, overriding from outside is a coin flip.
- Should I wrap a shadcn component to customise it?
- No. Wrapping makes sense around a dependency you cannot edit, and here you own the file. A wrapper adds a layer, a file and an indirection for nothing, and a second component beside the first is simply two buttons, which is the number that never goes down on its own.
- How do you keep track of components you have edited?
- A one-line comment at the top saying what changed and why. It converts an archaeological question into a readable one, and it is what stops --overwrite quietly deleting a fix nobody remembered making. Keeping the edit minimal also means the component can be re-derived from a newer upstream version.



