Table of Contents

  1. Motivation and planning
  2. Setting up the Next.js project
  3. Organising components in the registry
  4. Defining the registry index (registry.json)
  5. Building the registry and generating JSON files
  6. Deploying to Vercel
  7. Conclusion
  8. 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:

2. Setting up the Next.js project

To kickstart development, I used the official Shadcn/UI registry template as my base . This template already had a Next.js project configured for a registry, which saved a lot of setup time. I initialised my project from the template and pushed the initial code to GitHub as elvinlari/shadcn-registry. The template project came with a default registry.json and some example components to demonstrate the structure (like a “hello-world” component). It also included the necessary Tailwind config, Next.js config, and a build script. Using this template ensured my registry would meet Shadcn’s schema and file layout from the start.

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.tsxmarquee.tsxvideo-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:

  1. The above code is a representative example for illustration. In my actual code, the content and props were tailored to my needs.
  2. 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/reactmedia-chromereact-fast-marquee). Under registryDependencies, I declare other shadcn items this block relies on (buttoncardbadgetabs); 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-backgroundtext-foregroundring, etc.). Finally, I appended a global style through css by contributing to @layer baseto 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 https://shadcn-elvinlari-registry.vercel.app/r/landing-page.json will just return the JSON describing the component (Next.js serves it like a static asset). This static approach makes hosting easy and caching efficient.

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 URL and saw the documentation site up and running. The homepage of my registry site includes a list of components available and an introduction (I added a personal About the Author section with my social links, to give it a nice personal touch). Each component has its own page showing how to use it and a preview of the component in action. For example, the Landing Page block’s page displays the announcement banner, marquee, and video player demo, as well as the code snippets.

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 (https://shadcn-elvinlari-registry.vercel.app) and reach out if you have any questions or suggestions. Happy coding!

I hope this has been a helpful guide. Thank you for reading!

https://ui.shadcn.com/docs— Shadcn/UI Documentation
https://github.com/shadcn-ui/registry-template.git—Shadcn/UI Registry Template
https://github.com/elvinlari/shadcn-registry.git—My Custom Registry Code
https://shadcn-elvinlari-registry.vercel.app —My Deployed Shadcn Registry