I’m increasingly convinced that for developer-first tools, a really good docs experience is a durable, non-trivial advantage.

Part of this thesis is that Really Good Docs Experiences, in addition to having great information architecture and strong writing/prose, should be thought of less as ancillary content repositories that can be farmed out to whatever your helpdesk software is and more as important web-apps in their own right.


Every now and then, when a docs site does something cool I try to figure out how they do it. Take shadcn/ui, for instance — how are all of these previews being generated based on Typescript? It’d be one thing if the components were HTML — you can just throw them into an iFrame — but clearly there’s something more complicated going on here.

Blessedly, these docs are open source, so I can find out exactly how. Let’s take that example Notifications card for example:

That page is being rendered in MDX; there’s a generic ComponentPreview component rendering it:

<ComponentPreview
  name="card-demo"
  description="A card showing notifications settings."
/>

It looks like that ComponentPreview component is just proxying out to some big __registry__:

import { Icons } from "@/components/icons";

// ...

const Preview = React.useMemo(() => {
  const Component = Index[config.style][name]?.component;

  if (!Component) {
    return (
      <p className="text-sm text-muted-foreground">
        Component{" "}
        <code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm">
          {name}
        </code>{" "}
        not found in registry.
      </p>
    );
  }

  return <Component />;
}, [name, config.style]);

The component in the registry is right here, and that’s the exact source code that we see. But how is the registry aware of its constituents?

Because it’s autogenerated! There’s a scripts/build-registry.ts file. This build file is gnarly (understandably so), but at a high level it spits out this massive export file:

    "card-demo": {
      name: "card-demo",
      type: "components:example",
      registryDependencies: ["card","button","switch"],
      component: React.lazy(() => import("@/registry/default/example/card-demo")),
      source: "",
      files: ["registry/default/example/card-demo.tsx"],
      category: "undefined",
      subcategory: "undefined",
      chunks: []
    },

And it finds the things to populate in that registry via a manually-enumerated list of potential components:

  {
    name: "card-demo",
    type: "components:example",
    registryDependencies: ["card", "button", "switch"],
    files: ["example/card-demo.tsx"],
  },

So, in sum, walking our way back to the final artifact:

  1. shadcn has a big, manually-created list of example files (and associated dependencies/metadata.)
  2. A build-time script analyzes that list and autogenerates both an augmented code snippet for that example file and a big index that allows the site to import that snippet live.
  3. A previewer component pulls in that autogenned snippet and its raw-source equivalent.
  4. That previewer component is declared by an mdx file.

Nothing magical; nothing complex. But certainly bespoke.

Lightning bolt
About the author

I'm Justin Duke — a software engineer, writer, and founder. I currently work as the CEO of Buttondown, the best way to start and grow your newsletter, and as a partner at Third South Capital.

Lightning bolt
Greatest hits

Lightning bolt
Elsewhere

Lightning bolt
Don't miss the next essay

Get a monthly roundup of everything I've written: no ads, no nonsense.