Skip to content

Documentation page

Shell, page header and content blocks in the order a reader needs them.

Contentintermediatedocsdeveloperreferenceshell

Live preview

Open the full page
full widthLive preview — open it in a new tab for the full-height version.
Open the preview in a new tab

Source

This exact file renders the preview above.

import DocumentationNavbar from '@/components/blocks/navigation/documentation'
import DocumentationHeader from '@/components/blocks/headers/documentation'
import CodeSampleHero from '@/components/blocks/sections/hero/code-sample'
import NumberedListFeatures from '@/components/blocks/sections/features/numbered-list'
import ApiFirstIntegrations from '@/components/blocks/sections/integrations/api-first'
import CategorisedFAQ from '@/components/blocks/sections/faq/categorised'
import DocumentationFooter from '@/components/blocks/footers/documentation'

/**
 * Documentation page
 *
 * Shell, page header, then content blocks in the order a reader needs them:
 * show the code, explain the method, list the surface, answer the questions.
 * The previous/next footer is what turns a set of pages into a path.
 */
export default function DocumentationPattern() {
  return (
    <>
      <DocumentationNavbar />
      <main>
        <DocumentationHeader />
        <CodeSampleHero />
        <NumberedListFeatures />
        <ApiFirstIntegrations />
        <CategorisedFAQ />
      </main>
      <DocumentationFooter />
    </>
  )
}

components/patterns/documentation.tsx

import DocumentationNavbar from '@/components/blocks/navigation/documentation'
import DocumentationHeader from '@/components/blocks/headers/documentation'
import CodeSampleHero from '@/components/blocks/sections/hero/code-sample'
import NumberedListFeatures from '@/components/blocks/sections/features/numbered-list'
import ApiFirstIntegrations from '@/components/blocks/sections/integrations/api-first'
import CategorisedFAQ from '@/components/blocks/sections/faq/categorised'
import DocumentationFooter from '@/components/blocks/footers/documentation'

/**
 * Documentation page
 *
 * Shell, page header, then content blocks in the order a reader needs them:
 * show the code, explain the method, list the surface, answer the questions.
 * The previous/next footer is what turns a set of pages into a path.
 */
export default function DocumentationPattern() {
  return (
    <>
      <DocumentationNavbar />
      <main>
        <DocumentationHeader />
        <CodeSampleHero />
        <NumberedListFeatures />
        <ApiFirstIntegrations />
        <CategorisedFAQ />
      </main>
      <DocumentationFooter />
    </>
  )
}

components/blocks/navigation/documentation.tsx

'use client'

import { useState } from 'react'
import Link from 'next/link'
import { ChevronRight, Menu, X } from 'lucide-react'
import { cn } from '@/lib/cn'
import { BrandMark } from '@/components/library/brand'
import { Badge } from '@/components/ui/badge'
import { SearchField } from '@/components/ui/search-field'
import { docsNav } from '@/content/nav-demo'

/**
 * Documentation navbar
 *
 * Header plus a collapsible section tree. The tree is the primary navigation
 * on desktop and moves into an in-flow disclosure below `lg` — deliberately
 * not a drawer, so a reader can keep their place in the page while scanning
 * the outline.
 *
 * Groups use a real `<details>`-style disclosure with `aria-expanded`, so the
 * open/closed state is announced.
 */
export default function DocumentationNavbar() {
  const [mobileOpen, setMobileOpen] = useState(false)
  const [collapsed, setCollapsed] = useState<Record<string, boolean>>({})
  const activeHref = '/docs/design-tokens'

  const toggle = (label: string) =>
    setCollapsed((current) => ({ ...current, [label]: !current[label] }))

  const tree = (
    <nav aria-label="Documentation" className="flex flex-col gap-4">
      {docsNav.map((group) => {
        const isCollapsed = collapsed[group.label] ?? false
        return (
          <div key={group.label}>
            <button
              type="button"
              onClick={() => toggle(group.label)}
              aria-expanded={!isCollapsed}
              className="flex w-full items-center gap-1.5 rounded-sm px-2 py-1 text-left"
            >
              <ChevronRight
                className={cn(
                  'size-3 text-ink-subtle transition-transform',
                  !isCollapsed && 'rotate-90',
                )}
                aria-hidden="true"
              />
              <span className="label-caps text-ink-subtle">{group.label}</span>
            </button>
            {!isCollapsed ? (
              <ul className="mt-1 flex flex-col gap-0.5 border-l border-line-subtle pl-3">
                {group.links.map((link) => {
                  const active = link.href === activeHref
                  return (
                    <li key={link.href}>
                      <Link
                        href={link.href}
                        aria-current={active ? 'page' : undefined}
                        className={cn(
                          '-ml-px flex min-h-8 items-center border-l-2 px-3 text-sm transition-colors',
                          active
                            ? 'border-accent font-medium text-accent'
                            : 'border-transparent text-ink-muted hover:border-line-strong hover:text-ink',
                        )}
                      >
                        {link.label}
                      </Link>
                    </li>
                  )
                })}
              </ul>
            ) : null}
          </div>
        )
      })}
    </nav>
  )

  return (
    <div className="w-full bg-surface">
      <div className="border-b border-line">
        <div className="mx-auto flex h-14 w-full max-w-7xl items-center gap-3 px-4 sm:px-6">
          <button
            type="button"
            onClick={() => setMobileOpen((value) => !value)}
            aria-expanded={mobileOpen}
            aria-controls="docs-nav-panel"
            className="flex size-9 items-center justify-center rounded-md text-ink-muted hover:bg-surface-sunken lg:hidden"
          >
            {mobileOpen ? (
              <X className="size-5" aria-hidden="true" />
            ) : (
              <Menu className="size-5" aria-hidden="true" />
            )}
            <span className="sr-only">{mobileOpen ? 'Close outline' : 'Open outline'}</span>
          </button>
          <Link href="/docs/getting-started" className="flex items-center gap-2 text-ink-strong">
            <BrandMark className="size-5 text-accent" />
            <span className="display-type text-md font-semibold">Foundry</span>
          </Link>
          <Badge size="sm" className="hidden sm:inline-flex">
            v1.0
          </Badge>
          <div className="ml-auto w-full max-w-64">
            <SearchField
              fieldSize="sm"
              placeholder="Search docs"
              aria-label="Search documentation"
              shortcutHint="/"
            />
          </div>
        </div>
      </div>

      <div className="mx-auto flex w-full max-w-7xl gap-8 px-4 sm:px-6">
        <aside className="hidden w-56 shrink-0 py-6 lg:block">{tree}</aside>
        <div
          id="docs-nav-panel"
          hidden={!mobileOpen}
          className="w-full border-b border-line-subtle py-4 lg:hidden"
        >
          {tree}
        </div>
        <div className="hidden min-w-0 flex-1 border-l border-line-subtle py-6 pl-8 lg:block">
          <p className="text-sm text-ink-muted">
            Page content sits here. The outline stays fixed while the article scrolls.
          </p>
        </div>
      </div>
    </div>
  )
}

components/blocks/footers/documentation.tsx

import Link from 'next/link'
import { ArrowLeft, ArrowRight, MessageSquare, PencilLine } from 'lucide-react'
import { Container } from '@/components/ui/layout'
import { Divider } from '@/components/ui/layout'

/**
 * Documentation footer
 *
 * Two jobs: move the reader to the next page, and give them somewhere to go
 * when the page was wrong. Previous/next uses `rel` hints and a symmetrical
 * two-card layout that collapses to a single column on mobile.
 */
export default function DocumentationFooter() {
  return (
    <footer className="bg-canvas">
      <Container>
        <Divider />
        <div className="grid gap-3 py-8 sm:grid-cols-2">
          <Link
            href="/docs/design-tokens"
            rel="prev"
            className="group flex flex-col rounded-lg border border-line bg-surface p-4 transition-colors hover:border-line-strong"
          >
            <span className="flex items-center gap-1.5 text-xs text-ink-subtle">
              <ArrowLeft className="size-3.5" aria-hidden="true" />
              Previous
            </span>
            <span className="mt-1.5 text-sm font-semibold text-ink-strong group-hover:text-accent">
              Design tokens
            </span>
          </Link>

          <Link
            href="/docs/contributing"
            rel="next"
            className="group flex flex-col items-end rounded-lg border border-line bg-surface p-4 text-right transition-colors hover:border-line-strong"
          >
            <span className="flex items-center gap-1.5 text-xs text-ink-subtle">
              Next
              <ArrowRight className="size-3.5" aria-hidden="true" />
            </span>
            <span className="mt-1.5 text-sm font-semibold text-ink-strong group-hover:text-accent">
              Contributing
            </span>
          </Link>
        </div>

        <div className="flex flex-col gap-3 border-t border-line py-6 sm:flex-row sm:items-center sm:justify-between">
          <nav aria-label="Page actions">
            <ul className="flex flex-wrap gap-x-5 gap-y-2">
              <li>
                <Link
                  href="/docs/contributing"
                  className="inline-flex items-center gap-1.5 text-sm text-ink-muted hover:text-accent"
                >
                  <PencilLine className="size-3.5" aria-hidden="true" />
                  Suggest an edit
                </Link>
              </li>
              <li>
                <Link
                  href="/forms/support-request"
                  className="inline-flex items-center gap-1.5 text-sm text-ink-muted hover:text-accent"
                >
                  <MessageSquare className="size-3.5" aria-hidden="true" />
                  Report a problem
                </Link>
              </li>
            </ul>
          </nav>
          <p className="text-xs text-ink-subtle">Last updated in v1.0.0</p>
        </div>
      </Container>
    </footer>
  )
}

Demo source — adapt to your project. Foundry is not published as a package.

Usage

Show the code, explain the method, list the surface, answer the questions. The previous-and-next footer is what turns a set of pages into a path.

  • Keep the outline in flow on mobile rather than hiding it behind a drawer.
  • A version and last-updated line is what makes documentation trustable.

Variants and states

Every entry below is a genuine difference in behaviour or layout, and every one of them is visible in the preview above.

  • Documentation shell with a section tree
  • Page header with version metadata
  • Code sample and API surface
  • Previous and next footer

Accessibility

Tree semantics
Group disclosures carry aria-expanded and the current page carries aria-current.
Native FAQ
The question groups use native details elements, so they work without JavaScript.

Foundry implements published ARIA patterns and is tested against them. No WCAG certification is claimed — see the accessibility documentation for what is and is not covered.