← Back to blog

The shadcn Registry Protocol: A Technical Deep Dive

·12 min read

The shadcn registry protocol is one of those infrastructure decisions that looks simple from the outside and reveals surprising depth when you dig in. It is how shadcn/ui distributes components as source code instead of compiled packages. It is also the mechanism that makes AI-native component registries like kit possible.

This post covers the protocol in technical detail: how the JSON schema works, how the CLI resolves dependencies, how the install process writes files into your project, and what you need to build your own registry. The target is developers who want to understand the plumbing — either to extend it or to build on top of it.

Why source code distribution matters

Before getting into the mechanics, it helps to understand the design decision. Traditional component libraries ship as npm packages. You install them, import the exported types and components, and your code depends on the package version forever. Customizing beyond the exposed API means either patching the package (fragile) or forking it (expensive to maintain).

shadcn/ui chose a different model: copy the source into your project. You run an install command, the component files land in your codebase, and from that point on you own them. There is no runtime dependency on the registry. No version pinning. No update pressure. Just files.

The tradeoff is that you also get the maintenance burden. But that tradeoff is worth it for design-system-adjacent code, where you almost always need to diverge from the upstream defaults eventually. And for AI coding tools, it matters for a different reason: the model can read files in your project. It cannot read inside node_modules. Source-installed components are visible to the tools generating code against them.

The registry schema

A registry is a collection of JSON files served over HTTP. There are two schema types: a registry manifest and individual registry items.

