Skip to content
Marketing block

Three-Column Docs Frame

Sidebar, article and on-this-page rail — each column answers a different question, and they collapse on mobile in the order that keeps the article alive.

290 lineslucide-reactAdded 13 Aug 2026Updated 13 Sept 2026
  • docs
  • documentation
  • sidebar
  • toc
  • layout

What's included

  • components/docs-layout.tsx
  • Needs lucide-react

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add docs-layout

Or over MCP, from your editor's agent — no account needed.

License

Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.

Was this useful?

Start a page with this section — add more, order them, and leave with the page source.

Preview

Docs navigation

Authentication

Every request to the Acme API is authenticated with a bearer token. This page covers creating a key, storing it safely, and rotating it without downtime.

Acme issues two kinds of keys. Secret keys (prefixed sk_live_) can read and write everything in your project and must only ever live on a server. Publishable keys (prefixed pk_live_) are safe to ship in a browser bundle and can only create client sessions.

Keys are scoped to a project, not to your account. Deleting a project revokes its keys immediately; removing a teammate does not, so treat offboarding as a reason to rotate.

Creating an API key

Create a key from the dashboard under Settings → API keys, then pass it as a bearer token on every request:

curl https://api.acme.dev/v1/projects \
  -H "Authorization: Bearer $ACME_API_KEY"

Note: the full secret is shown once, at creation. Acme stores only a hash, so a lost key cannot be recovered — it can only be rotated.

Rotating keys

Rotation is overlap, not replacement: create the new key, deploy it everywhere, and only then revoke the old one. Both keys stay valid during the window, so a slow rollout never turns into an outage.

Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.

Source

components/docs-layout.tsx
/**
 * <DocsLayout> — the three-column documentation screen.
 *
 * The layout survives because each column answers a different question.
 * The sidebar answers "where am I in the product", the article answers
 * "what does this page say", and the toc rail answers "where am I in the
 * page". Merge any two and one of those questions goes unanswered.
 *
 * The classic failure is collapsing them in the wrong order on mobile.
 * The correct order is the reverse of usefulness: the toc goes first
 * (hidden below xl — a short page is its own toc), the sidebar second
 * (folded behind a summary line below lg), and the article never. A docs
 * site that hides prose to keep navigation visible has it backwards.
 */

import * as React from 'react'
import {
  ChevronRight,
  ChevronDown,
  ArrowLeft,
  ArrowRight,
  Info,
} from 'lucide-react'

interface DocsNavItem {
  label: string
  href: string
  active?: boolean
}

interface DocsNavGroup {
  group: string
  items: DocsNavItem[]
}

export interface DocsLayoutProps {
  title?: string
  standfirst?: string
  /**
   * Prefix for the article's heading anchors.
   *
   * The anchors are readable slugs — `#rotating-keys` — which is right for a
   * docs page and wrong the moment two of these frames land on one document:
   * both links then jump to the first copy. One route per document needs no
   * prefix. A catalog hub, a tabbed help centre, or a legal section
   * rendering several documents at once does.
   */
  anchorPrefix?: string
  className?: string
}

const NAV: DocsNavGroup[] = [
  {
    group: 'Getting started',
    items: [
      { label: 'Introduction', href: '#introduction' },
      { label: 'Quickstart', href: '#quickstart' },
      { label: 'Authentication', href: '#authentication', active: true },
      { label: 'SDKs & clients', href: '#sdks' },
    ],
  },
  {
    group: 'Guides',
    items: [
      { label: 'Pagination', href: '#pagination' },
      { label: 'Rate limits', href: '#rate-limits' },
      { label: 'Webhooks', href: '#webhooks' },
    ],
  },
  {
    group: 'API reference',
    items: [
      { label: 'Projects', href: '#ref-projects' },
      { label: 'Deployments', href: '#ref-deployments' },
      { label: 'Logs', href: '#ref-logs' },
    ],
  },
]

const TOC: { label: string; href: string; active?: boolean }[] = [
  { label: 'Creating an API key', href: '#creating-an-api-key', active: true },
  { label: 'Rotating keys', href: '#rotating-keys' },
]

