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.tsfiles and a generated__index__.tsxfor runtime resolution. - File placement: Store component source files in
apps/v4/registry/<style>/ui/and register them in the style-specific_registry.tsusing theregistryItemSchemastructure. - Build process: Run
pnpm run build-registry(or rely on watch mode) to regenerateapps/v4/registry/__index__.tsx, which maps names to lazy-loaded components. - Runtime consumption: Use
getRegistryComponent(name, style)fromapps/v4/lib/registry.tsto lazily load components, orgetRegistryItemto 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
fixFilePathsandfixImportto 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →