How to Create Custom Components that Integrate with shadcn's Registry System

You can create custom components that integrate with shadcn's registry system by placing your component file in the appropriate registry folder, declaring it in a style-specific _registry.ts file, and letting the build script generate the runtime index that powers lazy loading and dependency resolution.

The shadcn/ui ecosystem uses a registry-based architecture to manage UI components as declarative items. When you create custom components that integrate with shadcn's registry system, you tap into automatic dependency tracking, lazy loading, and style-specific resolution without modifying core library code. This guide walks through the exact file paths, schema requirements, and runtime APIs used in the shadcn-ui/ui repository.

Understanding the Registry Architecture

shadcn/ui exposes every UI building block—components, hooks, blocks, and pages—as a declarative registry item. At build time, a script scans the registry folders (e.g., apps/v4/registry/...) and generates a map (Index) that the runtime library consumes to load components lazily, resolve file paths, and cache results.

Concept Description Location
Registry item definition JSON-like object describing name, type, dependencies, and source files apps/v4/registry/<style>/ui/_registry.ts
Index generation Auto-generated Index mapping style to item name to RegistryItem apps/v4/registry/__index__.tsx (generated by scripts/build-registry.mts)
Runtime helpers Functions like getRegistryComponent that read the index, load files, fix imports, and cache results apps/v4/lib/registry.ts
LRU cache In-memory cache to avoid repeated file I/O during development apps/v4/lib/registry.ts (lines 14-20)
File-path fixing Normalizes paths and target locations for generated files apps/v4/lib/registry.ts (fixFilePaths, getFileTarget)
Demo integration Optional demo component lookup that prefers examples when available apps/v4/lib/registry.ts (getDemoComponent, getDemoItem)

Step-by-Step Workflow to Create Custom Registry Components

When you add a new component, you only need to declare it in a registry file and place the implementation file in the matching folder. The build script picks it up automatically, and the runtime helpers expose it via getRegistryComponent or a direct import from the generated Index.

Step 1: Create the Component File

Place your component in the appropriate style folder. For the new-york-v4 style, this is apps/v4/registry/new-york-v4/ui/.

// apps/v4/registry/new-york-v4/ui/my-button.tsx
import * as React from "react"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/registry/new-york-v4/ui/button"

export interface MyButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  /** Optional visual variant */
  variant?: keyof typeof buttonVariants
}

/**
 * A thin wrapper around the default `button` component that demonstrates
 * how to add a custom UI element to the registry.
 */
export const MyButton = React.forwardRef<HTMLButtonElement, MyButtonProps>(
  ({ className, variant = "default", ...props }, ref) => (
    <button
      ref={ref}
      className={cn(buttonVariants({ variant }), className)}
      {...props}
    />
  )
)
MyButton.displayName = "MyButton"

The component lives next to other UI items so the path remains consistent with the registry structure.

Step 2: Register the Component in the Style-Specific Registry

Add an entry to the _registry.ts file for your target style. This file exports an array of Registry["items"] that the build script consumes.

// apps/v4/registry/new-york-v4/ui/_registry.ts
import { type Registry } from "shadcn/schema"

export const ui: Registry["items"] = [
  // … existing items
  {
    name: "my-button",
    type: "registry:ui",
    // optional list of external packages this component needs
    dependencies: [],

    // The file(s) that make up the component.
    files: [
      {
        path: "ui/my-button.tsx",
        type: "registry:ui",
      },
    ],
  },
]

The structure matches every other entry in the file. The schema is validated against registryItemSchema from packages/shadcn/schema/index.ts.

Step 3: Regenerate the Registry Index

Run the build script to scan all _registry.ts files and regenerate the runtime index.


# In a development environment the watch mode updates automatically.

# To run it manually:

pnpm run build-registry   # ← uses apps/v4/scripts/build-registry.mts

The script writes to apps/v4/registry/__index__.tsx, creating a map that associates style names with lazy-loaded component factories.

Step 4: Consume the Component in Your Application

Use the runtime helper getRegistryComponent to lazily load your custom component.

// app/example/page.tsx
import * as React from "react"
import { getRegistryComponent } from "@/lib/registry"

export default async function ExamplePage() {
  // Lazily load the component from the registry
  const MyButton = await getRegistryComponent("my-button", "new-york-v4")

  return (
    <div className="p-8">
      <h1 className="mb-4 text-2xl font-bold">Custom Registry Component</h1>

      {/* The returned value is a React component */}
      <MyButton variant="outline">Click me</MyButton>
    </div>
  )
}

