FairGarden Docs

Build-time optimized documentation infrastructure for React and Next.js. Live demos, API references and search are computed once in your build, so readers get plain HTML that enhances itself.

pnpm install @fairgarden/docs
8 petals
'use client';

import * as React from 'react';
import { Slider } from '@base-ui/react/slider';
import { Flower } from '@/components/Flower';
import styles from './PetalSlider.module.css';

interface PetalSliderProps {
  /** Petal count the slider starts at. */
  defaultPetals?: number;
}

export function PetalSlider({ defaultPetals = 8 }: PetalSliderProps) {
  const [petals, setPetals] = React.useState<number>(defaultPetals);

  return (
    <div className={styles.root}>
      <Flower petals={petals} label={`A flower with ${petals} petals`} />
      <Slider.Root min={3} max={16} value={petals} onValueChange={setPetals}>
        <output className={styles.value}>{petals} petals</output>
        <Slider.Control className={styles.control}>
          <Slider.Track className={styles.track}>
            <Slider.Indicator className={styles.indicator} />
            <Slider.Thumb className={styles.thumb} aria-label="Petals" />
          </Slider.Track>
        </Slider.Control>
      </Slider.Root>
    </div>
  );
}
PetalSlider.module.css
.root {
  display: grid;
  justify-items: center;
  gap: var(--size-4);
}

.value {
  display: block;
  text-align: center;
  font-weight: var(--font-weight-6);
  color: var(--purple-12);
}

.control {
  display: flex;
  align-items: center;
  width: min(280px, 60vw);
  padding-block: var(--size-3);
  touch-action: none;
}

.track {
  width: 100%;
  height: var(--size-1);
  border-radius: var(--radius-round);
  background-color: var(--purple-5);
}

.indicator {
  border-radius: var(--radius-round);
  background-color: var(--purple-9);
}

.thumb {
  width: var(--size-4);
  height: var(--size-4);
  border-radius: var(--radius-round);
  background-color: var(--purple-9);
  border: var(--border-size-2) solid var(--purple-1);
}

/* Base UI renders the thumb as a `div` around a visually hidden range input, and the input
   is what takes focus, so the ring follows the thumb's focused descendant. */
.thumb:has(:focus-visible) {
  outline: var(--border-size-2) solid var(--purple-9);
  outline-offset: var(--border-size-2);
}

Measured on real documentation

These patterns were first applied to the Base UI docs, which this package was originally developed to power. Profiling the change showed:

  • −58% HTML parse time, from 18.4 ms to 7.8 ms
  • −37% HTML and CSS render time, from 43 ms to 27 ms
  • −23% Largest Contentful Paint on a page with six demos
  • 0 s Total Blocking Time, with no long tasks

Read how the numbers were measured

Everything a docs site needs, computed ahead of time

Syntax highlighters are large and TypeScript parsing is slow, so both run in your bundler and ship their results as JSON. Every piece is a separate import, so you can adopt them one at a time.

Live demos with real source

One createDemo call renders a component beside its highlighted source, with a tab for every file it imports and an optional TypeScript to JavaScript toggle.

Explore the Code Highlighter

API reference from TypeScript

Props, defaults and JSDoc descriptions are extracted from your types at build time, and committed as a types.md you can review in a pull request.

Explore type extraction

Search without a server

Titles, sections, props and exports are collected from your pages at compile time, then indexed and queried in the browser with Orama. Press ⌘K or Ctrl+K to try it.

Explore search

Highlighting with no runtime

Code is highlighted once by the bundler and shipped as compressed HAST. A precomputed block downloads no highlighter and no grammars.

Explore the HAST pattern

Live editing

Readers can edit a demo and watch it re-render. Transpiling runs in a Web Worker, and a broken edit keeps the last good preview on screen.

Explore live demos

Pages that settle once

Deferred content swaps in together instead of as a cascade of layout shifts, and large content streams in progressively.

Explore coordinated streaming

One line in, a full demo out

The demo at the top of this page is the file below. createDemo needs only the module URL and a component. A build-time loader follows the imports, highlights every file, can derive the JavaScript version, and writes the result back into the call, so the page renders from data instead of running a highlighter.

index.ts
// What you write
import { createDemo } from '../createDemo';
import { PetalSlider } from './PetalSlider';

export const DemoPetalSlider = createDemo(import.meta.url, PetalSlider);
index.ts
// What the bundler receives (simplified)
export const DemoPetalSlider = createDemo(
  import.meta.url,
  PetalSlider,
  {
    precompute: {
      Default: {
        fileName: 'PetalSlider.tsx',
        source: { hastCompressed: '…' },
        extraFiles: {
          'PetalSlider.module.css': { source: { hastCompressed: '…' } },
        },
        transforms: {
          js: { fileName: 'PetalSlider.jsx', hasDelta: true },
        },
      },
    },
  },
);
  1. Bind your UI once. createDemoFactory({ DemoContent }) turns your own components into the createDemo that every demo calls.
  2. Write one line per demo. import.meta.url is the identity, so names, slugs and file links come from the folder structure.
  3. Let the loader do the work. withFairGardenDocs registers webpack and Turbopack loaders that highlight, transform and cache each demo by its dependencies, and hot reload it when one of them changes.
  4. Render from JSON. At runtime the page only converts precomputed HAST into React elements.

Read the architecture

Your TypeScript is the API reference

Pass a component, hook or function to createTypes. Its props, types and JSDoc comments become the reference shown here, extracted while the site builds.

