In a single application, shadcn add needs no thought: there is one components.json, one set of aliases, one place files go. In a monorepo there are two questions it cannot answer for you - which package owns the UI, and which package the command is being run against - and getting them wrong produces the same component in three apps with three sets of edits.
Decide where UI lives first
Two arrangements work, and mixing them does not.
A shared UI package. packages/ui holds the primitives, and every app imports from it. Components are added once, to that package. This is the right default: one button, one place to fix it, and the apps stay thin.
Per-app components. Each app has its own components/ui. Right when the apps are genuinely different products that happen to share a repository, and wrong when they are the same product on different surfaces, because the fourth button arrives within a month.
The failure to avoid is the accidental third arrangement, where a shared package exists and people keep adding to apps anyway because that is what the command did by default.
The configuration
Each package that owns components needs its own components.json, and the CLI supports monorepos directly: run it against the package rather than the repository root.
The aliases in that file are what decide the import paths written into every component, so in a shared package they should point at the package's own paths, not at an app's. When an app consumes the package, its own components.json aliases can point at the shared package, which is what lets add in an app resolve a registry dependency to the shared button instead of writing a second copy.
The three things that break
Tailwind does not see the package. The app's Tailwind setup has to scan the shared package's source, or the classes used only there are stripped from the build and the component renders unstyled. In Tailwind v4 that is a @source directive pointing at the package; in v3 it is a content glob. This is the most common monorepo symptom: the component works in isolation and looks broken in the app.
The theme lives in the wrong place. CSS variables belong at the app level, since each app owns its palette, while the components that read them live in the package. Putting the tokens in the package means every app gets the same theme whether it wants it or not.
Two React copies. Hooks fail at runtime with a message about invalid hook calls when the package and the app resolve different copies of React. The fix is a peer dependency on React in the package plus your package manager's hoisting or resolution setting, and it is worth doing before you spend an afternoon on the symptom.
Keeping the apps honest
Two habits that are worth more than any configuration.
Put the shared package on the import path everywhere, and lint against reaching into another app's components. A cross-app import is how a monorepo turns into a tangle nobody wants to touch.
And when you add a component from a registry, add it to the shared package, then let the app import it. Adding directly into an app is how the fourth button gets created by someone who was in a hurry and did not know the third existed - which is the same problem a team registry solves for teams that do not share a repository.
Where else to look
The honest list, because the answer is not always us:
| Source | Best for | Trade-off |
|---|---|---|
| shadcn monorepo docs | The canonical setup and the --cwd behaviour | Assumes a particular workspace layout |
| Turborepo or Nx | Task orchestration and caching around the packages | Neither decides where your UI lives |
| A private registry | Sharing components across repositories, not just packages | A registry to host and maintain |
| 21st | Components to add into the shared package | 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, and in a monorepo it takes the package as its working directory:
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
- Where should components live in a monorepo?
- In a shared UI package that every app imports from, unless the apps are genuinely different products that happen to share a repository. The failure mode is the accidental third arrangement, where a shared package exists and people keep adding to apps because that is what the command did by default.
- How do you run the shadcn CLI in a monorepo?
- Against the package rather than the repository root, with --cwd. Each package that owns components needs its own components.json, and in a shared package the aliases should point at that package's paths, so the imports written into every component resolve correctly for consumers.
- Why does a shared component render unstyled in the app?
- Tailwind is not scanning the package's source, so classes used only there are stripped from the app's build. In v4 that is a @source directive pointing at the package; in v3 it is a content glob. It is the most common monorepo symptom and it looks like a broken component rather than a build configuration problem.
- Why do hooks fail with an invalid hook call in a workspace?
- Two copies of React are being resolved, one for the package and one for the app. Declare React as a peer dependency in the package and configure your package manager's hoisting or resolutions so a single copy is shared. It is worth checking first, before spending an afternoon on the symptom.