const SNIPPET = `curl https://api.acme.dev/v1/projects \\
  -H "Authorization: Bearer $ACME_API_KEY"`

function SidebarNav() {
  return (
    <nav aria-label="Docs" className="space-y-6">
      {NAV.map((section) => (
        <div key={section.group}>
          <h2 className="px-3 pb-1.5 text-xs font-bold uppercase tracking-wider text-muted-foreground">
            {section.group}
          </h2>
          <ul className="space-y-0.5">
            {section.items.map((item) => (
              <li key={item.label}>
                <a
                  href={item.href}
                  aria-current={item.active ? 'page' : undefined}
                  className={`block break-words rounded-lg px-3 py-1.5 text-sm transition-colors ${
                    item.active
                      ? 'bg-primary/10 font-medium text-primary'
                      : 'text-muted-foreground hover:bg-muted hover:text-foreground'
                  }`}
                >
                  {item.label}
                </a>
              </li>
            ))}
          </ul>
        </div>
      ))}
    </nav>
  )
}

export function DocsLayout({
  title = 'Authentication',
  standfirst = 'Every request to the Acme API is authenticated with a bearer token. This page covers creating a key, storing it safely, and rotating it without downtime.',
  anchorPrefix = '',
  className = '',
}: DocsLayoutProps) {
  // The table of contents is a module constant, so the prefix is applied to
  // its hrefs here rather than at the literal — otherwise the links keep
  // pointing at the unprefixed copy.
  const toc = TOC.map((item) => ({ ...item, href: `#${anchorPrefix}${item.href.slice(1)}` }))

  return (
    <div className={`w-full bg-background text-foreground ${className}`}>
      {/* Mobile: the sidebar folds behind a summary line; the article stays. */}
      <details className="group border-b border-border/60 lg:hidden">
        <summary className="flex cursor-pointer list-none items-center gap-2 px-4 py-3 text-sm font-medium [&::-webkit-details-marker]:hidden">
          <ChevronDown
            aria-hidden
            className="h-4 w-4 text-muted-foreground transition-transform group-open:rotate-180"
          />
          Docs navigation
        </summary>
        <div className="border-t border-border/60 px-2 py-4">
          <SidebarNav />
        </div>
      </details>

      <div className="mx-auto grid w-full max-w-7xl grid-cols-1 gap-10 px-4 py-8 sm:px-6 lg:grid-cols-[13rem_minmax(0,1fr)] lg:px-8 xl:grid-cols-[13rem_minmax(0,1fr)_12rem]">
        <div className="hidden lg:block">
          <SidebarNav />
        </div>

        <article className="min-w-0">
          <nav aria-label="Breadcrumb">
            <ol className="flex items-center gap-1.5 text-sm text-muted-foreground">
              <li>
                <a href="#docs" className="transition-colors hover:text-foreground">
                  Docs
                </a>
              </li>
              <ChevronRight aria-hidden className="h-3.5 w-3.5 rtl:rotate-180" />
              <li>
                <a href="#getting-started" className="transition-colors hover:text-foreground">
                  Getting started
                </a>
              </li>
              <ChevronRight aria-hidden className="h-3.5 w-3.5 rtl:rotate-180" />
              <li aria-current="page" className="font-medium text-foreground">
                {title}
              </li>
            </ol>
          </nav>

          <h1 className="mt-4 text-3xl font-bold tracking-tight">{title}</h1>
          <p className="mt-3 text-base text-muted-foreground">{standfirst}</p>

          <p className="mt-6 text-sm leading-relaxed text-muted-foreground">
            Acme issues two kinds of keys. Secret keys (prefixed{' '}
            <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs text-foreground">
              sk_live_
            </code>
            ) can read and write everything in your project and must only ever live on a
            server. Publishable keys (prefixed{' '}
            <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs text-foreground">
              pk_live_
            </code>
            ) are safe to ship in a browser bundle and can only create client sessions.
          </p>
          <p className="mt-4 text-sm leading-relaxed text-muted-foreground">
            Keys are scoped to a project, not to your account. Deleting a project revokes
            its keys immediately; removing a teammate does not, so treat offboarding as a
            reason to rotate.
          </p>

          <h2
            id={`${anchorPrefix}creating-an-api-key`}
            className="mt-10 text-xl font-semibold tracking-tight"
          >
            Creating an API key
          </h2>
          <p className="mt-3 text-sm leading-relaxed text-muted-foreground">
            Create a key from the dashboard under Settings → API keys, then pass it as a
            bearer token on every request:
          </p>

          <div className="mt-4 overflow-hidden rounded-xl border border-border/60 bg-zinc-950">
            <div aria-hidden className="flex items-center gap-1.5 border-b border-white/10 px-4 py-2.5">
              <span className="h-2.5 w-2.5 rounded-full bg-red-500/80 border border-transparent" />
              <span className="h-2.5 w-2.5 rounded-full bg-amber-500/80 border border-transparent" />
              <span className="h-2.5 w-2.5 rounded-full bg-emerald-500/80 border border-transparent" />
              <span className="ms-2 font-mono text-xs text-white/60">terminal</span>
            </div>
            <pre className="overflow-x-auto p-4 text-xs leading-relaxed">
              <code className="font-mono text-zinc-300">{SNIPPET}</code>
            </pre>
          </div>

          <div className="mt-6 flex gap-3 rounded-xl border border-sky-500/30 bg-sky-500/10 p-4">
            <Info aria-hidden className="mt-0.5 h-4 w-4 shrink-0 text-sky-500" />
            <p className="text-sm leading-relaxed text-muted-foreground">
              <span className="font-medium text-foreground">Note:</span> the full secret is
              shown once, at creation. Acme stores only a hash, so a lost key cannot be
              recovered — it can only be rotated.
            </p>
          </div>

          <h2 id={`${anchorPrefix}rotating-keys`} className="mt-10 text-xl font-semibold tracking-tight">
            Rotating keys
          </h2>
          <p className="mt-3 text-sm leading-relaxed text-muted-foreground">
            Rotation is overlap, not replacement: create the new key, deploy it everywhere,
            and only then revoke the old one. Both keys stay valid during the window, so a
            slow rollout never turns into an outage.
          </p>

          <nav aria-label="Pagination" className="mt-12 grid gap-4 sm:grid-cols-2">
            <a
              href="#quickstart"
              className="group flex min-w-0 items-center gap-3 rounded-xl border border-border/60 bg-card/60 p-4 transition-colors hover:border-primary/40"
            >
              <ArrowLeft
                aria-hidden
                className="h-4 w-4 shrink-0 text-muted-foreground transition-colors group-hover:text-primary rtl:rotate-180"
              />
              <span className="min-w-0">
                <span className="block text-xs text-muted-foreground">Previous</span>
                <span className="block truncate text-sm font-medium">Quickstart</span>
              </span>
            </a>
            <a
              href="#sdks"
              className="group flex min-w-0 items-center justify-end gap-3 rounded-xl border border-border/60 bg-card/60 p-4 text-end transition-colors hover:border-primary/40"
            >
              <span className="min-w-0">
                <span className="block text-xs text-muted-foreground">Next</span>
                <span className="block truncate text-sm font-medium">SDKs & clients</span>
              </span>
              <ArrowRight
                aria-hidden
                className="h-4 w-4 shrink-0 text-muted-foreground transition-colors group-hover:text-primary rtl:rotate-180"
              />
            </a>
          </nav>
        </article>

        <nav aria-label="On this page" className="hidden xl:block">
          <h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
            On this page
          </h2>
          <ul className="mt-3 space-y-1 border-s border-border/60">
            {toc.map((item) => (
              <li key={item.href}>
                <a
                  href={item.href}
                  aria-current={item.active ? 'location' : undefined}
                  className={`-ms-px block border-s py-1 ps-4 text-sm transition-colors ${
                    item.active
                      ? 'border-primary font-medium text-primary'
                      : 'border-transparent text-muted-foreground hover:text-foreground'
                  }`}
                >
                  {item.label}
                </a>
              </li>
            ))}
          </ul>
        </nav>
      </div>
    </div>
  )
}

