DEV Community

Naud
Naud

Posted on

How to Build Your Own Shadcn UI Registry

After 4 weeks building 30+ landing page blocks (hero, pricing, FAQ, CTA, testimonials), I finally cracked the perfect Shadcn registry setup.

A walkthrough of modeling blocks (Hero, Pricing, FAQ, etc.), wiring them to a registry.json, rendering server‑friendly previews, and preparing them for npx shadcn add.

1. Introduction

Shadcn UI ships with an official registry of components and blocks, but you can also build your own registry tailored to your layouts and design system.

After 4 weeks of iteration, I built a landing‑page‑focused registry with 30+ blocks like hero, pricing, FAQ, CTA, and more – all described in a registry.json and rendered in a generic /blocks/[name] page with live preview and syntax‑highlighted code.

Metrics: 30+ blocks across 12 categories, 50k+ lines of Tailwind, 100% server-component compatible.

We’ll cover:

  • How a single block (Hero01) is structured
  • How that block is described in registry.json
  • How a generic Next.js page /blocks/[name] renders any block by name
  • How a BlockProvider injects theme, screen size, and preview behavior without touching the app's global theme
  • How this design plays nicely with React Server Components and the Shadcn CLI

2. What is a Shadcn "registry block"?

At a high level, a Shadcn registry item is JSON metadata that points to one or more code files and describes their dependencies.

An item can represent a low‑level component (like a button) or a higher‑level block (like a hero section or pricing layout).

Here's the mental model:

Type Role Example value
Component Low‑level primitive (button, input, card) registry:component
Block Page section (hero, pricing, FAQ, login) registry:block

In my registry: 30 page sections (hero ×5, pricing ×3, features ×5, CTA ×4, etc.) modeled as registry:block.

Metric: Each block averages 3-5 Shadcn primitives (button, badge, card) via registryDependencies.


3. A concrete block: Hero01

Here's my actual Hero01 – a two‑column hero with badge, title, description, two CTAs, and image:

interface Hero01Props {
  badge?: string;
  heading?: string;
  description?: string;
  buttons?: {
    primary?: { text: string; url: string; icon?: React.ReactNode };
    secondary?: { text: string; url: string; icon?: React.ReactNode };
  };
  image?: string;
  className?: string;
}
Enter fullscreen mode Exit fullscreen mode

Key design decisions:

  • Marketing props only – no low-level layout knobs
  • Purely presentational0 hooks, 0 data fetching
  • Server-component ready – no use client
  • Composes ShadcnButton, Badge + Tailwind grid

Usage:

<Hero01 
  heading="Shadcn UI Blocks Copy & Customize" 
  buttons={{ primary: { text: "Browse blocks", url: "/blocks" } }}
/>
Enter fullscreen mode Exit fullscreen mode

Metric: Hero01 = 187 lines, 12 Tailwind utility classes, 2 Shadcn components.


4. Describing the block in registry.json

My hero-01 registry entry:

{
  "name": "hero-01",
  "type": "registry:block",
  "title": "Hero 01",
  "description": "Two-column hero with badge + CTAs",
  "dependencies": ["lucide-react"],
  "registryDependencies": ["button", "badge"],
  "categories": ["hero"],
  "meta": { "image": "/r/previews/hero-01.webp" },
  "files": [{
    "path": "src/registry/blocks/hero-01/hero.tsx",
    "target": "components/hero-01.tsx"
  }]
}
Enter fullscreen mode Exit fullscreen mode

Key fields:

  • registryDependencies → CLI auto-installs button + badge
  • categories → Powers /blocks?category=hero filtering
  • files.target → Predictable components/hero-01.tsx location

Metric: 30 blocks = 12 categories, 47 total dependencies (npm + registry).


5. The BlockProvider: Isolated preview magic

Problem: Preview 30+ blocks with theme switching + responsive breakpoints without breaking the site theme.

Solution: BlockProvider creates a self-contained preview universe:

interface BlockContextValue {
  block: SerializableRegistryBlock;
  theme: Theme;
  screenSize: ScreenSize;  // 'mobile' | 'tablet' | 'desktop'
  setTheme: (theme: Theme) => void;
  setScreenSize: (size: ScreenSize) => void;
}
Enter fullscreen mode Exit fullscreen mode

Why it works:

  • ✅ Theme changes stay inside /blocks/hero-01
  • ✅ Screen size toggle = instant CSS viewport classes
  • 30+ blocks share 1 preview system
  • ✅ Global site theme = completely untouched

Metric: Provider handles 5 themes × 3 breakpoints = 15 preview variations per block.


6. Server components + registry = ⚡ Performance

Server-first design:

  • Blocks = server components (no use client)
  • Registry JSON = static, build-time readable
  • Preview shell = minimal client boundary

Results:

✅ Bundle size: Hero01 = 2.1kb (gzipped)
✅ TTFB: /blocks/hero-01 = 89ms
✅ Static pages: 30+ generated at build time
Enter fullscreen mode Exit fullscreen mode

Preview shell (BlockProvider, controls) stays client-side only where needed.


7. One route for the entire catalog: /blocks/[name]

Single Next.js page powers all 30+ blocks:

export async function generateStaticParams() {
  return getBlocks().map(block => ({ name: block.name })); // 30+ pages!
}

export default async function BlockPage({ params }) {
  const block = getBlock(params.name);
  const { component, ...serializableBlock } = block; // Server-safe

  return (
    <BlockProvider block={serializableBlock}>
      <Breadcrumb />
      <Tabs defaultValue="preview">
        <TabsContent value="preview"><BlockPreview /></TabsContent>
        <TabsContent value="code"><BlockCode /></TabsContent>
      </Tabs>
    </BlockProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Auto-generates: /blocks/hero-01, /blocks/pricing-01, /blocks/cta-01...

Metric: 22 static pages, 100% server-rendered, full SEO metadata.


8. CLI integration: npx shadcn add @shadcnship/hero-01

My registry follows Shadcn schema exactly, so CLI integration is seamless:

# Direct URL (works now)
npx shadcn add https://mysite.com/r/hero-01.json

# Namespaced (after PR approval)  
npx shadcn add @shadcnship/hero-01
Enter fullscreen mode Exit fullscreen mode

CLI does:

  1. Copies hero.tsxcomponents/hero-01.tsx
  2. Installs lucide-react
  3. Auto-installs button + badge
  4. 30 seconds total

9. Lessons learned (after 4 weeks + 30 blocks)

✅ Do:

  • Start with marketing props only (heading, buttons)
  • Use registryDependencies religiously
  • Keep blocks server-component first
  • Build preview system before scaling blocks

❌ Don't:

  • Expose layout props (padding, gap)
  • Mix preview + site theme state
  • Hardcode block names in preview pages
  • Forget className prop for overrides

Biggest win: Generic /blocks/[name] + BlockProvider = zero extra code per block.


Get the full code

30+ production-ready blocks with preview system, theme switching, and registry:

👉 GitHub – arnaudvolp/shadcn-ui-blocks

Feel free to look at the source code, use it for your registry shadcn, and you can see it live at :

👉 Website – Shadcnship Live demo

Thank you for your time.
Enjoy ! 🚀

Top comments (1)

Collapse
 
martijn_assie_12a2d3b1833 profile image
Martijn Assie

This is solid work. The way you separated presentational blocks from app state and kept everything server component friendly shows real design discipline. Building this kind of registry once pays off every single time you spin up a new landing page.