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
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
<x-wirekit::reading-toc />renders a<nav aria-label="Page sections">immediately inside<main>. The strip auto-scans<main>for<h2>elements (defaultlevels="2") and renders one link per heading.- The strip itself is
position: stickywithtop: var(--reading-toc-offset). Default offset is0— the strip sits flush against the viewport top. Passoffset="4rem"to clear a 64px fixed nav above. - An IntersectionObserver tracks which section is in the viewport and updates
activeIndexas the reader scrolls. The active link getsaria-current="location"and the active-state color from--reading-toc-color-active. - 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.
{{-- 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. scrollTomath — 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.
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
<x-wirekit::reading-toc>— full props reference<x-wirekit::reading-spine>— vertical sidebar TOC for dense docs- Long-form Article recipe — the sibling pattern for blog posts