Table of Contents
- Motivation and planning
- Setting up the Next.js project
- Organising components in the registry
- Defining the registry index (registry.json)
- Building the registry and generating JSON files
- Deploying to Vercel
- Conclusion
- Resources/Links
1. Motivation and planning
I decided to create a custom Shadcn UI component registry to easily reuse and share my own UI components across projects. As a full-stack developer, I’m passionate about building beautiful, accessible, and performant web apps, and I often craft custom components that I want to use in multiple projects. The Shadcn UI framework provides a CLI for installing components from a remote registry, so running my own registry made perfect sense. It would allow me to distribute a collection of my favourite UI patterns and components (like complex form fields, interactive blocks, etc.) through a central repository.
Before coding, I read through the Shadcn UI documentation on custom registries to understand the requirements. Essentially, a registry is just a web app (in my case, a Next.js app) that serves JSON definitions of components. The only hard requirement is that it must provide a registry.json file at its root URL (the index of all components), and individual JSON files for each component, conforming to Shadcn’s schema. With this in mind, I planned out the project structure and technology stack:
- Next.js 13 with the App Router for the application framework.
- Tailwind CSS v4 for styling, since the Shadcn template is configured for Tailwind 4.
- shadcn/ui CLI and registry template as a starting point, to get the correct file structure and scripts for building the registry.
- Deployment on Vercel, to host the registry at a public URL where the CLI (and other developers) can reach the JSON files.
2. Setting up the Next.js project
To kickstart development, I used the official Shadcn/UI registry
After cloning the template, I updated the project metadata. I changed the name and homepage fields in registry.json to reflect my project (giving my registry a unique name and linking to its website). I then removed the scaffolded templates in registry/new-york/blocks/
and registry/new-york/ui/
and updated app/page.tsx
with my own homepage content. I also verified that Tailwind CSS was set up correctly (the template was already using Tailwind v4, which I stuck with).
3. Organising components in the registry
With the scaffolding ready, I started adding my own components into the registry. The project uses a specific folder structure for registry items. All components live under the registry/ directory, grouped by a namespace (the template uses a default namespace called “new-york”
). Under that, I organised components by category.
For example, I created a folder:
registry/
└── new-york/
└── blocks/
└── landing-page/
├── landing-page.tsx
├── components/
│ ├── announcement.tsx
│ ├── marquee.tsx
│ └── video-player.tsx
└── README.md
Here, landing-page is a Block component; essentially a full page section composed of smaller parts. Inside its folder, I have a landing-page.tsx
which assembles the block’s JSX, and a components folder containing dependency components. In this case, the dependency components include an announcement banner, a scrolling marquee text, and an embedded video player. I wrote each of those pieces as separate React components (announcement.tsx
, marquee.tsx
, video-player.tsx
) to keep things modular.
Organising components this way (each in its own folder with a clear structure) made it easy to manage and document them. I also added a README.md
inside the landing-page folder, describing what the block and components do and how to use them. This README content is displayed on the registry site as documentation for that component.
To illustrate, here’s a simplified version of the marquee component that I included in the landing page block:
// registry/new-york/blocks/landing-page/components/marquee.tsx
"use client"
import React from "react"
export function Marquee({ text }: { text: string }) {
return (
<div className="overflow-hidden whitespace-nowrap bg-muted px-4 py-2">
<span className="animate-marquee text-sm font-medium">
{text}
</span>
</div>
);
}
This Marquee component simply takes text and scrolls it (assuming a Tailwind CSS animation class animate-marquee is defined). I created similar React components for Announcement (perhaps a top banner with a message) and VideoPlayer (using an HTML5 video or iframe).
Then, in landing-page.tsx
of the landing page block, I put these pieces together:
// registry/new-york/blocks/landing-page/landing-page.tsx
import { Announcement } from "@/components/announcement"
import { Marquee } from "@/components/marquee"
import { VideoPlayer } from "@/components/video-player"
export default function LandingPage() {
return (
<section className="landing-page">
<Announcement message="Welcome to my custom registry!" />
<Marquee text="Building with Shadcn UI is fun" />
<VideoPlayer src="/demo.mp4" />
</section>
);
}
Notes:
- The above code is a representative example for illustration. In my actual code, the content and props were tailored to my needs.
- Use path aliases, not relative paths. In your registry source and item files, import from
@/components/...
instead of./components/...
so the generated code resolves correctly when a consumer installs it into a different folder structure. Relative imports are tied to your registry’s local file layout; once the files are copied into another project, those./
paths can break because the install location (e.g.,app/
,components/
, or a different nesting) won’t match your repo. The@
alias is project-rooted and is what most Next.js/shadcn setups expect, so it remains stable across projects.
Bad (relative, may break after install):
import { Button } from "./components/ui/button"
import { Announcement } from "./components/announcement"
Good (alias, portable):
import { Button } from "@/components/ui/button"
import { Announcement } from "@/components/announcement"
4. Defining the registry index (registry.json)
The heart of the registry is the registry.json file in the project root. This file serves as an index of all components/blocks available. Each entry in registry.json lists a component’s name, type, title, and the files that make it up, among other metadata.
After adding my components, I edited registry.json to register them. For example, I added an entry for the Landing Page block like this:
{
"$schema": "https://ui.shadcn.com/schema/registry.json",
"name": "Lynqsides Shadcn Registry",
"homepage": "https://shadcn-elvinlari-registry.vercel.app",
"items": [
{
"name": "landing-page",
"type": "registry:block",
"title": "Landing Page",
"description": "A full landing page section with announcement banner, marquee, and video player.",
"dependencies": ["@icons-pack/react-simple-icons", "@number-flow/react", "media-chrome", "react-fast-marquee"],
"registryDependencies": ["button", "card", "badge", "tabs"],
"files": [
{
"path": "registry/new-york/blocks/landing-page/landing-page.tsx",
"type": "registry:block"
},
{
"path": "registry/new-york/blocks/landing-page/components/announcement.tsx",
"type": "registry:ui"
},
{
"path": "registry/new-york/blocks/landing-page/components/marquee.tsx",
"type": "registry:ui"
},
{
"path": "registry/new-york/blocks/landing-page/components/video-player.tsx",
"type": "registry:ui"
}
],
"cssVars": {
"theme": {
"font-heading": "Poppins, sans-serif"
},
"light": {
"brand": "20 14.3% 4.1%",
"radius": "0.5rem"
},
"dark": {
"brand": "20 14.3% 4.1%"
}
},
"css": {
"@layer base": {
"html": {
"scroll-behavior": "smooth"
}
}
}
},
// ... other items ...
]
}
I set "type": "registry:block"
because the landing-page
is a composite, full-page section rather than a single UI primitive. Under dependencies
, I list external npm packages the consumer must get (@icons-pack/react-simple-icons
, @number-flow/react
, media-chrome
, react-fast-marquee
). Under registryDependencies
, I declare other shadcn items this block relies on (button
, card
, badge
, tabs
); the CLI will fetch those from the upstream registry, so the block works out of the box. The files
array is the install payload: one entry for the orchestrating block file (registry:block
) and entries for each supporting piece in components/
(registry:ui
). This tells the CLI exactly which files to copy into the target app and their default installation location.
You can also ship design tokens via cssVars
: a global theme token for the heading font (Poppins, sans-serif
), plus light/dark palette values like brand
(HSL triplet) and a radius
for rounding. These integrate with shadcn/Tailwind’s variable-driven colours (e.g., bg-background
, text-foreground
, ring
, etc.). Finally, I appended a global style through css
by contributing to @layer base
to enable smooth scrolling on html
. Together, this entry ensures the block, its subcomponents, its npm deps, its shadcn deps, and its theme/base CSS all install and “just work” when someone runs shadcn add
against my registry.
5. Building the registry and generating JSON files
Once the code for components and the registry.json
index were in place, I used the Shadcn CLI to build the registry. The command:
pnpm registry:build
scans the registry.json
and the referenced component files, and produces the distributable JSON files for each item. After running this, my Next.js app’s public directory was populated with a JSON file per component (as well as an overall registry.json
for the index) that will be served to users. For example, public/r/landing-page.json
was generated, containing the combined content and metadata of the Landing Page component. The CLI automates this conversion from my source files to the JSON schema that other projects can consume. Having these as static files means the registry doesn’t need a live database. Any request to
Note: The template provided a dynamic Next.js route handler for registry items, but since we output static files, the dynamic route simply proxies those or handles any special cases. In practice, I found the static files sufficient for all my components.
6. Deploying to Vercel
With everything tested locally, I was ready to deploy. I pushed the final code to the GitHub repo and connected it to Vercel for continuous deployment. The deployment process on Vercel was straightforward: I set up a new project, pointed it to my GitHub repository, and Vercel handled installing dependencies and building the Next.js app. Since this is a Next.js App Router project, Vercel recognized it and optimized the build for me.
I ended up doing a few quick-fix commits to adjust the domain value once the app was live (making sure there were no leftover references to a localhost or template example domain). Those “fix: edit domain” commits in my history were exactly for this.
After a successful deployment, I visited my registry’s
Finally, I tested the integration end-to-end. I went to a separate test Next.js project and tried installing a component from my registry using the Shadcn CLI. I ran the following command:
pnpm dlx shadcn@latest add https://shadcn-elvinlari-registry.vercel.app/r/landing-page.json
This command pulls landing-page.json
from my public registry, copies all referenced files into the project (mirroring my registry’s folder structure), and installs both the npm dependencies and shadcn registryDependencies the block needs. It also applies any cssVars
and merges the item’s css
into the project’s global styles (or writes the shipped CSS file if I included one). Result: my custom components were ready to use immediately, with code and styles fully wired up.
Conclusion
In summary, I built my own Shadcn UI registry by leveraging Next.js and the Shadcn registry template. I authored custom components, defined them in a registry.json
index, and used the Shadcn CLI’s build process to generate JSON specs for each component . The project doubles as a documentation site where I (and others) can browse and preview components, and it serves as a distribution mechanism so that any React project can install these components via one CLI command. Deploying on Vercel made it easy to share my registry with the world — it’s fast and globally accessible.
This was a really fun project because it combined my love for building reusable components with the excitement of creating a platform others can use. I now have a personal UI library, “Lynq Components”, that is just a pnpm dlx shadcn add
away for any of my future projects. Feel free to check out the registry (
I hope this has been a helpful guide. Thank you for reading!