Before you paste

  • Styling is Tailwind utility classes on semantic tokens (bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.
  • Install: npm i lucide-react
  • Every prop has a default, so it renders standalone before you wire it up.

Where it goes

Drop it at components/docs-layout.tsx and import it where you need the section:

import { DocsLayout } from '@/components/docs-layout'

Customize

2 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.

Props

Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.

PropTypeDefault
titlestring'Authentication'
standfirststring—
anchorPrefixPrefix for the article's heading anchors. The anchors are readable slugs — `#rotating-keys` — which is right for a docs page and wrong the moment two of these frames land on one document: both links then jump to the first copy. One route per document needs no prefix. A catalog hub, a tabbed help centre, or a legal section rendering several documents at once does.string''
classNamestring''

Not using React?

The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.

docs-layout.html
<!--
  Three-Column Docs Frame — markup from the Hoverlab catalog.

  This is the block rendered once to HTML and wrapped as a component
  file. It is not a port of the React source: the Tailwind classes carry
  the design, which is the part that took the work, and they are the same
  in every framework.

  This block has no interactive behaviour, so nothing is missing.
-->
<div class="w-full bg-background text-foreground ">
  <details class="group border-b border-border/60 lg:hidden">
    <summary class="flex cursor-pointer list-none items-center gap-2 px-4 py-3 text-sm font-medium [&amp;amp;::-webkit-details-marker]:hidden"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down h-4 w-4 text-muted-foreground transition-transform group-open:rotate-180" aria-hidden="true"><path d="m6 9 6 6 6-6"></path></svg>Docs navigation</summary>
    <div class="border-t border-border/60 px-2 py-4">
      <nav aria-label="Docs" class="space-y-6">
        <div>
          <h2 class="px-3 pb-1.5 text-xs font-bold uppercase tracking-wider text-muted-foreground">Getting started</h2>
          <ul class="space-y-0.5">
            <li>
              <a href="#introduction" class="block break-words rounded-lg px-3 py-1.5 text-sm transition-colors text-muted-foreground hover:bg-muted hover:text-foreground">Introduction</a>
            </li>
            <li>
              <a href="#quickstart" class="block break-words rounded-lg px-3 py-1.5 text-sm transition-colors text-muted-foreground hover:bg-muted hover:text-foreground">Quickstart</a>
            </li>
            <li>
              <a href="#authentication" aria-current="page" class="block break-words rounded-lg px-3 py-1.5 text-sm transition-colors bg-primary/10 font-medium text-primary">Authentication</a>
            </li>
            <li>
              <a href="#sdks" class="block break-words rounded-lg px-3 py-1.5 text-sm transition-colors text-muted-foreground hover:bg-muted hover:text-foreground">SDKs &amp; clients</a>
            </li>
          </ul>
        </div>
        <div>
          <h2 class="px-3 pb-1.5 text-xs font-bold uppercase tracking-wider text-muted-foreground">Guides</h2>
          <ul class="space-y-0.5">
            <li>
              <a href="#pagination" class="block break-words rounded-lg px-3 py-1.5 text-sm transition-colors text-muted-foreground hover:bg-muted hover:text-foreground">Pagination</a>
            </li>
            <li>
              <a href="#rate-limits" class="block break-words rounded-lg px-3 py-1.5 text-sm transition-colors text-muted-foreground hover:bg-muted hover:text-foreground">Rate limits</a>
            </li>
            <li>
              <a href="#webhooks" class="block break-words rounded-lg px-3 py-1.5 text-sm transition-colors text-muted-foreground hover:bg-muted hover:text-foreground">Webhooks</a>
            </li>
          </ul>
        </div>
        <div>
          <h2 class="px-3 pb-1.5 text-xs font-bold uppercase tracking-wider text-muted-foreground">API reference</h2>
          <ul class="space-y-0.5">
            <li>
              <a href="#ref-projects" class="block break-words rounded-lg px-3 py-1.5 text-sm transition-colors text-muted-foreground hover:bg-muted hover:text-foreground">Projects</a>
            </li>
            <li>
              <a href="#ref-deployments" class="block break-words rounded-lg px-3 py-1.5 text-sm transition-colors text-muted-foreground hover:bg-muted hover:text-foreground">Deployments</a>
            </li>
            <li>
              <a href="#ref-logs" class="block break-words rounded-lg px-3 py-1.5 text-sm transition-colors text-muted-foreground hover:bg-muted hover:text-foreground">Logs</a>
            </li>
          </ul>
        </div>
      </nav>
    </div>
  </details>
  <div class="mx-auto grid w-full max-w-7xl grid-cols-1 gap-10 px-4 py-8 sm:px-6 lg:grid-cols-[13rem_minmax(0,1fr)] lg:px-8 xl:grid-cols-[13rem_minmax(0,1fr)_12rem]">
    <div class="hidden lg:block">
      <nav aria-label="Docs" class="space-y-6">
        <div>
          <h2 class="px-3 pb-1.5 text-xs font-bold uppercase tracking-wider text-muted-foreground">Getting started</h2>
          <ul class="space-y-0.5">
            <li>
              <a href="#introduction" class="block break-words rounded-lg px-3 py-1.5 text-sm transition-colors text-muted-foreground hover:bg-muted hover:text-foreground">Introduction</a>
            </li>
            <li>
              <a href="#quickstart" class="block break-words rounded-lg px-3 py-1.5 text-sm transition-colors text-muted-foreground hover:bg-muted hover:text-foreground">Quickstart</a>
            </li>
            <li>
              <a href="#authentication" aria-current="page" class="block break-words rounded-lg px-3 py-1.5 text-sm transition-colors bg-primary/10 font-medium text-primary">Authentication</a>
            </li>
            <li>
              <a href="#sdks" class="block break-words rounded-lg px-3 py-1.5 text-sm transition-colors text-muted-foreground hover:bg-muted hover:text-foreground">SDKs &amp; clients</a>
            </li>
          </ul>
        </div>
        <div>
          <h2 class="px-3 pb-1.5 text-xs font-bold uppercase tracking-wider text-muted-foreground">Guides</h2>
          <ul class="space-y-0.5">
            <li>
              <a href="#pagination" class="block break-words rounded-lg px-3 py-1.5 text-sm transition-colors text-muted-foreground hover:bg-muted hover:text-foreground">Pagination</a>
            </li>
            <li>
              <a href="#rate-limits" class="block break-words rounded-lg px-3 py-1.5 text-sm transition-colors text-muted-foreground hover:bg-muted hover:text-foreground">Rate limits</a>
            </li>
            <li>
              <a href="#webhooks" class="block break-words rounded-lg px-3 py-1.5 text-sm transition-colors text-muted-foreground hover:bg-muted hover:text-foreground">Webhooks</a>
            </li>
          </ul>
        </div>
        <div>
          <h2 class="px-3 pb-1.5 text-xs font-bold uppercase tracking-wider text-muted-foreground">API reference</h2>
          <ul class="space-y-0.5">
            <li>
              <a href="#ref-projects" class="block break-words rounded-lg px-3 py-1.5 text-sm transition-colors text-muted-foreground hover:bg-muted hover:text-foreground">Projects</a>
            </li>
            <li>
              <a href="#ref-deployments" class="block break-words rounded-lg px-3 py-1.5 text-sm transition-colors text-muted-foreground hover:bg-muted hover:text-foreground">Deployments</a>
            </li>
            <li>
              <a href="#ref-logs" class="block break-words rounded-lg px-3 py-1.5 text-sm transition-colors text-muted-foreground hover:bg-muted hover:text-foreground">Logs</a>
            </li>
          </ul>
        </div>
      </nav>
    </div>
    <article class="min-w-0">
      <nav aria-label="Breadcrumb">
        <ol class="flex items-center gap-1.5 text-sm text-muted-foreground">
          <li>
            <a href="#docs" class="transition-colors hover:text-foreground">Docs</a>
          </li>
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right h-3.5 w-3.5 rtl:rotate-180" aria-hidden="true">
            <path d="m9 18 6-6-6-6"></path>
          </svg>
          <li>
            <a href="#getting-started" class="transition-colors hover:text-foreground">Getting started</a>
          </li>
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right h-3.5 w-3.5 rtl:rotate-180" aria-hidden="true">
            <path d="m9 18 6-6-6-6"></path>
          </svg>
          <li aria-current="page" class="font-medium text-foreground">Authentication</li>
        </ol>
      </nav>
      <h1 class="mt-4 text-3xl font-bold tracking-tight">Authentication</h1>
      <p class="mt-3 text-base text-muted-foreground">Every request to the Acme API is authenticated with a bearer token. This page covers creating a key, storing it safely, and rotating it without downtime.</p>
      <p class="mt-6 text-sm leading-relaxed text-muted-foreground">Acme issues two kinds of keys. Secret keys (prefixed <code class="rounded bg-muted px-1.5 py-0.5 font-mono text-xs text-foreground">sk_live_</code>) can read and write everything in your project and must only ever live on a server. Publishable keys (prefixed <code class="rounded bg-muted px-1.5 py-0.5 font-mono text-xs text-foreground">pk_live_</code>) are safe to ship in a browser bundle and can only create client sessions.</p>
      <p class="mt-4 text-sm leading-relaxed text-muted-foreground">Keys are scoped to a project, not to your account. Deleting a project revokes its keys immediately; removing a teammate does not, so treat offboarding as a reason to rotate.</p>
      <h2 id="creating-an-api-key" class="mt-10 text-xl font-semibold tracking-tight">Creating an API key</h2>
      <p class="mt-3 text-sm leading-relaxed text-muted-foreground">Create a key from the dashboard under Settings → API keys, then pass it as a bearer token on every request:</p>
      <div class="mt-4 overflow-hidden rounded-xl border border-border/60 bg-zinc-950">
        <div aria-hidden="true" class="flex items-center gap-1.5 border-b border-white/10 px-4 py-2.5">
          <span class="h-2.5 w-2.5 rounded-full bg-red-500/80 border border-transparent"></span>
          <span class="h-2.5 w-2.5 rounded-full bg-amber-500/80 border border-transparent"></span>
          <span class="h-2.5 w-2.5 rounded-full bg-emerald-500/80 border border-transparent"></span>
          <span class="ms-2 font-mono text-xs text-white/60">terminal</span>
        </div>
        <pre class="overflow-x-auto p-4 text-xs leading-relaxed"><code class="font-mono text-zinc-300">curl https://api.acme.dev/v1/projects \
  -H &quot;Authorization: Bearer $ACME_API_KEY&quot;</code></pre>
      </div>
      <div class="mt-6 flex gap-3 rounded-xl border border-sky-500/30 bg-sky-500/10 p-4">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info mt-0.5 h-4 w-4 shrink-0 text-sky-500" aria-hidden="true">
          <circle cx="12" cy="12" r="10"></circle>
          <path d="M12 16v-4"></path>
          <path d="M12 8h.01"></path>
        </svg>
        <p class="text-sm leading-relaxed text-muted-foreground"><span class="font-medium text-foreground">Note:</span> the full secret is shown once, at creation. Acme stores only a hash, so a lost key cannot be recovered — it can only be rotated.</p>
      </div>
      <h2 id="rotating-keys" class="mt-10 text-xl font-semibold tracking-tight">Rotating keys</h2>
      <p class="mt-3 text-sm leading-relaxed text-muted-foreground">Rotation is overlap, not replacement: create the new key, deploy it everywhere, and only then revoke the old one. Both keys stay valid during the window, so a slow rollout never turns into an outage.</p>
      <nav aria-label="Pagination" class="mt-12 grid gap-4 sm:grid-cols-2">
        <a href="#quickstart" class="group flex min-w-0 items-center gap-3 rounded-xl border border-border/60 bg-card/60 p-4 transition-colors hover:border-primary/40">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-left h-4 w-4 shrink-0 text-muted-foreground transition-colors group-hover:text-primary rtl:rotate-180" aria-hidden="true">
            <path d="m12 19-7-7 7-7"></path>
            <path d="M19 12H5"></path>
          </svg>
          <span class="min-w-0">
            <span class="block text-xs text-muted-foreground">Previous</span>
            <span class="block truncate text-sm font-medium">Quickstart</span>
          </span>
        </a>
        <a href="#sdks" class="group flex min-w-0 items-center justify-end gap-3 rounded-xl border border-border/60 bg-card/60 p-4 text-end transition-colors hover:border-primary/40">
          <span class="min-w-0">
            <span class="block text-xs text-muted-foreground">Next</span>
            <span class="block truncate text-sm font-medium">SDKs &amp; clients</span>
          </span>
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right h-4 w-4 shrink-0 text-muted-foreground transition-colors group-hover:text-primary rtl:rotate-180" aria-hidden="true">
            <path d="M5 12h14"></path>
            <path d="m12 5 7 7-7 7"></path>
          </svg>
        </a>
      </nav>
    </article>
    <nav aria-label="On this page" class="hidden xl:block">
      <h2 class="text-xs font-bold uppercase tracking-wider text-muted-foreground">On this page</h2>
      <ul class="mt-3 space-y-1 border-s border-border/60">
        <li>
          <a href="#creating-an-api-key" aria-current="location" class="-ms-px block border-s py-1 ps-4 text-sm transition-colors border-primary font-medium text-primary">Creating an API key</a>
        </li>
        <li>
          <a href="#rotating-keys" class="-ms-px block border-s py-1 ps-4 text-sm transition-colors border-transparent text-muted-foreground hover:text-foreground">Rotating keys</a>
        </li>
      </ul>
    </nav>
  </div>
</div>
  • This is rendered HTML, not a translation of the React source. The Tailwind classes carry the design and work in any framework.
  • It is one frame: the component in its initial state, with no props applied beyond the defaults.
  • Requires Tailwind, and the design tokens the classes reference (bg-card, text-muted-foreground, and so on). The template ZIPs ship a globals.css that defines them.

What each framework gets across the whole catalog — effects convert properly; this rung is markup.

For AI

The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.

See the prompt

Used in these pages

Want the whole screen instead of this one section? Open a page and copy it entire.

11 more blocks in Content & Blog

All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.

Browse Content & Blog

Shipping one commercially

Copying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.

  • A commercial licence for everything in the catalog
  • Unlimited bundle exports, in Vue, Svelte and Tailwind
  • One payment — no subscription, nothing to renew
Pro — $79 once

More Content & Blog blocks

View category
Open the full page for this block

Release Timeline

A dated changelog on a vertical rail with added/changed/fixed tags and machine-readable <time> values.

Content & Blog145 linesNo deps
Open the full page for this block

Public Roadmap Columns

Shipped / in progress / planned in three columns, with status carried by icon as well as colour.

Content & Blog122 lines1 dep

Blog Index Grid

A featured post at full width above a card grid, with drawn covers built from tokens — a blog index with no image pipeline and no broken-thumbnail state.

Content & Blog188 lines1 dep

Case Study Card Grid

A grid of case-study cards that lead with the metric rather than the company name, each one linking out to the full write-up — the block a customer-stories section or a home page needed and only had a full case-study page for.

Content & Blog136 lines1 dep
Open the full page for this block

Article Header & Byline

Kicker, headline, standfirst, byline and share row, with the opening prose and a pull quote included — the type scale of a post, decided once instead of per-article.

Content & Blog171 lines1 dep
Open the full page for this block

Team Grid

An about-page team section on initials avatars rather than photos, so shipping it never waits on headshots and there is no broken-image state to design.

Content & Blog166 lines1 dep

Careers Openings Board

Openings grouped by department with location, type and salary on every row — candidates self-select by craft first, and a flat list makes them scan for it.

Content & Blog186 lines1 dep
Open the full page for this block

API Endpoint Reference

Method badge, path, parameter table and a request/response pair kept side by side — an example request whose response is a scroll away gets pasted blind.

Content & Blog237 lines1 dep
Open the full page for this block

Package Manager Code Tabs

One install command behind npm/pnpm/yarn/bun tabs with a working copy button — every reader uses exactly one package manager, so showing all four wastes three lines.

Content & Blog152 lines1 dep
Open the full page for this block

Résumé Document

A CV as a document rather than a timeline of cards — one column, dates on the end edge, and a print layer that puts it on paper without a separate PDF to keep in step.

Content & Blog347 lines1 dep
Open the full page for this block

Video Player Shell

The watch screen with no playback in it: a ratio-locked stage, a control bar drawn down to the buffered layer, and an up-next rail. Drop a video element in and wire your own player.

Content & Blog327 lines1 dep