The Complete Guide to Setting Up a shadcn Registry
The shadcn registry lets you distribute your own components, hooks, pages, configs, and any other files so that anyone can pull them into their project using a single CLI command — just like installing shadcn's own components. This guide covers every step from scratch: creating the manifest, registering items, building, serving, setting up namespaces, authentication, and publishing.
Works with any framework. Next.js, Vite, Vue, Svelte, PHP — the registry only requires that your server can serve JSON over HTTP.
What You Will Have at the End
my-project/
├── registry/
│ └── new-york/
│ └── hello-world/
│ └── hello-world.tsx ← your source component
├── public/
│ └── r/
│ └── hello-world.json ← auto-generated, served to CLI
├── registry.json ← the registry manifest
└── package.json
Developers install your component with:
npx shadcn@latest add https://acme.com/r/hello-world.jsonPrerequisites
- Node.js 18 or later
- A project that can serve static files over HTTP (Next.js used throughout this guide)
- Basic familiarity with the
shadcnCLI
Starting fresh? Use the official registry template — it is already pre-configured and lets you skip the boilerplate.
Step 1 — Create the registry.json Manifest
registry.json is the entry point of your registry. The shadcn CLI fetches this file first to discover every item you are distributing. It must be present at the root of your registry endpoint.
Create this file at the root of your project:
{
"$schema": "https://ui.shadcn.com/schema/registry.json",
"name": "acme",
"homepage": "https://acme.com",
"items": []
}Field reference
| Field | Required | Description |
|---|---|---|
$schema | ✅ | Points to the shadcn JSON schema — enables editor validation and autocomplete |
name | ✅ | A short identifier for your registry, used in data attributes and metadata |
homepage | ✅ | The public URL where your registry or site lives |
items | ✅ | An array of registry item definitions — populated in the next step |
Step 2 — Create Your First Component
Place components inside the required registry/[STYLE]/[NAME]/ directory structure. The style name (new-york below) is arbitrary — name it anything as long as it lives under registry/.
import { Button } from "@/components/ui/button"
export function HelloWorld() {
return <Button>Hello World</Button>
}Your directory tree looks like this:
registry/
└── new-york/
└── hello-world/
└── hello-world.tsx
A registry item can contain multiple files. A realistic component might include a component file, a hook, and a utility — all grouped together:
registry/
└── new-york/
└── data-table/
├── data-table.tsx ← registry:component
├── use-data-table.ts ← registry:hook
└── data-table-utils.ts ← registry:lib
Step 3 — Register the Item in registry.json
Add an entry to the items array for every component you want to expose:
{
"$schema": "https://ui.shadcn.com/schema/registry.json",
"name": "acme",
"homepage": "https://acme.com",
"items": [
{
"name": "hello-world",
"type": "registry:block",
"title": "Hello World",
"description": "A simple hello world component.",
"files": [
{
"path": "registry/new-york/hello-world/hello-world.tsx",
"type": "registry:component"
}
]
}
]
}Registry item field reference
| Field | Required | Description |
|---|---|---|
name | ✅ | Unique slug for this item across your registry |
type | ✅ | Item-level type — see the type table below |
title | ✅ | Human-readable name shown in the CLI and tooling |
description | ✅ | Short description — also helps LLMs understand the component |
files | ✅ | Array of file definitions with path and type |
author | — | Author name and email: "Jane Doe <jane@acme.com>" |
registryDependencies | — | Other registry items this item requires |
dependencies | — | npm packages this item requires |
devDependencies | — | npm dev packages needed during development |
cssVars | — | CSS variables to inject into the consuming project |
css | — | Raw CSS rules to add (@layer, @keyframes, @utility, etc.) |
envVars | — | .env variables to add to the consuming project |
docs | — | A message shown to the user after installation |
categories | — | Tags for organising items: ["dashboard", "sidebar"] |
meta | — | Arbitrary key/value metadata |
Item type reference
The item-level type tells the CLI what kind of thing it is overall:
| Type | Use for |
|---|---|
registry:block | Complex multi-file components or full sections |
registry:component | Simple single-purpose components |
registry:ui | UI primitives (single-file, like shadcn's own components) |
registry:hook | Custom React hooks |
registry:lib | Utility functions and helpers |
registry:page | Full page or file-based routes |
registry:file | Miscellaneous files (configs, env files, etc.) |
registry:theme | Full theme definitions |
registry:style | Registry styles, e.g. new-york |
registry:base | Entire design system base |
registry:font | Font configurations |
registry:item | Universal/untyped registry items |
File type reference
Each file inside files also has a type that controls where the CLI places the file in the consumer's project:
| File type | Target directory |
|---|---|
registry:component | components/ |
registry:ui | components/ui/ |
registry:hook | hooks/ |
registry:lib | lib/ |
registry:page | app/ or pages/ (requires target) |
registry:file | Anywhere (requires target) |
For registry:page and registry:file, you must also provide the target field:
{
"files": [
{
"path": "registry/new-york/hello-world/page.tsx",
"type": "registry:page",
"target": "app/hello/page.tsx"
},
{
"path": "registry/new-york/hello-world/.env",
"type": "registry:file",
"target": "~/.env"
}
]
}Use ~ to reference the project root (e.g. ~/foo.config.js). You can also use target placeholders that respect whatever paths the consumer has configured in their components.json:
| Placeholder | Resolves to |
|---|---|
@components/ | aliases.components |
@ui/ | aliases.ui |
@lib/ | aliases.lib |
@hooks/ | aliases.hooks |
Example:
{
"files": [
{
"path": "registry/new-york/example/button.tsx",
"type": "registry:ui",
"target": "@ui/button.tsx"
},
{
"path": "registry/new-york/example/helper.ts",
"type": "registry:lib",
"target": "@lib/helper.ts"
}
]
}Step 4 — Declare Dependencies
If your component relies on npm packages or other registry items, declare them so the CLI installs everything automatically when a developer adds your component.
{
"items": [
{
"name": "date-picker-form",
"type": "registry:block",
"title": "Date Picker Form",
"description": "A validated date picker field built with React Hook Form.",
"registryDependencies": [
"button",
"calendar",
"popover",
"https://acme.com/r/custom-input.json"
],
"dependencies": [
"zod@^3.20.0",
"react-hook-form",
"@hookform/resolvers",
"date-fns"
],
"devDependencies": [
"tw-animate-css"
],
"files": [
{
"path": "registry/new-york/date-picker-form/date-picker-form.tsx",
"type": "registry:component"
},
{
"path": "registry/new-york/date-picker-form/use-date-picker.ts",
"type": "registry:hook"
}
]
}
]
}registryDependencies accepts three formats:
- Short names like
"button"— resolved from the official shadcn registry - Namespaced names like
"@acme/input"— resolved from configured namespaced registries - Full URLs like
"https://acme.com/r/editor.json"— fetched directly
dependencies accepts npm package names. Pin versions with name@version syntax (e.g. "zod@^3.20.0").
Step 5 — Add CSS Variables, CSS Rules, and Env Variables
You can inject CSS variables, raw CSS, and environment variables into the consumer's project alongside your files.
CSS Variables
{
"cssVars": {
"theme": {
"font-heading": "Poppins, sans-serif"
},
"light": {
"brand": "oklch(0.205 0.015 18)",
"radius": "0.5rem"
},
"dark": {
"brand": "oklch(0.205 0.015 18)"
}
}
}Raw CSS Rules
{
"css": {
"@layer base": {
"body": {
"font-size": "var(--text-base)",
"line-height": "1.5"
}
},
"@keyframes wiggle": {
"0%, 100%": { "transform": "rotate(-3deg)" },
"50%": { "transform": "rotate(3deg)" }
},
"@utility text-magic": {
"font-size": "var(--text-base)",
"line-height": "1.5"
}
}
}Environment Variables
{
"envVars": {
"NEXT_PUBLIC_APP_URL": "http://localhost:4000",
"DATABASE_URL": "postgresql://postgres:postgres@localhost:5432/postgres",
"OPENAI_API_KEY": ""
}
}Environment variables are added to .env.local or .env. Existing variables are never overwritten. Use envVars for development or example values only — never for production secrets.
Installation Docs Message
{
"docs": "To use this component, create an OPENAI_API_KEY at https://platform.openai.com."
}This message is displayed to the developer in the terminal after the component installs.
Step 6 — Build the Registry
Install the shadcn CLI
# pnpm
pnpm add shadcn@latest
# npm
npm install shadcn@latest
# yarn
yarn add shadcn@latest
# bun
bun add shadcn@latestAdd a build script to package.json
{
"scripts": {
"dev": "next dev",
"build": "next build",
"registry:build": "shadcn build"
}
}Run the build
pnpm registry:buildThe build command reads registry.json, validates every item against the schema, and generates one JSON file per item inside public/r/. So hello-world becomes public/r/hello-world.json.
You can change the output directory:
shadcn build --output ./public/registryThe generated JSON files inside
public/r/are what the CLI fetches when a developer runsshadcn add. Never edit them manually — always editregistry.jsonand re-run the build.
Step 7 — Serve the Registry
Start your dev server:
pnpm devYour registry items are now available at:
http://localhost:3000/r/hello-world.json
Open that URL in a browser to verify it returns valid JSON. You can also inspect any registry item payload before installing it:
pnpm dlx shadcn@latest view http://localhost:3000/r/hello-world.jsonStep 8 — Install a Registry Item
With the server running, any developer can install your component:
# pnpm
pnpm dlx shadcn@latest add http://localhost:3000/r/hello-world.json
# npm
npx shadcn@latest add http://localhost:3000/r/hello-world.json
# yarn
yarn dlx shadcn@latest add http://localhost:3000/r/hello-world.json
# bun
bunx shadcn@latest add http://localhost:3000/r/hello-world.jsonThe CLI resolves all declared registryDependencies and dependencies automatically, installing the component and everything it needs in one step.
Step 9 (Optional) — Set Up Content Negotiation for Clean URLs
By default the install URL looks like https://acme.com/r/hello-world.json. With HTTP Content Negotiation you can shorten this all the way to just https://acme.com — your server detects whether the request comes from the CLI or a browser and routes accordingly.
When the shadcn CLI requests a registry, it sends these headers:
User-Agent: shadcn
Accept: application/vnd.shadcn.v1+json, application/json;q=0.9
Your server inspects those headers and returns either the registry JSON (to the CLI) or your normal HTML page (to browsers). From a single URL you can serve:
- HTML to browsers — your landing page, docs, or marketing site
- JSON to the CLI — an installable registry item
- Markdown to AI agents and LLMs — a machine-readable version of your content
Next.js setup
Configure rewrites in next.config.ts:
import type { NextConfig } from "next"
const nextConfig: NextConfig = {
async rewrites() {
return {
beforeFiles: [
// Route CLI requests identified by Accept header
{
source: "/",
has: [
{
type: "header",
key: "accept",
value: "(.*)application/vnd\\.shadcn\\.v1\\+json(.*)",
},
],
destination: "/r/index.json",
},
// Route CLI requests identified by User-Agent
{
source: "/",
has: [
{
type: "header",
key: "user-agent",
value: "shadcn",
},
],
destination: "/r/index.json",
},
],
}
},
async headers() {
return [
{
source: "/",
// Tell CDNs and proxies to cache separately per Accept and User-Agent
headers: [{ key: "Vary", value: "Accept, User-Agent" }],
},
]
},
}
export default nextConfigExpress.js setup
app.get("/", (req, res) => {
res.vary("Accept")
res.vary("User-Agent")
// CLI identified by its vendor Accept header
if (req.accepts("application/vnd.shadcn.v1+json")) {
return res.json(registryData)
}
// CLI identified by User-Agent as fallback
if (req.get("User-Agent") === "shadcn") {
return res.json(registryData)
}
// Browsers get the normal homepage
res.send(htmlContent)
})With this in place, developers can install directly from your domain root:
npx shadcn@latest add https://acme.comStep 10 (Optional) — Set Up Namespaced Registries
Namespaces let developers reference your registry items using a short @namespace/name syntax instead of full URLs. They are configured in the consumer's components.json, but understanding namespaces helps you design your registry URL structure correctly.
Consumer-side configuration
The developer adds your registry to their components.json:
{
"registries": {
"@acme": "https://registry.acme.com/r/{name}.json"
}
}The {name} placeholder is replaced with the item name at install time. So @acme/hello-world fetches https://registry.acme.com/r/hello-world.json.
You can also use a {style} placeholder to serve style-specific variants:
{
"registries": {
"@acme": "https://registry.acme.com/{style}/{name}.json"
}
}With this, @acme/hello-world for a project using the new-york style fetches https://registry.acme.com/new-york/hello-world.json.
Once configured, installing is one command
pnpm dlx shadcn@latest add @acme/hello-worldInstall multiple items at once:
pnpm dlx shadcn@latest add @acme/header @acme/footer @acme/data-tableMulti-registry setups
Consumers can configure multiple registries in one project — for example organised by type, team, or stability:
{
"registries": {
"@acme-ui": "https://registry.acme.com/ui/{name}.json",
"@acme-hooks": "https://registry.acme.com/hooks/{name}.json",
"@acme-ai": "https://registry.acme.com/ai/{name}.json",
"@v0": "https://v0.dev/chat/b/{name}"
}
}Step 11 (Optional) — Add Authentication to Your Registry
If your registry is private, add authentication using environment variables. The consumer configures this in their components.json:
{
"registries": {
"@acme-internal": {
"url": "https://internal.acme.com/registry/{name}.json",
"headers": {
"Authorization": "Bearer ${INTERNAL_REGISTRY_TOKEN}"
}
}
}
}Then the consumer sets the token in their .env.local:
INTERNAL_REGISTRY_TOKEN=your_token_hereThe CLI automatically expands ${VAR_NAME} from process.env — the token is never logged or stored. Environment variables work in headers, params, and URLs.
Supported authentication methods
Bearer token:
{ "Authorization": "Bearer ${REGISTRY_TOKEN}" }API key in header:
{ "X-API-Key": "${API_KEY}" }Basic auth:
{ "Authorization": "Basic ${BASE64_CREDENTIALS}" }Query parameter:
{
"url": "https://registry.example.com/{name}.json",
"params": { "api_key": "${API_KEY}" }
}Always use HTTPS for private registries. Never commit actual tokens to version control.
Step 12 — Publish Your Registry
Deploy your project to any public (or private) host. For Next.js, Vercel is the simplest option:
vercel deployOnce deployed, your registry is live. Update any local http://localhost:3000 URLs to your production domain.
Import Rules Inside Registry Components
All imports inside registry components must use the @/registry path prefix. This is enforced so the build tool can correctly transform paths when writing files into a consumer's project.
// ✅ Correct — always import from @/registry/...
import { useDataTable } from "@/registry/new-york/data-table/use-data-table"
// ❌ Wrong — never import from @/components/...
import { useDataTable } from "@/components/use-data-table"Guidelines Checklist
Follow these rules when building components for your registry:
- Place items under
registry/[STYLE]/[NAME]/— nested underregistry/ name,type,description, andfilesare required on every item- Write clear
titleanddescription— they help LLMs understand your component - List every registry item dependency in
registryDependencies - List every npm package in
dependencieswith an appropriate version range - All imports inside registry files must use
@/registry/...paths - Group related files (component, hook, utility) inside the same named folder
- Run
shadcn buildand commit the updatedregistry.jsonevery time you add or change an item - Never edit files inside
public/r/manually — they are generated
Complete registry.json Example
Here is a real-world manifest for a multi-component design system registry:
{
"$schema": "https://ui.shadcn.com/schema/registry.json",
"name": "acme-ui",
"homepage": "https://ui.acme.com",
"items": [
{
"name": "hello-world",
"type": "registry:block",
"title": "Hello World",
"description": "A minimal hello world component demonstrating the registry.",
"files": [
{
"path": "registry/new-york/hello-world/hello-world.tsx",
"type": "registry:component"
}
]
},
{
"name": "data-table",
"type": "registry:block",
"title": "Data Table",
"description": "Feature-rich data table with sorting, filtering, and pagination.",
"registryDependencies": ["button", "input", "select"],
"dependencies": ["@tanstack/react-table"],
"files": [
{
"path": "registry/new-york/data-table/data-table.tsx",
"type": "registry:component"
},
{
"path": "registry/new-york/data-table/use-data-table.ts",
"type": "registry:hook"
},
{
"path": "registry/new-york/data-table/data-table-utils.ts",
"type": "registry:lib"
}
]
},
{
"name": "auth-form",
"type": "registry:block",
"title": "Auth Form",
"description": "Login and signup form with Zod validation and React Hook Form.",
"registryDependencies": ["button", "input", "label", "card"],
"dependencies": ["zod@^3.20.0", "react-hook-form", "@hookform/resolvers"],
"envVars": {
"NEXT_PUBLIC_AUTH_URL": "http://localhost:3000/api/auth"
},
"docs": "Set NEXT_PUBLIC_AUTH_URL in your .env.local to point to your auth endpoint.",
"files": [
{
"path": "registry/new-york/auth-form/auth-form.tsx",
"type": "registry:component"
}
]
},
{
"name": "dashboard-page",
"type": "registry:page",
"title": "Dashboard Page",
"description": "A full dashboard page with sidebar, header, and content area.",
"registryDependencies": ["sidebar", "hello-world", "data-table"],
"files": [
{
"path": "registry/new-york/dashboard-page/page.tsx",
"type": "registry:page",
"target": "app/dashboard/page.tsx"
}
]
}
]
}Quick Reference: The Full Workflow
| Step | Command / Action |
|---|---|
| 1. Create manifest | Create registry.json with name, homepage, items: [] |
| 2. Add component | Create file at registry/[style]/[name]/[name].tsx |
| 3. Register item | Add entry to items[] in registry.json |
| 4. Declare deps | Add registryDependencies and dependencies fields |
| 5. Install CLI | pnpm add shadcn@latest |
| 6. Add build script | "registry:build": "shadcn build" in package.json |
| 7. Build | pnpm registry:build → generates public/r/*.json |
| 8. Serve | pnpm dev → items at localhost:3000/r/[name].json |
| 9. Install item | npx shadcn@latest add http://localhost:3000/r/[name].json |
| 10. Publish | Deploy to Vercel / any host |