# How to Update shadcn Components Without Losing Edits

> Update shadcn components without losing edits: what shadcn diff and add --diff show, why a 2-way diff hides whose change is whose, and how a 3-way merge works.

- Author: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Published: Oct 9, 2026
- Category: [Component guides](https://wingo-ui.com/blog/category/components)
- Reading time: 10 min
- Canonical: https://wingo-ui.com/blog/update-shadcn-components-without-losing-edits

## TL;DR

The shadcn CLI has no merge: add --diff lists every difference between your copy and the registry without saying which side made it, and add --overwrite replaces the file, edits included. To keep your edits, run a 3-way merge on each file (the copy as installed, your copy and the new upstream) with git merge-file, keeping the installed copies on a git branch. The Wingo UI CLI saves the installed copy itself, so npx wingo-ui@latest update runs that merge and stops only where both sides changed the same or adjacent lines.

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 `cn` from the `cn` package" ([shadcn changelog](https://ui.shadcn.com/docs/changelog)). The import line changed in every component.
- **March 2025:** new dark mode colors. The [Tailwind v4 page](https://ui.shadcn.com/docs/tailwind-v4) 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](https://wingo-ui.com/components/data-table) 1.0.1 fixed header cells that dropped over the first row while the header was pinned. The fix was one `className` in 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](https://ui.shadcn.com/docs/changelog/2026-03-cli-v4) 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`:

```diff
@@ -1,9 +1,8 @@
 import * as React from "react"
 import { cva, type VariantProps } from "class-variance-authority"
+import { cn } from "cn"
 import { Slot } from "radix-ui"

-import { cn } from "@/lib/utils"
-
@@ -19,10 +18,9 @@
         link: "text-primary underline-offset-4 hover:underline",
-        brand: "bg-orange-600 text-white hover:bg-orange-600/90",
       },
       size: {
-        default: "h-10 px-4 py-2 has-[>svg]:px-3",
+        default: "h-9 px-4 py-2 has-[>svg]:px-3",
```

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](https://github.com/shadcn-ui/ui/blob/main/skills/shadcn/SKILL.md) 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:

```bash
for f in components/ui/*.tsx; do
  first=$(git log --diff-filter=A --format=%H -- "$f" | tail -1)
  git diff --quiet "$first" -- "$f" && echo "untouched: $f"
done
```

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:

```bash
git switch -c shadcn-upstream
for f in components/ui/*.tsx; do
  first=$(git log --diff-filter=A --format=%H -- "$f" | tail -1)
  git show "$first:$f" > "$f"
done
git commit -am "shadcn: components as installed"
git switch -
```

### 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:

```bash
git switch shadcn-upstream
npx shadcn@latest add button dialog --overwrite --yes
git add -A && git commit -m "shadcn: upstream $(date +%F)"
git switch -

for f in $(git diff --name-only shadcn-upstream~1 shadcn-upstream -- components/ui); do
  # a file upstream added: take it as is
  if [ ! -f "$f" ]; then git show "shadcn-upstream:$f" > "$f"; continue; fi
  git show "shadcn-upstream~1:$f" > /tmp/base.tsx
  git show "shadcn-upstream:$f" > /tmp/upstream.tsx
  git merge-file -L yours -L installed -L upstream "$f" /tmp/base.tsx /tmp/upstream.tsx \
    || echo "resolve conflicts in $f"
done
```

`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:

```bash
git diff shadcn-upstream~1 shadcn-upstream -- package.json app/globals.css
```

### 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:

```bash
npx wingo-ui@latest outdated               # installed vs latest, the bump, local edits
npx wingo-ui@latest diff data-table --base # only your edits since install
npx wingo-ui@latest update --dry-run       # what would merge, nothing written
npx wingo-ui@latest update                 # 3-way merge into every installed item
```

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:

```tsx
<<<<<<< local
      className={cn("group/head relative select-none", reorder && "cursor-grab active:cursor-grabbing", isDragging && "opacity-40", className)}
=======
      className={cn(
        // a sticky header cell is already a containing block; a plain "relative" would win over "sticky" in cn and
        // leave the sticky top offset on a relative cell, pushing the header down over the first row
        "group/head [&:not(.sticky)]:relative",
        reorder && "cursor-grab active:cursor-grabbing",
        isDragging && "opacity-40",
        className,
      )}
>>>>>>> wingo-ui 1.0.1
```

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.

> Live demo (Data Table): Scroll inside the table: the header row stays pinned at the top of the box while the rows move under it. Below 768px the rows turn into cards. Try it at [Data Table](https://wingo-ui.com/components/data-table) and install it with `npx wingo-ui@latest add data-table`.

We also ran the published CLI end to end on the free [Field](https://wingo-ui.com/components/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](https://wingo-ui.com/docs/mcp)'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](https://wingo-ui.com/docs/updates).

## 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](https://wingo-ui.com/components/ui-config), 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 `SubmitButton` that composes the [Button](https://wingo-ui.com/components/button) lives in your code and never conflicts.
- **Keep your formatter off copied files.** Add `components/ui` to `.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](https://wingo-ui.com/components/dialog) that stays a centered card on phones, all without touching a component file:

```tsx
// app/providers.tsx
"use client";

import type { ReactNode } from "react";
import { UIProvider } from "@/lib/ui-config";

export function Providers({ children }: { children: ReactNode }) {
  return (
    <UIProvider
      defaults={{
        button: { radius: "full" },
        dialog: { responsive: false },
        "data-table": { defaultPageSize: 50 },
      }}
    >
      {children}
    </UIProvider>
  );
}
```

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:

> Live demo (UIProvider): The first button uses its built-in defaults; the two below get radius full, size lg and tone brand from a UIProvider, and the outline one keeps the variant it passes itself. Try it at [UIProvider](https://wingo-ui.com/components/ui-config) and install it with `npx wingo-ui@latest add ui-config`.

For one component at a time, see the [component guides](https://wingo-ui.com/blog/category/components), starting with the [React form components guide](https://wingo-ui.com/blog/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](https://wingo-ui.com/pricing). The [shadcn alternatives](https://wingo-ui.com/blog/shadcn-alternatives) post compares how other libraries handle updates.

## Components in this post

- [Button](https://wingo-ui.com/components/button) (Free): The button every screen starts with: five variants, six tones, three sizes plus icon sizes, and a loading state that never jumps. Install: `npx wingo-ui@latest add button`
- [Dialog](https://wingo-ui.com/components/dialog) (Free): The modal for focused tasks: a spring-scaled card on desktop that becomes a bottom sheet on phones, with nesting and a scrolling body. Install: `npx wingo-ui@latest add dialog`
- [Data Table](https://wingo-ui.com/components/data-table) (Pro): The admin table on TanStack Table v9: search, filters, sorting, bulk actions, pinning, resizing, virtualization and cards on phones. Install: `npx wingo-ui@latest add data-table`
- [Field](https://wingo-ui.com/components/field) (Free): The wrapper every form control shares: label, hint, animated error and counter, fieldsets, and the one box recipe all inputs use. Install: `npx wingo-ui@latest add field`
- [UIProvider](https://wingo-ui.com/components/ui-config) (Free): App-wide configuration for Wingo UI: default props for every component, the Intl locale and the reduced-motion policy. Install: `npx wingo-ui@latest add ui-config`

## FAQ

### How do you update shadcn components?

Run npx shadcn@latest add <name> --diff to see how your file differs from the registry, then npx shadcn@latest add <name> --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 <component> --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.

---

Source: https://wingo-ui.com/blog/update-shadcn-components-without-losing-edits
