Skip to main content
WireKit

Marketing Landing TOC

The canonical pattern for marketing landing pages with 3-4 anchored sections (Hero, Features, Pricing, FAQ). A horizontal sticky strip sits across the top of the article container, highlighting the link for whichever section is currently in view as the reader scrolls.

Sibling pattern to the long-form-article recipe. The two split cleanly:

  • Long-form article — vertical sidebar spine + bookmark + meta. Content is many headings deep, dense; hover-expand TOC suits.
  • Marketing landing TOC — horizontal strip + sticky-position. Content is flat (4 sections, all <h2>), wide; horizontal strip is the right shape.

<x-wirekit::reading-toc> and <x-wirekit::reading-spine> are intentionally not interchangeable — pick by content shape.

Full Composition

Marketing landing page with reading-toc strip

The preview above is pure WireKit, zero custom CSS — every visible-text alignment, spacing, and typography rule comes from the components' built-in defaults:

  • <x-wirekit::brand-bar padding="sm"> carries the content-edge spine via --padding-wk-x-sm (= 0.625 rem = 10 px) — the brand text sits 10 px inside the viewport edge so it lines up with the article-body content edge below.
  • <x-wirekit::reading-toc flush> extends viewport-edge-to-viewport-edge with zero horizontal padding on the nav AND the list AND the first/last link. The strip's chrome (background + bottom border) and the visible "Features" / "Pricing" / "Extras" text all run flush to the viewport boundary — giving the TOC strip an "anchored" feel against the marketing-page chrome.
  • <x-wirekit::prose density="compact"> tightens the heading + paragraph margins so the marketing-page rhythm reads tight without the generous 2.5-rem h2 top-margin of the long-form-article default.

Drop the recipe into your own Blade view as-is:

{{-- 1. Brand-bar — logo, optional tagline, optional right-side actions
        (e.g. a Sign-in link). Carries the content-edge spine padding
        out of the box. --}}
<x-wirekit::brand-bar padding="sm">
    <x-slot:brand>
        <x-wirekit::brand name="⚡ Acme" />
    </x-slot:brand>
    <x-slot:tagline>Ship faster, in less time.</x-slot:tagline>
</x-wirekit::brand-bar>

{{-- 2. Sticky TOC strip with `flush` so the strip's chrome and the
        first link's visible text run viewport-edge-to-viewport-edge
        (zero horizontal padding on the nav). Auto-scans the nearest
        <main>/<article> for <h2> headings and renders one link per
        heading. --}}
<x-wirekit::reading-toc flush />

{{-- 3. Prose wrapper with `density="compact"` for marketing-page
        rhythm: tighter heading margins, smaller h2 type scale than
        the long-form-article default. --}}
<x-wirekit::prose density="compact">
    <article>
        <section id="features"><h2>Features</h2>...</section>
        <section id="pricing"><h2>Pricing</h2>...</section>
        <section id="faq"><h2>FAQ</h2>...</section>
    </article>
</main>

What's happening

  1. <x-wirekit::reading-toc /> renders a <nav aria-label="Page sections"> immediately inside <main>. The strip auto-scans <main> for <h2> elements (default levels="2") and renders one link per heading.
  2. The strip itself is position: sticky with top: var(--reading-toc-offset). Default offset is 0 — the strip sits flush against the viewport top. Pass offset="4rem" to clear a 64px fixed nav above.
  3. An IntersectionObserver tracks which section is in the viewport and updates activeIndex as the reader scrolls. The active link gets aria-current="location" and the active-state color from --reading-toc-color-active.
  4. Click any link → smooth-scroll to the matching <section> heading; the URL hash updates without a full history push.

Add a fixed nav above the TOC

If your real app already pins a brand-bar / primary-nav to the top of the viewport, the TOC strip needs to sit BELOW it on scroll. Pass an offset matching the nav's height — the value flows into the sticky top position, the IntersectionObserver's rootMargin, AND the smooth-scroll target math, all from one source.

The preview below mirrors the first preview's content. The brand-bar stays in normal flow — it scrolls away with the rest of the article — and the TOC strip pins to the viewport top once the user scrolls past it. No sticky on the brand-bar, no offset on the TOC: the simplest composition, the one developers reach for first.

If your real app already pins a brand-bar / primary-nav to the top of the viewport (a true position: fixed element OUTSIDE the article container, e.g. an app-level nav), the TOC needs to clear that height. Pass an offset matching the nav's height — the value flows into the sticky top position, the IntersectionObserver's rootMargin, AND the smooth-scroll target math, all from one source. The offset is shown in the snippet below the preview.

Reading TOC pinning to top after the brand-bar scrolls away
{{-- 1. brand-bar in normal flow — it scrolls with the article and
        leaves the viewport as the reader scrolls past. The `actions`
        slot anchors a sign-in link to the bar's right content edge. --}}
<x-wirekit::brand-bar padding="sm">
    <x-slot:brand>
        <x-wirekit::brand name="Brand" />
    </x-slot:brand>
    <x-slot:actions>
        <a href="/login">Sign in</a>
    </x-slot:actions>
</x-wirekit::brand-bar>

{{-- 2. `flush` keeps the first link's visible text on the same
        vertical spine as the brand text above and the h2 headings
        below. With no `offset`, the strip sticks to the viewport
        top (`top: 0`) once the brand-bar has scrolled away. --}}
<x-wirekit::reading-toc flush />

{{-- 3. If your real app pins a primary nav above the article (a
        true `position: fixed` element outside this snippet's scope),
        pass `offset="4rem"` matching the nav's height. The value
        flows into the sticky `top`, the IntersectionObserver's
        `rootMargin`, AND the smooth-scroll target math at once. --}}
{{-- <x-wirekit::reading-toc flush offset="4rem" /> --}}

<x-wirekit::prose density="compact">
    <article>
        {{-- 4. Anchored sections — same as the first preview. --}}
        <section id="features"><h2>Features</h2>...</section>
        <section id="pricing"><h2>Pricing</h2>...</section>
        <section id="extras"><h2>Extras</h2>...</section>
    </article>
</x-wirekit::prose>

The offset value passes through to:

  • position: sticky; top: var(--reading-toc-offset) — the visual offset of the strip.
  • IntersectionObserver rootMargin — so a section becomes "active" when its heading crosses the bottom edge of the strip, not the top of the viewport.
  • scrollTo math — so smooth-scroll lands the heading just below the strip, not under it.

Use with reading-shell

The recipe above uses <x-wirekit::reading-toc> directly. The shell wrapper is the alternative if you want a single tag that bundles the TOC with whichever other reading-* primitives apply. :toc="true" :spine="false" is the canonical marketing-landing combo. The shell's other defaults (progress bar, bookmark) still apply unless you turn them off explicitly.

reading-shell wrapping the marketing TOC

When NOT to use

If your page has more than 5-6 anchored sections, or any nested <h3> headings under the <h2>s, the strip overflows fast at viewport widths under 1024px — even with the per-link --reading-toc-link-max-width: 24ch truncation. Reach for <x-wirekit::reading-spine hideBelow="none"> instead. The spine's collapsed-tick mode is the right shape for dense vertical TOCs at narrow widths.

See Also