Skip to main content
Exit fullscreen Esc

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 — every visible-text alignment, spacing, and typography rule comes from the components' built-in defaults rather than from a stylesheet:

  • <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" / "FAQ" 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>
</x-wirekit::prose>

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. As the reader scrolls, the component works out which heading has come up to the line just below the strip and updates activeIndex. 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

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.
  • The line that decides the active section — so a section becomes "active" when its heading comes up to the strip, not when it reaches 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