Skip to content
Aback UI

ArchitectureReact

Copy-paste components vs an npm component library

When to copy component source into your React project and when to install a versioned package: ownership, upgrades, bundle size, and how to use both.

By the Aback UI teamPublished 8 min read

Copying component source into your project gives you full ownership: you can change any line, but you also own every future update. Installing a versioned npm package gives you updates with a version bump, but you can only customise what the package exposes. Copy the components you expect to change, install the ones you want maintained for you, and know that you can combine both.

What is the difference?

A traditional React component library is a dependency. You run your package manager, import a component, and the implementation lives in node_modules. You never edit it; you upgrade it.

The copy-paste model, popularised by shadcn/ui, treats components as source you add to your repository. A command-line tool reads a registry, writes files such as components/ui/button.tsx into your project, and installs any npm dependencies those files need. After that, the file is yours. There is no runtime package for that component to upgrade.

With Aback UI, the two paths look like this. Copying the source:

bash
npx aback-ui@latest init
npx aback-ui@latest add button dialog
app/page.tsxtsx
import { Button } from "@/components/ui/button";

Installing the package:

bash
npm install @aback-ui/ui @aback-ui/tokens
app/page.tsxtsx
import { Button } from "@aback-ui/ui";

Both give you the same component, built from the same canonical source. The difference is who owns the file after installation. The installation guide covers the CLI path and the npm package guide covers the package path.

When should you copy component source?

Copying is the better choice when you expect to change the component itself.

  • You need to change markup or behaviour. Adding a slot, changing the DOM structure, or wiring in product-specific logic is a one-line edit in your own file. With a package you would need a prop that the maintainers anticipated, or a fork.
  • You are starting from a block. A sign-in page, a settings section, or a dashboard shell is a starting point, not a finished product. You will rename fields, swap data sources, and restructure the layout. Aback UI ships blocks such as sign-in and settings-section through the CLI only, for exactly this reason.
  • You want to read the code you ship. Source in your repository shows up in code review, search, and your editor's go-to-definition, with nothing hidden behind a compiled build.
  • You do not want upstream changes arriving unannounced. A copied file only changes when you change it.

The cost is maintenance. Bug fixes and accessibility improvements upstream do not reach your copy until you pull them in, and the more you have edited a file, the more work that merge becomes.

When is an npm package the better choice?

A package is the better choice when you want the component maintained for you.

  • You use the component as designed. If you only pass props and class names, a copied file is code you maintain for no benefit.
  • You maintain several apps. One version bump updates every app that depends on the package. Keeping a dozen copies of a dialog in sync by hand is the problem a shared library exists to solve.
  • You want a clear upgrade record. A version number in package.json and a changelog tell you exactly which release you are on. A copied file has no version of its own.

The cost is flexibility. You customise through the API the package exposes: props, class names, CSS variables, and composition. For Aback UI that is a reasonable surface, since the components accept className and read their colours from semantic CSS variables, but it is still a boundary.

One Tailwind detail for packages

Tailwind CSS v4 does not scan node_modules. When a package's components use Tailwind classes, you must point Tailwind at the built files with @source, or the components render unstyled. Copied source lives in your project, so Tailwind finds it automatically.
app/globals.csscss
@import "tailwindcss";
@import "@aback-ui/tokens/tokens.css";
@source "../node_modules/@aback-ui/ui/dist";

Ownership, upgrades, and bundle size side by side

ConcernCopied sourcenpm package
Who owns the codeYou, after installationThe package maintainers
Changing markup or behaviourEdit the fileOnly through props, classes, tokens, or composition
Getting fixesCompare and merge by handBump the version
Knowing what changedA diff against the registry (aback-ui diff)The changelog
Import path@/components/ui/button@aback-ui/ui
Tailwind setupNothing extraAn @source line for the package
Bundle sizeWhat you importWhat you import (if the package is tree-shakeable)

Bundle size deserves a closer look, because it is often cited as a reason to copy. A copied dialog still imports its primitives from npm, so the dependency weight is the same. A package published as ES modules with "sideEffects": false, as @aback-ui/ui is, lets your bundler drop every component you do not import. In both cases you ship roughly what you use. Measure your own build before treating either model as the lighter one.

How do you update code you copied?

The usual failure of the copy-paste model is not the first install; it is month six, when the upstream component has had three fixes and nobody remembers which local edits were intentional. Good tooling makes that comparison explicit.

The Aback UI CLI compares your installed files with the current registry version, without writing anything:

bash
# One component
npx aback-ui@latest diff button

# Every installed component and block
npx aback-ui@latest diff --all

When you decide to take the new version, run add again. The CLI asks before it replaces a file that already exists. --dry-run reports what would change without writing, and --no-overwrite keeps every existing file. The full list of options is in the CLI reference.

bash
# See the plan first
npx aback-ui@latest add button --dry-run

# Then apply it, confirming each overwrite
npx aback-ui@latest add button

Two habits make this easier:

  1. Keep edits small and commented. A comment explaining why a line differs from upstream turns a confusing diff into a quick decision.
  2. Prefer wrapping to editing. If you only need different defaults, write a SaveButton that renders Button with your props. The copied file stays identical to upstream and updates cleanly.

For packages, the equivalent step is reading the changelog before a version bump. Aback UI is pre-1.0 at the time of writing (October 2026), so a minor release can change an API; the changelog lists changes with migration notes.

Can you use both in one project?

Yes, and for many teams it is the best arrangement: import the stable primitives from the package, and copy the handful of components and blocks you need to reshape. A few rules keep a mixed setup predictable:

  • Load one theme. aback-ui init writes styles/aback.css, and the package ships the same theme as @aback-ui/tokens/tokens.css. Import one of them, not both, so there is a single place to override tokens.
  • One implementation per component. If you copy dialog, import it from @/components/ui/dialog everywhere. Two dialogs that look the same but behave differently are worse than either model on its own.
  • Keep the @source line while anything is still imported from the package.

A short decision checklist

For each component or block, ask:

  1. Will we change its markup or behaviour, not just its props? Copy it.
  2. Is it a page-level starting point, such as a sign-in form or dashboard shell? Copy it.
  3. Do several apps need it to stay identical? Install the package.
  4. Will we use it exactly as documented? Install the package.
  5. Unsure? Start with the package. Copying later is one command; un-forking is not.

If you want a broader comparison of tools that use the copy model, see Aback UI vs shadcn/ui. To try both paths, browse the component catalogue: each component page shows the CLI command and, for components in the package, the package import.

Frequently asked questions

Are copy-paste components smaller than an npm component library?

Not inherently. A tree-shakeable ESM package ships only the components you import, and copied source still depends on the same npm packages, such as Radix UI. The bundle mostly reflects which components you use, not how you installed them.

Do copied components ever get updates?

Only when you pull them in. With Aback UI, npx aback-ui@latest diff shows how your copy differs from the current registry version, and add asks before replacing a file you already have.

Can I mix copied components and package imports?

Yes. A common pattern is to copy the few components you need to change and import the rest from the package. Use one theme file, and avoid having two implementations of the same component in the same app.

Why does the npm package need a Tailwind @source line?

Tailwind CSS v4 does not scan node_modules for class names. The @source directive tells it to scan the package's built files so the classes the components use are generated.

Share this post

Try Aback UI in your project

Accessible React components you install as source. MIT licensed.

Get started