How shadcn/ui registries actually work: registry.json, URL resolution, styles, and namespaces — explained by a team that runs a production registry.

You have probably run npx shadcn@latest add button more times than you can count. Have you ever looked at where that code actually comes from?
The answer is a registry: an HTTP endpoint that serves JSON files matching a published schema. The shadcn CLI fetches that JSON, resolves dependencies, and writes real source files into your project. No npm package, no node_modules import — the code lands in your repo and you own it. The official docs describe the registry as "a distribution system for code", and that is exactly what it is: components, hooks, pages, config, and rules delivered as copyable files over HTTP.
We run one of these in production — the registry behind our Pro Blocks — so this guide covers both the official mechanics and what they look like once real customers, paid content, and multiple styles get involved.
TL;DR: A shadcn/ui registry is any HTTP endpoint that serves component JSON matching the registry schema.
npx shadcn add buttonresolves against the official registry;npx shadcn add https://your.site/r/thing.jsoninstalls from yours. You define items inregistry.json, build them withshadcn build, and serve static JSON — or generate responses dynamically, like we do for Pro Blocks.
Strip away the tooling and a registry is three things:
name, a type, a files array with inlined source code, plus optional dependencies (npm packages), registryDependencies (other registry items), and cssVars.shadcn CLI fetches the JSON, installs npm dependencies, recursively resolves registry dependencies, and writes the files to the paths your components.json aliases dictate.The important contrast is with a traditional component library. When you npm install a library, you import compiled code you do not control, and upgrading means taking whatever the next version ships. When you install from a registry, the source is copied into your project once. There is no runtime dependency on the registry at all — it is a delivery mechanism, not a package manager.
That ownership model is the whole reason shadcn/ui works the way it does, and the registry system generalizes it: anyone can distribute code the same way the official components are distributed.
When you run shadcn add <something>, the CLI supports three input shapes:
shadcn add button resolves against the official registry at ui.shadcn.com, using your project's configured style.shadcn add https://example.com/r/fancy-card.json fetches that exact JSON from any registry, no configuration needed.shadcn add @acme/button looks up the @acme namespace in your components.json and expands a URL template.Namespaces are configured under the registries key in components.json, per the namespace docs:
{
"registries": {
"@acme": "https://registry.acme.com/r/{name}.json"
}
}
The CLI replaces the {name} placeholder with the item you asked for. There is also an optional {style} placeholder, which the CLI substitutes with your project's configured style — that is how one registry can serve different code per style. If you have ever wondered why components.json has a style field at all, our guide to choosing a shadcn/ui style covers what actually changes between them.
Because URLs are the ground truth here, we probed the official registry directly rather than trusting docs or old blog posts. Observed on 2026-07-31:
| URL | HTTP status |
|---|---|
https://ui.shadcn.com/r/styles/default/button.json | 200 |
https://ui.shadcn.com/r/styles/new-york/button.json | 200 |
https://ui.shadcn.com/r/styles/new-york-v4/button.json | 200 |
https://ui.shadcn.com/r/button.json | 404 |
Two things stand out. First, the style-scoped paths for default and new-york still return 200 even though those styles are deprecated — existing projects keep working, which is exactly the backward-compatibility behavior you want from a registry you depend on. The new-york-v4 path is the current Tailwind v4 delivery path. Second, the bare /r/button.json path returns 404: the official registry does not resolve items without a style segment, which is worth knowing if you ever hardcode registry URLs in scripts or CI.
The resolution pipeline, end to end: name or URL → components.json lookup (style, namespaces, aliases) → final URL → fetch JSON → resolve registryDependencies recursively → install npm dependencies → write files to their target paths.
If the item JSON is the payload, registry.json is the manifest. It lives at the root of your registry project and declares what you publish:
{
"$schema": "https://ui.shadcn.com/schema/registry.json",
"name": "acme",
"homepage": "https://acme.com",
"items": [
{
"name": "fancy-card",
"type": "registry:component",
"title": "Fancy Card",
"description": "A card with a gradient border.",
"files": [
{
"path": "registry/fancy-card/fancy-card.tsx",
"type": "registry:component"
}
]
}
]
}
Item type matters because it controls default install targets. The registry-item schema defines the full set — the ones you will use most are registry:ui (primitives), registry:component (simple components), registry:block (multi-file components), registry:hook, registry:lib, registry:page (route files), registry:style, and registry:theme.
Until recently, one registry.json had to list every item, which got unwieldy for large registries. The May 2026 "Registry Include and Validate" changelog added two pieces:
include lets the root registry.json compose other registry.json files:
{
"$schema": "https://ui.shadcn.com/schema/registry.json",
"name": "acme",
"homepage": "https://acme.com",
"include": [
"components/ui/registry.json",
"hooks/registry.json"
]
}
Each include path must point to an explicit registry.json file (no folder shorthand), included files may omit name and homepage, and item names must stay unique across everything that gets resolved. Running shadcn build flattens the whole tree into built output with no include field remaining.
registry validate checks your source files before you publish:
npx shadcn@latest registry validate
It validates the root manifest, included files, item schema errors, duplicate names, and local file paths — without requiring a build first. If you maintain a registry of any size, put this in CI. We learned the manual version of this lesson the slow way: a malformed item JSON fails at install time, on a customer's machine, which is the worst possible place to discover it.
Here is the minimal path from nothing to a working registry, following the official getting-started guide.
1. Start from the template. Clone shadcn-ui/registry-template — a Next.js project preconfigured with the build step and file layout.
2. Add your component under a registry directory, for example registry/fancy-card/fancy-card.tsx, and reference it from registry.json as shown above.
3. Build.
npx shadcn@latest build
This generates one static JSON file per item in public/r/ — for example public/r/fancy-card.json — with your source code inlined into the files[].content field.
4. Serve it. Run next dev locally or deploy anywhere that serves public/. Your registry is now the set of URLs under /r/.
5. Install from it.
npx shadcn@latest add http://localhost:3000/r/fancy-card.json
That is genuinely all a registry is. Optionally, register a namespace so consumers get the short syntax:
npx shadcn@latest registry add @acme=https://acme.com/r/{name}.json
npx shadcn@latest add @acme/fancy-card
Note what is absent from this list: you do not need Next.js, a database, or a server at all. Static JSON on any host satisfies the contract. The template just makes the build-and-serve loop convenient.
The template covers the happy path. Our registry — the one that serves Pro Blocks, several hundred sections and page templates built on shadcn/ui — has to handle a few things the template does not, and the gap between the two is where the interesting decisions live.
Static items, dynamic delivery. Every block is pre-built into a static JSON file checked into the repo, one file per item — the same output shape shadcn build produces. But we serve them through Next.js route handlers instead of public/, because the route is where authentication, rate limiting, and cache headers happen. Conceptually:
// app/api/registry/styles/[style]/[name]/route.ts
export async function GET(request, { params }) {
const { style, name } = await params;
return serveRegistryItem(request, name, dirsForStyle(style));
}
Per-style delivery via the {style} placeholder. Consumers configure one URL template ending in /styles/{style}/{name}, and the CLI substitutes their project's style. On our side, that segment picks which registry directory serves the request — blocks built for Radix UI versus blocks built for Base UI are separate item sets, with CSS-only items (themes, style presets) shared between them so we do not maintain duplicates. One lesson from running this: treat the style segment as routing input, not trust input — validate it and fall back sensibly, because you will receive values you did not anticipate, including styles from very old components.json files.
Auth for paid content. Free-tier items are served to anyone with IP-based rate limiting. Paid items require a license key sent as a request header; the route validates it against the payment provider, caches the validation result, and returns 401 or 403 with a helpful JSON error when it fails. This maps directly onto the official authentication docs pattern — the CLI can attach headers per namespace, with env-var expansion so tokens never live in components.json:
{
"registries": {
"@private": {
"url": "https://api.company.com/registry/{name}.json",
"headers": {
"Authorization": "Bearer ${REGISTRY_TOKEN}"
}
}
}
}
Legacy URLs never die. Our original endpoint predates per-style delivery and is still hit daily by existing customers, so it lives on as a permanent alias for the default item set. The official registry's own 200s on deprecated style paths tell the same story: registry URLs are public API, and consumers hardcode them in scripts, CI, and docs. Version by adding paths, not changing them.
What we would do differently. Start with the {style} placeholder in the URL scheme from day one, even with a single style — retrofitting a path segment is an API migration. And make error responses rich immediately: an install failure inside a CLI is a terrible debugging surface, so a JSON body that says why (wrong library variant, missing license, item does not exist) saves real support volume.
The consumer of a registry is increasingly not a human typing npx shadcn add. Since the August 2025 CLI 3.0 release, shadcn has shipped an MCP server that lets AI agents browse, search, and install registry items directly. An agent asked to "build a pricing page" can query a registry for pricing sections and install one, instead of hallucinating a component from training data.
This changes what a registry is for. It stops being a convenience for developers and becomes the distribution layer for agent workflows: a machine-readable catalog of code your organization has already approved. Our own agent skills consume our registry API the same way the CLI does — the JSON contract does not care who is fetching. If you are building AI-assisted design-to-code pipelines, this is the same principle we wrote about in using a Figma kit as a design system for Claude: agents produce dramatically better output when they pull from a structured source of truth instead of improvising.
If you maintain internal components and your team uses coding agents, publishing a registry — even a private, static one — is one of the highest-leverage moves available right now.
Is a registry the same as a fork of shadcn/ui? No. A fork is a copy of the component source. A registry is a distribution format — a schema plus an HTTP endpoint. Registries commonly serve components that have nothing to do with the official set: hooks, config files, full pages, entire themes.
Can a registry be private?
Yes. The authentication docs cover token-based access: your endpoint checks a header, and consumers configure that header (with env-var expansion) on the namespace in components.json. Our paid Pro Blocks work exactly this way, with license keys validated server-side.
Do I need Next.js to run one? No. The contract is JSON over HTTP. Static hosting, any framework's route handlers, or a public GitHub repo all work. The registry-template uses Next.js purely for convenience.
What is the difference between dependencies and registryDependencies?
dependencies are npm packages installed via your package manager. registryDependencies are other registry items — names or URLs — that the CLI resolves recursively before installing, so a block can pull in the button and card it is composed from.
@namespace/name expands templates from components.json — with {name} and {style} placeholders.include composes multi-file registries and registry validate catches broken items before your users do.If you want to see a production registry from the consumer side, Pro Blocks installs through everything described in this post — the same CLI, the same schema, the same URL resolution you would use to run your own.
Founder @ shadcndesign.com

A practical guide to theming shadcn/ui with CSS variables: OKLCH tokens, dark mode, custom brand palettes, and applying a full theme in minutes.

How shadcn/ui charts actually work: what ChartContainer does, how ChartConfig drives colors, and what breaks when you move from Recharts 2 to Recharts 3.

The Default style in shadcn/ui is deprecated and New York is what new projects get. Here's what actually differed between the two, what New York looks like today, and what to do if your components.json still says default.