types.ts
import { createTypes } from '@/functions/createTypes';
import { Flower } from './Flower';

export const TypesFlower = createTypes(import.meta.url, Flower);
  • Documents components, hooks, functions and classes
  • Picks up data attributes and CSS variables from enums
  • Writes a types.md next to the page, so API changes show up in code review

Explore type extraction

A flat flower that fans its petals evenly around the center. It mainly serves as the component documented by the landing page demos.

PropTypeDescription
label
string | undefined

Accessible name. The flower is treated as decorative when omitted.

color
FlowerColor | undefined

Petal color, taken from the Radix color scales.

petals
number | undefined

Number of petals fanned evenly around the center.

size
number | undefined

Width and height of the flower in pixels.

FlowerColor
type FlowerColor = 'purple' | 'plum' | 'crimson' | 'amber'
FlowerProps
type FlowerProps = {
  /**
   * Number of petals fanned evenly around the center.
   * @default 8
   */
  petals?: number;
  /**
   * Petal color, taken from the Radix color scales.
   * @default 'purple'
   */
  color?: FlowerColor;
  /**
   * Width and height of the flower in pixels.
   * @default 160
   */
  size?: number;
  /** Accessible name. The flower is treated as decorative when omitted. */
  label?: string;
}

Let readers change the code

Any demo can be editable. Change a petals value or a color in this one, and the garden re-renders as you type.

  • Source is transpiled off the main thread, in a Web Worker shared by the page
  • A broken edit keeps the last good preview, with the error beside it
  • The editing engine loads on demand, so read-only blocks never ship it

Explore live demos

import * as React from 'react';
import { Flower } from '@/components/Flower';
import styles from './garden.module.css';

export default function Garden() {
  return (
    <div className={styles.bed}>
      <Flower petals={5} color="crimson" size={96} />
      <Flower petals={8} color="purple" size={128} />
      <Flower petals={12} color="amber" size={96} />
    </div>
  );
}

Point at the lines that matter

Mark lines with comments in your source. The first block is the file as written. The second is what readers get: the comments are gone, the marked line is highlighted, and everything outside the focused region folds away until they expand it.

// What you write
import * as React from 'react';
import { Flower } from './Flower';
import { PetalInput } from './PetalInput';
import styles from './Garden.module.css';

interface GardenProps {
  /** Petal count each flower starts with. */
  defaultPetals?: number;
}

export function Garden({ defaultPetals = 8 }: GardenProps) {
  const [petals, setPetals] = React.useState(defaultPetals);
  const reset = () => setPetals(defaultPetals);

  return (
    <section className={styles.root}>
      <h2 className={styles.title}>Garden</h2>
      {/* @focus-start @padding 1 */}
      <div className={styles.bed}>
        <Flower petals={petals} /> {/* @highlight */}
        <Flower petals={petals} color="plum" />
      </div>
      <PetalInput value={petals} onChange={setPetals} />
      {/* @focus-end */}
      <button type="button" className={styles.reset} onClick={reset}>
        Reset
      </button>
    </section>
  );
}
// What readers see
import * as React from 'react';
import { Flower } from './Flower';
import { PetalInput } from './PetalInput';
import styles from './Garden.module.css';

interface GardenProps {
  /** Petal count each flower starts with. */
  defaultPetals?: number;
}

export function Garden({ defaultPetals = 8 }: GardenProps) {
  const [petals, setPetals] = React.useState(defaultPetals);
  const reset = () => setPetals(defaultPetals);

  return (
    <section className={styles.root}>
      <h2 className={styles.title}>Garden</h2>
      <div className={styles.bed}>
        <Flower petals={petals} />
        <Flower petals={petals} color="plum" />
      </div>
      <PetalInput value={petals} onChange={setPetals} />
      <button type="button" className={styles.reset} onClick={reset}>
        Reset
      </button>
    </section>
  );
}

Explore code emphasis and the collapsing window.

Every file, readable by crawlers

Highlighted code is mostly <span> markup, and it outweighs the code it colors several times over. Ship that for every file of every demo and a page outgrows a crawler's budget, so the choice becomes bloat or only the visible tab. Here the HTML carries code as plain text in <pre><code>, and the highlighted version swaps in after hydration, as each block nears the viewport.

Initial HTMLPlain textHighlighted
A 27-line demo1.33 KB4.79 KB
A very large file49 KB203 KB

That leaves room for every file and every variant of a demo, so search engines and AI agents can read all of it. Opt in with fallbackUsesExtraFiles and fallbackUsesAllVariants. View this page's source to check: both files of the demo at the top are there as plain text, including the stylesheet behind its second tab.

Read the semantic HTML convention or see where the sizes come from.

Still just markdown

Every page is an MDX file that reads fine on GitHub. A demo is described in a sentence, imported directly above where it is used, and followed by a link to its source. On the site the demo renders in place and the link disappears.

page.mdx
Drag the slider and the flower redraws with that many petals.

import { DemoPetalSlider } from './demos/petal-slider';

<DemoPetalSlider />

[See Demo](./demos/petal-slider/)

Links point at real files, so they work on GitHub and in your editor, and become clean URLs when the site builds.

Read the markdown-first convention or read this page on GitHub.

Questions

Start with one demo

Install the package, wrap your Next.js config, and turn a single component into a demo. The rest can follow.

Released under the MIT License. First built in June 2025 to power Base UI's documentation, and hard forked from mui/mui-public in July 2026.