The registry manifest (validated against https://ui.shadcn.com/schema/registry.json) describes the registry itself and its items:

{
  "$schema": "https://ui.shadcn.com/schema/registry.json",
  "name": "n3wth",
  "homepage": "https://kit.newth.ai",
  "items": [
    {
      "name": "button",
      "type": "registry:ui",
      "title": "Button",
      "description": "Multi-variant button with responsive sizes.",
      "registryDependencies": ["cn"],
      "files": [
        {
          "path": "registry/new-york/button/button.tsx",
          "type": "registry:ui"
        }
      ]
    }
  ]
}

The manifest is the source of truth during the build step. A build script reads it and generates the per-item JSON files that the CLI actually fetches. The manifest is for authoring; the individual item files are for distribution.

A registry item (validated against https://ui.shadcn.com/schema/registry-item.json) is what the CLI fetches. The key difference from the manifest entry is that it includes the full file content inline:

{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "button",
  "title": "Button",
  "description": "Multi-variant button with responsive sizes.",
  "type": "registry:ui",
  "registryDependencies": ["cn"],
  "files": [
    {
      "path": "registry/new-york/button/button.tsx",
      "content": "import { forwardRef } from 'react'\n// ... full source",
      "type": "registry:ui"
    }
  ]
}

The file content is embedded as a string. The CLI does not need to make a separate request per file. One fetch per registry item is all it takes.

Item types

The type field controls both how the CLI categorizes the item and where it writes the files. The defined types are:

registry:style      // Design system base (CSS vars, global styles)
registry:lib        // Utility functions (cn, formatters, hooks)
registry:ui         // UI components
registry:block      // Multi-file page sections or patterns
registry:component  // Single-purpose standalone components
registry:page       // Full page templates
registry:file       // Arbitrary files (config, types, etc.)

In practice, registry:ui and registry:lib cover the majority of use cases. registry:style is special: it installs CSS variables and global styles rather than component files, which is how design system tokens get distributed.

Dependency resolution

The two dependency fields serve different purposes:

{
  "dependencies": ["clsx", "tailwind-merge"],
  "registryDependencies": ["cn", "button"]
}

dependencies are npm packages. The CLI checks whether they are already in package.json and installs any that are missing. This is a straightforward npm install step.

registryDependencies are other registry items. When the CLI encounters these, it resolves them against the same registry. If you install a pricing-card that depends on button and cn, the CLI fetches and installs all three. Resolution is recursive — a dependency can have its own dependencies.

You can also reference items from the shadcn/ui base registry by name. If your component depends on a shadcn primitive, the CLI knows to fetch it from ui.shadcn.com rather than your registry.

The install flow in detail

Running the install command triggers a multi-step process:

npx shadcn add https://kit.newth.ai/r/pricing-card.json

Step by step:

  1. Fetch the item JSON — the CLI fetches the URL and parses the registry-item JSON. It validates the schema and extracts the item fields.
  2. Resolve registryDependencies — for each name in registryDependencies, the CLI constructs a URL (same base URL, item name as filename) and fetches that item too. This recurses until all dependencies are resolved.
  3. Read components.json — the CLI reads your project's components.json to determine the target directory and path aliases.
  4. Write files — each file in the resolved item set is written to the configured component directory. The path field in the item is used as a hint but the actual destination is controlled by your components.json config.
  5. Install npm dependencies — any packages in dependencies that are not already in package.json get added via your package manager.

The entire flow is stateless from the registry's perspective. There is no install ledger, no user accounts, no API keys. The CLI just fetches JSON and writes files.

How kit builds the registry

kit's registry lives in a Next.js project. The source components are in registry/ and the manifest is in registry.json. A build script reads the manifest, reads each referenced source file, and generates the per-item JSON files in public/r/. The Next.js app serves them as static files.

# Source
registry/
  lib/cn/cn.ts
  new-york/
    button/button.tsx
    badge/badge.tsx
    pricing-card/pricing-card.tsx
registry.json

# Built output
public/r/
  cn.json          # { name, type, files: [{ content: "..." }] }
  button.json
  badge.json
  pricing-card.json

The build script inlines the file content into each item JSON. After that, the static files are immutable until the next build. Deployment to Vercel gives you a CDN-backed static registry with zero server infrastructure.

Here is the structure of the build script pattern:

import { readFileSync, writeFileSync } from 'fs'
import { registry } from './registry.json'

for (const item of registry.items) {
  const files = item.files.map((file) => ({
    path: file.path,
    type: file.type,
    content: readFileSync(file.path, 'utf-8'),
  }))

  const registryItem = {
    $schema: 'https://ui.shadcn.com/schema/registry-item.json',
    name: item.name,
    title: item.title,
    description: item.description,
    type: item.type,
    dependencies: item.dependencies,
    registryDependencies: item.registryDependencies,
    files,
  }

  writeFileSync(
    `public/r/${item.name}.json`,
    JSON.stringify(registryItem, null, 2)
  )
}

Building your own registry

The protocol is open and the schema is public. Building your own registry requires three things: component source files, a build script that produces item JSON, and a server to host the JSON files.

The minimal viable registry is a static folder of JSON files on any web host. You do not need Next.js, a database, or a build pipeline. You can author the item JSON manually if your registry is small:

# Minimal registry structure
registry/
  components/
    my-button.tsx
  r/
    my-button.json   # authored by hand or generated

For a production registry with more than a handful of components, you want a build step. The key requirements are:

  • Single source of truth — the manifest drives the build. Do not manually maintain per-item JSON.
  • Content inlining — file content must be embedded in the item JSON, not referenced by path. The CLI cannot follow links back to your source tree.
  • Schema validation — validate your output against the schema before deploying. A malformed item JSON will silently fail to install in ways that are hard to debug.
  • Correct CORS headers — the CLI fetches your JSON from the user's machine, which means your server needs CORS configured for cross-origin requests. For static files on Vercel or Cloudflare, this is on by default.

The style item: distributing design tokens

The registry:style type is worth examining separately because it works differently from component items. Instead of writing TypeScript files, it injects CSS variables and configuration into your project.

{
  "name": "n3wth",
  "type": "registry:style",
  "description": "Flat, minimal design system tokens.",
  "cssVars": {
    "theme": {
      "font-sans": "Inter, system-ui, -apple-system, sans-serif",
      "radius": "0.625rem"
    },
    "light": {
      "background": "0 0% 100%",
      "foreground": "0 0% 3.9%",
      "primary": "0 0% 9%",
      "border": "0 0% 89.8%"
    },
    "dark": {
      "background": "0 0% 0%",
      "foreground": "0 0% 98%",
      "primary": "0 0% 98%",
      "border": "0 0% 14.9%"
    }
  },
  "files": []
}

When a style item is installed, the CLI writes these values into your globals.css or equivalent, following the Tailwind CSS variable convention. The component items in the registry then reference these variables using the standard hsl(var(--background)) pattern.

This is how a registry distributes a complete design system in one install command. Install the style first, then install individual components that reference the CSS variables the style defined. All components will pick up the correct tokens automatically.

What the protocol does not handle

The registry protocol is deliberately narrow. It handles one thing: distributing files. It does not handle:

  • Versioning — there is no version field in the schema. If you want versioning, you need to manage it out-of-band (different URLs, a version query param, or a separate registry manifest per version).
  • Updates — reinstalling a component overwrites the file. There is no diff or merge. This is intentional: you own the source, so updates are a manual decision.
  • Authentication — the CLI fetches public URLs. Private registries need to be handled at the network level (VPN, private CDN) not at the protocol level.
  • AI context — the protocol distributes source code. Teaching AI tools how to use that source code is a separate problem.

Extending the protocol for AI tools

AI coding tools have a different requirement than the shadcn CLI: they need to understand how to use a component, not just install it. A component's source tells the model the prop interface. But it does not tell the model which variant to use in which context, what constraints the design system enforces, or which components should be composed with which.

kit extends the registry pattern with an AI context layer. Each component has an associated context description that travels with the source. The context describes prop semantics, usage constraints, and composition patterns in a format optimized for language model consumption:

# Button component context (in .cursorrules / CLAUDE.md)

## Button
Props: variant, size, isLoading, leftIcon, rightIcon, asChild, touchTarget

Variants:
- primary: main CTA only. One per view.
- secondary: supporting actions
- ghost: tertiary, navigation contexts
- glass: over image/video backgrounds only

Constraints:
- Do not add custom className for sizing — use the size prop
- Use touchTarget={true} for mobile primary actions (WCAG 2.5.5)
- Loading state: set isLoading, do not disable manually
- Never nest interactive elements inside Button

This context is installed alongside the component source. From that point on, every AI tool that reads the context file understands not just the component's API but how to use it correctly in your design system.

The result is AI-generated code that references your actual components, uses the correct variants, and respects your design constraints — instead of reaching for shadcn defaults because that is what the training data contains.

Protocol summary

The shadcn registry protocol is:

  • A JSON schema for describing components and their dependencies
  • A URL convention: serve item JSON at https://your-registry.com/r/[name].json
  • A CLI that fetches, resolves dependencies, and writes files into the project
  • A style item type for distributing CSS variables alongside components

The entire system is stateless, public, and requires no special infrastructure. You can run a registry from a static file host. The protocol is open: no registry approval process, no npm account required.

If you are building a design system that other people (or AI tools) will use, this is the distribution mechanism to understand. It is what ships components as source code. Everything else — versioning, AI context, documentation — you build on top.

kit uses the shadcn registry protocol to distribute AI-native components: source code you own, plus a context pack that teaches your AI tools to use them correctly.

Build with kit