Internally, getRegistryComponent (lines 75-84 in apps/v4/lib/registry.ts) checks for a demo component first, then falls back to the generated Index:

export function getRegistryComponent(name: string, styleName: string) {
  const demoComponent = getDemoComponent(name, styleName)
  if (demoComponent) return demoComponent

  const { index, key } = getIndexForStyle(styleName)
  return index[key]?.[name]?.component
}

Accessing Raw Registry Definitions for Code Generation

If you need to inspect the component's source files or metadata—for example, to build a code-generation tool—use getRegistryItem instead of the component loader.

import { getRegistryItem } from "@/lib/registry"

async function inspect() {
  const item = await getRegistryItem("my-button", "new-york-v4")
  console.log(item?.files) // → [{ path: ".../ui/my-button.tsx", content: "...", ... }]
}

This function (lines 85-115 in apps/v4/lib/registry.ts) reads the source file, fixes imports using the internal fixImport utility, and returns a fully-typed object validated against registryItemSchema.

Key Files in the Registry System

File Role Link
apps/v4/lib/registry.ts Runtime helpers (getRegistryComponent, caching, import fixing) registry.ts
apps/v4/registry/new-york-v4/ui/_registry.ts Style-specific UI item declarations – where you add your component entry _registry.ts
apps/v4/registry/__index__.tsx Auto-generated map consumed by the runtime; created by the build script index.tsx
apps/v4/scripts/build-registry.mts Scans all _registry.ts files and writes __index__.tsx build-registry.mts
packages/shadcn/schema/index.ts Zod schema for registry items (registryItemSchema) used for validation schema/index.ts

These files together constitute the registry system. By placing a new component file in the appropriate folder and adding a matching entry to the corresponding _registry.ts, you seamlessly integrate custom UI pieces into shadcn’s ecosystem.

Summary

  • Registry-based architecture: shadcn/ui uses declarative registry items defined in _registry.ts files and a generated __index__.tsx for runtime resolution.
  • File placement: Store component source files in apps/v4/registry/<style>/ui/ and register them in the style-specific _registry.ts using the registryItemSchema structure.
  • Build process: Run pnpm run build-registry (or rely on watch mode) to regenerate apps/v4/registry/__index__.tsx, which maps names to lazy-loaded components.
  • Runtime consumption: Use getRegistryComponent(name, style) from apps/v4/lib/registry.ts to lazily load components, or getRegistryItem to access raw file content and metadata for code generation.
  • Caching and path fixing: The runtime uses an LRU cache (lines 14-20) and internal utilities like fixFilePaths and fixImport to normalize paths and handle cross-style imports.

Frequently Asked Questions

What is the purpose of the _registry.ts file?

The _registry.ts file acts as the declarative manifest for a specific style (e.g., new-york-v4). It exports an array of registry items that describe component names, types, dependencies, and file paths. The build script scans these files to generate the runtime __index__.tsx, making _registry.ts the single source of truth for what components are available in a given style.

How does the build-registry script work?

Located at apps/v4/scripts/build-registry.mts, the script recursively scans all _registry.ts files across registry folders. It validates entries against registryItemSchema from packages/shadcn/schema/index.ts, resolves file paths, and generates apps/v4/registry/__index__.tsx. This generated file exports an Index object that maps style names to lazy-loaded component factories, enabling the runtime helpers to resolve components without dynamic imports at the call site.

Can I use custom components without regenerating the index?

No. The runtime helpers in apps/v4/lib/registry.ts consume the generated __index__.tsx to resolve component paths and metadata. If you add a component to _registry.ts but do not regenerate the index, the Index object will not contain the new entry, and getRegistryComponent will return undefined. You must run pnpm run build-registry or rely on the watch mode to update __index__.tsx.

What is the difference between getRegistryComponent and getRegistryItem?

getRegistryComponent (lines 75-84 in apps/v4/lib/registry.ts) returns a React component that is ready to render. It first checks for a demo component, then falls back to the lazy-loaded component from the generated Index. In contrast, getRegistryItem (lines 85-115) returns the raw registry metadata, including file contents, paths, and dependencies. Use getRegistryComponent for rendering UI, and getRegistryItem when building code-generation tools or documentation that needs to inspect source code.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →