Skip to content
Facade UI
blocktemplate

Documentation page

A documentation page built from Facade UI sections: top navigation, side navigation, the article, an 'On this page' list and previous and next links. You supply one content object and the article body.

Open in v0 (opens in a new tab)

Preview

Open full width (opens in a new tab)
Preview width

Installation

Package manager
pnpm dlx shadcn@latest add @facade/docs-site

This component needs container, eyebrow, footer, heading, nav-side, nav-top, prose, types, utils. The CLI installs them for you.

Usage

This is the code for the preview above. To install it as a file, run npx shadcn@latest add @facade/docs-site-demo.

demos/docs-site.tsx
import { DocsSite } from "@registry/templates/docs-site"

import { DOCS_GROUPS, FOOTER_GROUPS, NAV_ITEMS } from "./content"

export function Demo() {
  return (
    <div className="-m-6 sm:-m-10">
      <DocsSite
        currentPath="#installation"
        content={{
          brand: "Facade UI",
          nav: NAV_ITEMS,
          navActions: [{ label: "Get started", href: "#start" }],
          sidebar: { label: "Documentation", groups: DOCS_GROUPS },
          page: {
            eyebrow: "Getting started",
            title: "Installation",
            description: "Add the tokens once, then install sections one at a time.",
            lastUpdated: { dateTime: "2026-09-21", label: "21 September 2026" },
          },
          toc: {
            items: [
              { label: "Requirements", href: "#requirements" },
              {
                label: "Install the tokens",
                href: "#tokens",
                children: [{ label: "Import the stylesheet", href: "#import" }],
              },
              { label: "Add a section", href: "#section" },
            ],
          },
          pagination: {
            prev: { label: "Introduction", href: "#introduction" },
            next: { label: "Theming", href: "#theming" },
          },
          footer: { groups: FOOTER_GROUPS, copyright: "© 2026 Facade UI contributors" },
        }}
      >
        <h2 id="requirements">Requirements</h2>
        <ul>
          <li>React 19 and Tailwind CSS v4</li>
          <li>
            A project set up with <code>shadcn init</code>
          </li>
        </ul>
        <h2 id="tokens">Install the tokens</h2>
        <p>Every section reads the same tokens, so they come first.</p>
        <pre>
          <code>npx shadcn@latest add @facade/tokens</code>
        </pre>
        <h3 id="import">Import the stylesheet</h3>
        <p>
          Import the file after Tailwind in your global stylesheet, so the tokens can
          override its defaults.
        </p>
        <pre>
          <code>@import &quot;./facade-tokens.css&quot;;</code>
        </pre>
        <h2 id="section">Add a section</h2>
        <p>
          Install any section by name. Its dependencies come with it, and the source lands
          in your project.
        </p>
      </DocsSite>
    </div>
  )
}

Source

These are the files the CLI copies into your project.

components/templates/docs-site.tsx
/**
 * DocsSite — a documentation page, assembled from registry sections.
 *
 * Top navigation, the side navigation, the article and an "On this page"
 * list, with previous and next links at the end. The article body is the
 * `children` slot, styled by `Prose`, so rendered markdown drops straight in.
 *
 * a11y:
 *
 *  - Three named `<nav>` landmarks: the sidebar, "On this page" and "Pages"
 *    for previous and next. Below `xl` the "On this page" list is hidden; the
 *    headings it points to are still in the article.
 *  - The page title is the only `<h1>`. The sidebar's group headings are
 *    `<h2>` and come before it in the source, as on most docs sites; that keeps
 *    heading navigation down the sidebar, so do not demote them.
 *  - Previous and next links carry `rel` and a visible "Previous" or "Next",
 *    so their purpose does not depend on position.
 *
 * Not a client component: it renders client sections but passes them only
 * data, a link component and `children`.
 *
 * Dependencies: react, @/lib/types, @/lib/utils,
 * @/components/sections/*, @/components/ui/*.
 */

import type { ElementType, ReactNode } from "react"

import type { CtaItem, LinkComponent } from "@/lib/types"
import { cn, slugId } from "@/lib/utils"
import { Footer, type FooterGroup, type FooterLink } from "@/components/sections/footer"
import { NavSide, type NavSideGroup } from "@/components/sections/nav-side"
import { NavTop, type NavItem } from "@/components/sections/nav-top"
import { Container } from "@/components/ui/container"
import { Eyebrow } from "@/components/ui/eyebrow"
import { Heading } from "@/components/ui/heading"
import { Prose } from "@/components/ui/prose"

export interface TocItem {
  label: string
  /** Usually `#` and the id of a heading in the article. */
  href: string
  children?: TocItem[]
}

export interface DocsPageLink {
  label: string
  href: string
}

export interface DocsSiteContent {
  brand: ReactNode
  nav: NavItem[]
  navActions?: CtaItem[]

  sidebar: {
    /** Names the sidebar landmark. Defaults to "Documentation". */
    label?: string
    groups: NavSideGroup[]
  }

  page: {
    eyebrow?: string
    title: string
    description?: string
    lastUpdated?: { dateTime: string; label: string }
  }

  /** The headings of this page. */
  toc?: { label?: string; items: TocItem[] }

  pagination?: { prev?: DocsPageLink; next?: DocsPageLink }

  footer: {
    groups?: FooterGroup[]
    brand?: ReactNode
    copyright?: ReactNode
    legal?: FooterLink[]
  }
}

export interface DocsSiteProps {
  content: DocsSiteContent
  /** The article body. Rendered markdown or JSX; `Prose` styles it. */
  children: ReactNode
  link?: LinkComponent
  /** Marks the current page in both navigations. */
  currentPath?: string
  /** Shown above the header. Usually a `Banner`. */
  banner?: ReactNode
}

const linkFocus =
  "focus-visible:ring-ring rounded-sm focus-visible:outline-none focus-visible:ring-2"

const pageLink =
  "hover:border-ring flex min-h-11 flex-col gap-1 border p-4 transition-colors"

function TocList({ items, Link }: { items: TocItem[]; Link: ElementType }) {
  return (
    <ul className="flex flex-col gap-2">
      {items.map((item) => (
        <li key={item.href} className="flex flex-col gap-2">
          <Link
            href={item.href}
            className={cn(
              "text-muted-foreground hover:text-foreground transition-colors",
              linkFocus,
            )}
          >
            {item.label}
          </Link>
          {item.children?.length ? (
            <div className="border-border border-l pl-3">
              <TocList items={item.children} Link={Link} />
            </div>
          ) : null}
        </li>
      ))}
    </ul>
  )
}

export function DocsSite({
  content,
  children,
  link,
  currentPath,
  banner,
}: DocsSiteProps) {
  const { sidebar, page, toc, pagination, footer } = content
  const Link = (link ?? "a") as ElementType
  const titleId = slugId(page.title, "page")
  const tocId = "docs-site-toc-title"
  const { prev, next } = pagination ?? {}

  return (
    <>
      {banner}

      <NavTop
        brand={content.brand}
        items={content.nav}
        actions={content.navActions}
        link={link}
        currentPath={currentPath}
      />

      <Container className="grid gap-10 py-10 lg:grid-cols-[16rem_minmax(0,1fr)] xl:grid-cols-[16rem_minmax(0,1fr)_13rem]">
        <NavSide
          label={sidebar.label ?? "Documentation"}
          groups={sidebar.groups}
          currentPath={currentPath}
          link={link}
          headingLevel={2}
        />

        <main id="main" className="min-w-0">
          <article aria-labelledby={titleId} className="flex max-w-3xl flex-col gap-10">
            <header className="flex flex-col gap-3">
              {page.eyebrow ? <Eyebrow tone="primary">{page.eyebrow}</Eyebrow> : null}
              <Heading
                level={1}
                id={titleId}
                className="text-display-sm text-balance font-semibold"
              >
                {page.title}
              </Heading>
              {page.description ? (
                <p className="text-muted-foreground text-pretty text-lg">
                  {page.description}
                </p>
              ) : null}
              {page.lastUpdated ? (
                <p className="text-muted-foreground text-sm">
                  Last updated{" "}
                  <time dateTime={page.lastUpdated.dateTime}>
                    {page.lastUpdated.label}
                  </time>
                </p>
              ) : null}
            </header>

            <Prose>{children}</Prose>

            {prev || next ? (
              <nav
                aria-label="Pages"
                className="border-border grid gap-4 border-t pt-8 sm:grid-cols-2"
              >
                {prev ? (
                  <Link
                    href={prev.href}
                    rel="prev"
                    className={cn(pageLink, linkFocus, "rounded-lg")}
                  >
                    <span className="text-muted-foreground text-sm">Previous</span>
                    <span className="font-medium">{prev.label}</span>
                  </Link>
                ) : null}
                {next ? (
                  <Link
                    href={next.href}
                    rel="next"
                    className={cn(
                      pageLink,
                      linkFocus,
                      "rounded-lg text-right sm:col-start-2",
                    )}
                  >
                    <span className="text-muted-foreground text-sm">Next</span>
                    <span className="font-medium">{next.label}</span>
                  </Link>
                ) : null}
              </nav>
            ) : null}
          </article>
        </main>

        {toc?.items.length ? (
          <nav aria-labelledby={tocId} className="hidden xl:block">
            <div className="sticky top-24 flex max-h-[calc(100dvh-7rem)] flex-col gap-3 overflow-y-auto text-sm">
              <Heading level={2} id={tocId} className="text-foreground font-semibold">
                {toc.label ?? "On this page"}
              </Heading>
              <TocList items={toc.items} Link={Link} />
            </div>
          </nav>
        ) : null}
      </Container>

      <Footer
        brand={footer.brand ?? content.brand}
        groups={footer.groups}
        copyright={footer.copyright}
        legal={footer.legal}
        link={link}
        headingLevel={2}
      />
    </>
  )
}

Props

DocsSiteProps

Props for DocsSiteProps
PropTypeDefault
content*RequiredDocsSiteContent—
children*RequiredThe article body. Rendered markdown or JSX; `Prose` styles it.ReactNode—
linkLinkComponent—
currentPathMarks the current page in both navigations.string—
bannerShown above the header. Usually a `Banner`.ReactNode—