The Complete Guide to Setting Up a shadcn Registry

Letter

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.json

Prerequisites

  • Node.js 18 or later
  • A project that can serve static files over HTTP (Next.js used throughout this guide)
  • Basic familiarity with the shadcn CLI

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:

registry.json
{
  "$schema": "https://ui.shadcn.com/schema/registry.json",
  "name": "acme",
  "homepage": "https://acme.com",
  "items": []
}

Field reference

FieldRequiredDescription
$schemaPoints to the shadcn JSON schema — enables editor validation and autocomplete
nameA short identifier for your registry, used in data attributes and metadata
homepageThe public URL where your registry or site lives
itemsAn 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/.

registry/new-york/hello-world/hello-world.tsx
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:

registry.json
{
  "$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

FieldRequiredDescription
nameUnique slug for this item across your registry
typeItem-level type — see the type table below
titleHuman-readable name shown in the CLI and tooling
descriptionShort description — also helps LLMs understand the component
filesArray of file definitions with path and type
authorAuthor name and email: "Jane Doe <jane@acme.com>"
registryDependenciesOther registry items this item requires
dependenciesnpm packages this item requires
devDependenciesnpm dev packages needed during development
cssVarsCSS variables to inject into the consuming project
cssRaw CSS rules to add (@layer, @keyframes, @utility, etc.)
envVars.env variables to add to the consuming project
docsA message shown to the user after installation
categoriesTags for organising items: ["dashboard", "sidebar"]
metaArbitrary key/value metadata

Item type reference

The item-level type tells the CLI what kind of thing it is overall:

TypeUse for
registry:blockComplex multi-file components or full sections
registry:componentSimple single-purpose components
registry:uiUI primitives (single-file, like shadcn's own components)
registry:hookCustom React hooks
registry:libUtility functions and helpers
registry:pageFull page or file-based routes
registry:fileMiscellaneous files (configs, env files, etc.)
registry:themeFull theme definitions
registry:styleRegistry styles, e.g. new-york
registry:baseEntire design system base
registry:fontFont configurations
registry:itemUniversal/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 typeTarget directory
registry:componentcomponents/
registry:uicomponents/ui/
registry:hookhooks/
registry:liblib/
registry:pageapp/ or pages/ (requires target)
registry:fileAnywhere (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:

PlaceholderResolves 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.

registry.json (with dependencies)
{
  "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

registry-item cssVars
{
  "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

registry-item css
{
  "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

registry-item envVars
{
  "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

registry-item docs
{
  "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@latest

Add a build script to package.json

package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "registry:build": "shadcn build"
  }
}

Run the build

pnpm registry:build

The 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/registry

The generated JSON files inside public/r/ are what the CLI fetches when a developer runs shadcn add. Never edit them manually — always edit registry.json and re-run the build.


Step 7 — Serve the Registry

Start your dev server:

pnpm dev

Your 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.json

Step 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.json

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

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 nextConfig

Express.js setup

server.js
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.com

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

components.json (consumer project)
{
  "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-world

Install multiple items at once:

pnpm dlx shadcn@latest add @acme/header @acme/footer @acme/data-table

Multi-registry setups

Consumers can configure multiple registries in one project — for example organised by type, team, or stability:

components.json (multi-registry example)
{
  "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:

components.json (consumer, private registry)
{
  "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:

.env.local (consumer project)
INTERNAL_REGISTRY_TOKEN=your_token_here

The 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 deploy

Once 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 under registry/
  • name, type, description, and files are required on every item
  • Write clear title and description — they help LLMs understand your component
  • List every registry item dependency in registryDependencies
  • List every npm package in dependencies with 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 build and commit the updated registry.json every 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:

registry.json
{
  "$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

StepCommand / Action
1. Create manifestCreate registry.json with name, homepage, items: []
2. Add componentCreate file at registry/[style]/[name]/[name].tsx
3. Register itemAdd entry to items[] in registry.json
4. Declare depsAdd registryDependencies and dependencies fields
5. Install CLIpnpm add shadcn@latest
6. Add build script"registry:build": "shadcn build" in package.json
7. Buildpnpm registry:build → generates public/r/*.json
8. Servepnpm dev → items at localhost:3000/r/[name].json
9. Install itemnpx shadcn@latest add http://localhost:3000/r/[name].json
10. PublishDeploy to Vercel / any host

Further Reading