Skip to content

Blocks

114 copy-paste sections in 14 categories, each a real file whose source is read from disk at build time.

What a block is

One file, one catalog entry, one rendered preview, one visible source. The code shown beside each block at /blocks is read from that file by a Server Component during the build — not a copy maintained alongside it — so what you copy is what renders above it. There is no way for the two to drift.

That definition also makes the count a fact rather than a claim. The index renders catalog.length, and src/lib/blocks/blocks.test.ts asserts the three-way match in both directions — so the number cannot be inflated by metadata for a block that does not exist, nor deflated by a file nobody catalogued.

Adding a block

// 1. The file. "use client" is required — the previews render from a
//    Server Component, and most blocks use state or Recharts.
//    src/components/blocks/stats/stat-sparkline-row.tsx
"use client";
export function StatSparklineRow() { /* … */ }

// 2. The catalog entry. src/lib/blocks/catalog.ts
{
  id: "stat-sparkline-row",
  name: "Sparkline row",
  category: "stats",
  description: "Eight trends visible at once, for a page with no room for cards.",
  file: "components/blocks/stats/stat-sparkline-row.tsx",
},

// 3. The category registry. src/lib/blocks/registry.stats.ts
export const statsRegistry: BlockRegistry = {
  ...,
  "stat-sparkline-row": StatSparklineRow,
};

Miss step 2 or 3 and the test names exactly what is missing. Miss the "use client" and it is caught by name too, rather than surfacing as a hooks error pointing at a primitive.

Why one registry per category

A single registry holding every block would be imported by every category page, so opening Stats would download the charts and the tables too — the catalog would get slower with each block added, which is the opposite of what a growing library should do. So catalog.ts holds pure metadata (feeding the index, the counts, ⌘K and the sitemap) and each registry.<category>.ts holds only its own components.

That is also why there is one route file per category rather than a [category] dynamic route: a dynamic route compiles to one shared page component, which would have to import every registry to switch on the parameter.

Adding a category

// src/lib/blocks/catalog.ts — the id becomes the route segment
export const BLOCK_CATEGORIES = [
  ...,
  { id: "navigation", name: "Navigation", description: "Tabs, steps and breadcrumbs." },
] as const;

// src/lib/blocks/registry.navigation.ts
export const navigationRegistry: BlockRegistry = { /* … */ };

// src/app/(dashboard)/blocks/navigation/page.tsx — six lines
export default function Page() {
  return <BlockCategoryPage category="navigation" registry={navigationRegistry} />;
}

The sitemap picks it up automatically: the block category routes are derived from BLOCK_CATEGORIES, not listed by hand.

Two limits worth knowing

  • Keep a category near a dozen blocks.Each block's source adds roughly 3KB gzipped to its category page — small, but linear. A five-block chart category is already 44KB gzipped.
  • The previews are not responsive previews. Blocks use Tailwind viewport breakpoints, so a max-width wrapper cannot trigger them — a width toggle would narrow the box while the block still rendered its desktop layout, which is a preview that lies. Resize the window instead.