How to Update shadcn Components Without Losing Edits
You installed a shadcn/ui button a few months ago, gave it a brand variant and a taller default size, and now upstream has changes you want. When you try to update shadcn components that you have edited, the CLI gives you two tools: add --overwrite, which replaces the file and your edits with it, and add --diff, which lists every difference between your copy and the registry without saying who made each one. This tutorial covers why copies drift, what shadcn diff can and cannot tell you, and how a 3-way merge pulls upstream fixes into files you already changed: first with a git script for stock shadcn/ui, then with a CLI that does it for you.
Why do copied components drift from upstream?
Because the copy stops changing the moment the CLI writes it, and upstream keeps going. A shadcn component is a file in your repo with no version attached. As of October 2026 (shadcn 4.21.4), components.json stores settings such as your style, Tailwind, aliases, icon library and registries, and nothing about which components you installed or when. Meanwhile upstream ships changes like these:
- September 2026: "Every shadcn component now imports
cnfrom thecnpackage" (shadcn changelog (opens in a new tab)). The import line changed in every component. - March 2025: new dark mode colors. The Tailwind v4 page (opens in a new tab) tells existing projects to run
add --all --overwrite, then "Review and re-apply any changes you made to your components." - Bug fixes, which land the same way. The Wingo UI Data Table 1.0.1 fixed header cells that dropped over the first row while the header was pinned. The fix was one
classNamein a file of almost 4,000 lines.
What does shadcn diff show, and what does it miss?
It shows a 2-way diff: your file against the registry's current version, after the CLI applies your aliases and icon library. The standalone command is deprecated; in shadcn 4.21.4 its help reads "[DEPRECATED] Use add [component] --diff instead." The flag arrived with shadcn CLI v4 (opens in a new tab) in March 2026, together with --dry-run and --view, and without a path it shows the first 5 files.
We tested it in a scratch Next.js project: the shadcn button as published in March 2026, plus two local edits, a brand variant and a 40px default size. This is the trimmed output of npx shadcn@4.21.4 add button --diff:
The first hunk is upstream's change. The other two are our edits, shown as lines --overwrite would undo. Nothing in the output tells them apart, because a 2-way diff only knows two files. To know which side changed a line, you need a third: the component as it was installed, which shadcn does not keep. In the same project, the deprecated npx shadcn@4.21.4 diff button printed "No updates found for button.", so stick with the flag.
For edited files, shadcn points you to your coding agent. The CLI v4 post suggests asking it to "check for updates from @shadcn and merge with my local changes", and the official skill (opens in a new tab) tells the agent to "read the local file, analyze the diff, and apply upstream updates while preserving local modifications." The agent sees the same 2-way diff, so unless it digs the installed copy out of git history, it has to guess which side made each change. The script below gets that copy from git, for you or for the agent.
How do you update all shadcn components at once?
Re-add them with --overwrite, which is safe only for files you never edited. shadcn 4.21.4 has no update command, so "shadcn update all components" comes down to npx shadcn@latest add --all --overwrite. It re-adds every component in the registry, including ones you never installed, and replaces your files. Re-running init --reinstall first offers to overwrite components.json, then reinstalls the components you already have, with a warning that they "will be re-installed and overwritten". Find the files you never touched first:
A file that still matches the commit that added it is safe to overwrite, so pass only those names to npx shadcn@latest add <name...> --overwrite. Merge the rest.
How do you update shadcn components you have edited?
Merge three versions of each file: the copy the CLI installed (the base), your copy, and the new upstream. Where only one side changed a region, the merge takes that side; where both changed the same lines, it writes conflict markers. This is the step shadcn's own docs skip when you upgrade shadcn components: they stop at re-applying your changes by hand or asking an agent. git merge-file does the merge; the work is getting the base and the upstream into files, and a branch that holds only pristine CLI output does both.
1. Create a branch with the files as installed
Run this once. It rebuilds each component from the commit that first added it, which is the copy the CLI wrote if you committed before editing:
2. Write the new upstream on that branch and merge it
Run this for every update. List the components you have, since --all would also install the ones you don't:
git merge-file writes the result into your file and exits with the number of conflicts, so the loop names every file that needs a look. If the commit on the upstream branch reports nothing to commit, upstream has not changed and there is nothing to merge. Otherwise the branch now holds the upstream you just merged, so the next update uses it as the base.
If your components import hooks (the sidebar's use-mobile.ts), add hooks/*.ts to the step 1 loop and hooks to the git diff in step 2. And when you add a component later, run the same add on the shadcn-upstream branch and commit it there too; without that, the loop finds no base for the file and every line that differs from upstream becomes a conflict.
3. Check what changed outside the components
The loop only touches components/ui. On the upstream branch the CLI may also have added npm packages and CSS variables:
What happened when we ran it
We ran the script with shadcn 4.21.4 on the scratch project above. Both edits stayed, the button picked up import { cn } from "cn", and the untouched dialog was replaced. Step 3 showed "cn": "^0.4.0" added to package.json; until cn is in your branch's package.json, the merged button fails on a clean install. For that particular change, also run npx shadcn@latest migrate cn. In a test project it installed cn and turned lib/utils.ts into export { cn } from "cn", so every @/lib/utils import in your own code keeps working.
Two caveats. If you edited a component before its first commit, step 1 records your edit as the installed copy, and the first merge silently reverts it, so read git diff after every merge. And if a formatter rewrote a component, run it on the base and upstream files too before merging. On a Prettier-reformatted copy of the Data Table, the merge stopped with one conflict exactly where upstream had changed the file; with all three files formatted the same way, it merged clean.
Can a CLI run the 3-way merge for you?
Yes, if it keeps the base itself. When npx wingo-ui@latest add installs a component, it records the version and a hash per file in wingo-ui.json and saves the pristine copy under .wingo-ui/base/. Commit both. From then on, as of CLI 1.0.3:
For each file, update compares your copy, the base and the new release. An untouched file is replaced. An edited file gets the upstream changes merged in, and its base moves to the new version for next time. It prints the release notes of every version you skipped, installs new dependencies, and reports files a release no longer ships without deleting them.
We replayed the real Data Table 1.0.1 fix through the CLI's merge code, with defaultPageSize changed from 25 to 50 in our copy. The merge was clean: the fix landed and the 50 stayed. Then we also added select-none to the header cell's classes, on the exact line the fix rewrote, and got one conflict:
Keep upstream's block, add select-none to its first string, delete the markers. Or settle every conflict at once with update --theirs (take the release) or --ours (keep yours); both also clear markers an earlier run left behind. git merge-file produced the same merge in both cases, apart from the marker label. In both tools an edit on the line right next to an upstream change also conflicts, while one untouched line between them was enough for a clean merge.
The table below has maxHeight set, so it scrolls inside its own box, which is the case the fix repaired.
$ npx wingo-ui@latest add data-tableWe also ran the published CLI end to end on the free Field, whose 1.0.1 fix keeps focus in an input when its error clears, after changing its optionalLabel default. update printed merged components/ui/field.tsx with the release note, and the edit stayed. Two guard rails: without a base copy (say .wingo-ui/ was never committed and a teammate runs the update) the CLI does not guess and asks for --theirs or --ours, and while conflicts remain it exits with code 1, so a CI step fails instead of shipping markers. Agents can run the same version check through the MCP server's check_updates tool, which compares wingo-ui.json with the latest release and lists the release notes. The full reference is in the updates docs.
How do you keep the next update conflict-free?
Own fewer lines of each component. Conflicts only happen where you and upstream change the same or adjacent lines.
- Set defaults from outside. Every Wingo UI component reads app-wide defaults through UIProvider, so a radius, a page size or the phone layout of a dialog needs no edit to the file. Explicit props win, then the provider, then the component's own defaults.
- Wrap for behavior. A
SubmitButtonthat composes the Button lives in your code and never conflicts. - Keep your formatter off copied files. Add
components/uito.prettierignore, or each upstream change near a reformatted line turns into a conflict. - Update a release or two at a time, so each merge stays small enough to review.
The 25 to 50 page size from the test above, a pill-shaped button and a Dialog that stays a centered card on phones, all without touching a component file:
Wrap {children} in app/layout.tsx with <Providers>, and every Button, Dialog and Data Table under it starts from these defaults. The demo shows the same mechanism on two buttons:
$ npx wingo-ui@latest add ui-configFor one component at a time, see the component guides, starting with the React form components guide.
What should you do next?
On stock shadcn/ui, create the shadcn-upstream branch now, while your history still shows what the CLI wrote. If you want the merge built in, install the free Button and Dialog with npx wingo-ui@latest add button dialog, commit wingo-ui.json and .wingo-ui/, and run npx wingo-ui@latest update --dry-run after the next release. The Data Table is part of Wingo UI Pro. The shadcn alternatives post compares how other libraries handle updates.
FAQ
How do you update shadcn components?
Run npx shadcn@latest add --diff to see how your file differs from the registry, then npx shadcn@latest add --overwrite to replace it. Overwrite drops your edits, so for files you changed, merge three versions (as installed, yours and upstream) with git merge-file instead.
Is the shadcn diff command deprecated?
Yes. As of shadcn 4.21.4 (October 2026) its help text says to use add --diff instead. The flag arrived with shadcn CLI v4 in March 2026, along with --dry-run and --view.
How do I update all shadcn components at once?
npx shadcn@latest add --all --overwrite re-adds every component in the registry and replaces your files, edits included. Overwrite only the files that still match the commit that added them, and merge the ones you changed.
Why does a 3-way merge keep my edits when a diff cannot?
A diff compares two files, so your edits and upstream's changes look the same. A 3-way merge also reads the file as it was installed, so it knows which side changed each line and only stops where both sides changed the same or adjacent lines.
Can the Wingo UI CLI update components I installed with shadcn?
No. wingo-ui update only merges items it installed itself, because it needs the base copy it saved in .wingo-ui/base at install time. For stock shadcn/ui files, use the git merge-file script from this post.
- shadcn/ui
- CLI
- Git
- Updates
- React