---
title: Marketing Landing TOC
description: Sticky horizontal TOC strip across the top of a marketing landing page — Hero, Features, Pricing, FAQ — with active-section highlighting.
visibility: guest
draft: false
related:
  - /components/reading
  - /blueprints/recipes/long-form-article
  - /components/hero
  - /components/feature-grid
  - /components/cta
---

# 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](/blueprints/recipes/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

:::preview{title="Marketing landing page with reading-toc strip", frame="iframe", height="640px", flush="true"}
<x-wirekit::container padding="sm" max="full">
<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>
<x-wirekit::reading-toc flush />
<x-wirekit::prose density="compact">
    <article style="padding: 0 var(--padding-wk-x-sm);">
        <section id="ml-features">
            <h2>Features</h2>
            <p>Bulleted pitch — three columns, each with an icon and a paragraph. Click "Features" in the strip above to jump straight here. The IntersectionObserver under the hood updates the active link as soon as this heading crosses the viewport-top + offset line.</p>
            <p>What makes the strip useful for marketing pages: flat anchor structure (4 sections, all <code>&lt;h2&gt;</code>), wide content. A vertical sidebar spine would feel excessive when there are only four jumps. The horizontal strip is the right shape.</p>
            <p>Each link auto-truncates at <code>--reading-toc-link-max-width: 24ch</code> so a long heading doesn't push siblings off-screen. The list itself is <code>overflow-x-auto</code>, so a strip with 6+ items still scrolls cleanly without breaking the page layout.</p>
            <p>The strip is mobile-hidden by default (<code>hideBelow="sm"</code>), since narrow viewports can't host a 3-4-link horizontal nav without overflow. The vertical reading-spine takes over below that breakpoint when both are present.</p>
            <p>Scroll past this paragraph to flip the active link to "Pricing". The active section is decided at the same line a jump stands a heading on, just below the strip, so the flip lands there rather than at the viewport top.</p>
            <p>The strip's keyboard model leans on the underlying anchor links — <kbd>Tab</kbd> moves between visible links, <kbd>Enter</kbd> activates the smooth-scroll. No custom focus ring is needed; the focus-visible state inherits the global <code>--color-wk-ring</code> token so it stays consistent with every other nav primitive on the page.</p>
            <p>Screen readers announce the strip as "Page sections, navigation" thanks to the <code>aria-label="Page sections"</code> on the wrapping <code>&lt;nav&gt;</code>. Each link carries the destination heading text directly — no extra ARIA labeling needed.</p>
            <p>Idle-state link color is <code>--reading-toc-color-idle</code> (muted-text by default). The active state swaps to <code>--reading-toc-color-active</code> (accent by default). Both tokens cascade through the theme system, so a Cupertino-themed page sees the strip in Cupertino blue automatically.</p>
            <p>Reduced-motion users get an instant active-link swap with no fade — the 150 ms transition on the link color collapses to <code>1ms</code> via the global <code>@media (prefers-reduced-motion: reduce)</code> block. Smooth-scroll collapses to instant scroll the same way.</p>
            <p>The strip's hover state is intentionally subtle — a 4% accent-tinted background on the link's pill chrome — so an inactive section's hover doesn't compete visually with the active link's bolder weight + accent color. Hover affordance without distraction.</p>
        </section>
        <section id="ml-pricing">
            <h2>Pricing</h2>
            <p>Three tiers, each in a card. The TOC link for "Pricing" highlights as soon as the reader scrolls into this section. The smooth-scroll math accounts for the strip's offset so the destination heading lands just below the strip, not under it.</p>
            <p>The active-state color comes from <code>--reading-toc-color-active</code>. By default it's the accent token; developers can override per-page via inline custom-property declarations or globally via <code>app.css</code>. Idle and active states transition over 150 ms unless <code>prefers-reduced-motion: reduce</code> is set.</p>
            <p>Click any link → smooth-scroll. The URL hash updates via <code>history.replaceState()</code>, not <code>pushState()</code> — back-button still goes to the previous page rather than the previous anchor.</p>
            <p>The hover state on each pricing card uses the same elevation token as the rest of the system (<code>--shadow-wk-md</code>). Adding a discount badge or "most popular" pill is a one-line override on the card's <code>:slot="badge"</code> if the recipe developer needs it.</p>
            <p>For longer pricing pages — multiple tiers per region, add-on rows, FAQ accordions per tier — switch the TOC's <code>levels="2,3"</code> so the strip also picks up the per-tier sub-headings.</p>
            <p>Tier comparison tables nest naturally under a pricing section. The TOC strip ignores any heading deeper than the configured level, so per-tier <code>&lt;h3&gt;</code> headings with <code>levels="2"</code> don't pollute the strip — but pass <code>levels="2,3"</code> and they become first-class anchors.</p>
            <p>Deep-linking works out of the box. Sharing a URL ending in <code>#ml-pricing</code> scrolls the reader directly to this section on page load, and the IntersectionObserver highlights the matching link once it settles. That first jump is the browser's own fragment navigation, so give the headings a <code>scroll-margin-top</code> when the strip carries an <code>offset</code> — the component adds nothing to a scroll it did not start.</p>
            <p>If your pricing section is dynamically rendered (Livewire pricing-tier component with locale-driven content), the TOC re-observes its target on every Livewire morph — the strip stays in sync as the section's heading or layout changes.</p>
            <p>Pricing-page conversion best practice: keep one canonical CTA per tier, repeat the primary CTA at the section's bottom edge so scrolling past the table lands on a buy button. The TOC handles the navigational symmetry — readers can jump back up to compare tiers without losing the CTA in the fold.</p>
            <p>For currency or region toggles, render them OUTSIDE the article container so they don't interfere with the TOC's section-heading scan. A toolbar above the brand-bar is the canonical placement; a sticky toolbar BELOW the TOC works too with appropriate <code>offset</code> stacking.</p>
        </section>
        <section id="ml-faq">
            <h2>FAQ</h2>
            <p>Common questions in a bulleted list — last anchor in the strip. The strip stays visible all the way down the page (<code>position: sticky; top: 0</code> by default), so the reader can jump back to any earlier section without scrolling all the way up.</p>
            <p>One reasonable extension is to add a "Back to top" pseudo-anchor at the end of the strip. Pass <code>:backToTop="true"</code> on a future revision; until then, developers can append a manual link at the strip's edge using the slot pattern.</p>
            <p>If your landing page grows beyond 5-6 anchored sections, switch to the vertical reading-spine instead. The horizontal strip overflows fast at viewport widths under 1024 px, even with per-link truncation.</p>
            <p>Bottom of the page — scrolling further won't flip any link past "FAQ" because there is no further anchor. The active state stays sticky to the last in-view section, which is the documented behavior.</p>
            <p>FAQ entries themselves benefit from the <code>&lt;x-wirekit::accordion&gt;</code> component — each question becomes an expandable row, with the answer hidden by default. The accordion's open/closed state is independent of the TOC active-link state, so opening an entry doesn't perturb the strip.</p>
            <p>For structured data, <code>&lt;x-wirekit::faq&gt;</code> emits the FAQPage JSON-LD for the questions it renders (its <code>schema</code> prop, on by default), so the page needs no second block. The TOC and that script coexist without interference.</p>
            <p>If a question links to a docs page, prefer same-tab navigation for in-domain links and an opt-in <code>target="_blank" rel="noopener"</code> for cross-domain links. The strip stays anchored to the current page; cross-domain navigation breaks the back-button-to-anchor contract.</p>
            <p>An "Ask another question" CTA at the bottom of the FAQ section flows naturally without disrupting the strip — the section's last paragraph and the CTA share the same anchor (<code>#ml-faq</code>), so the active link doesn't flicker between two sections at the boundary.</p>
            <p>For lengthy FAQ blocks (15+ questions), consider grouping them by topic with sub-headings (<code>&lt;h3&gt;</code>) and passing <code>levels="2,3"</code> to the TOC so each topic group gets its own anchor in the strip.</p>
            <p>The strip's <code>position: sticky</code> + <code>overflow-x: auto</code> combination remains stable to the bottom of the page — even on long FAQ sections that scroll past the natural viewport multiple times. No re-layout, no re-paint of the strip chrome.</p>
        </section>
    </article>
</x-wirekit::prose>
</x-wirekit::container>
:::

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:

```blade
{{-- 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.

:::preview{title="Reading TOC pinning to top after the brand-bar scrolls away", frame="iframe", height="640px", flush="true"}
<x-wirekit::container padding="sm" max="full">
<x-wirekit::brand-bar padding="sm">
    <x-slot:brand>
        <x-wirekit::brand name="Brand" />
    </x-slot:brand>
    <x-slot:actions>
        <x-wirekit::text size="sm" as="span"><x-wirekit::link href="/login" variant="muted" underline="none">Sign in</x-wirekit::link></x-wirekit::text>
    </x-slot:actions>
</x-wirekit::brand-bar>
<x-wirekit::reading-toc flush />
<x-wirekit::prose density="compact">
    <article style="padding: 0 var(--padding-wk-x-sm);">
        <section id="ml-offset-features">
            <h2>Features</h2>
            <p>This strip carries no <code>offset</code>, so it pins flush to the viewport top (<code>position: sticky; top: 0</code>) once the brand-bar scrolls away — the shape a page reaches for first. Pass <code>offset="4rem"</code> and the same value flows into the visual position AND the IntersectionObserver root margin together, so the active section flips at the strip's bottom edge rather than at the viewport top.</p>
            <p>Try scrolling: the brand-bar above is in normal flow and scrolls away, and the TOC takes its place at the very top. The active link cycles between Features and Pricing as the headings cross the strip's bottom edge.</p>
            <p>The offset value passes through to all three places at once: <code>position: sticky; top</code>, the IntersectionObserver's <code>rootMargin</code>, and the smooth-scroll target offset. A single source of truth keeps the visual position and the active-detection point in lockstep.</p>
            <p>Mounting a fixed brand-bar above the TOC is the common production shape. Pass the brand-bar's height as a number plus a unit — <code>offset="4rem"</code>, <code>offset="64px"</code> — and the TOC pins directly beneath it. The prop is a length, not a CSS expression: <code>var(…)</code> and <code>calc(…)</code> are rejected and fall back to <code>0</code> silently, so read the height token's value and write it out.</p>
            <p>Brand-bar heights commonly land at 3.5rem, 4rem, or 5rem depending on whether they carry a logo + nav + actions or just a slim brand-only strip. The offset is a prop rather than a cascading variable, so it travels per instance — put the number in a Blade variable or a config value your layout passes down, and every strip stays in step from one place.</p>
            <p>If multiple TOCs are present on the same page (rare but legitimate — e.g. a long-form article with both a top strip and a sidebar spine), each carries its own offset prop. They observe the same headings but render independently.</p>
            <p>For a sticky brand-bar instead of fixed, the offset behavior is identical — sticky elements occupy the same viewport region. An offset TOC's <code>top</code> simply stacks below the brand-bar's own <code>top: 0</code>, and the IntersectionObserver root margin clears both.</p>
            <p>When the offset value comes from a JS-resolved measurement (e.g. <code>document.querySelector('.brand-bar').offsetHeight</code>), resolve it on the server and bind the prop — <code>:offset="$navHeight . 'px'"</code>. The value is read once, when the strip initializes, and drives the sticky <code>top</code>, the IntersectionObserver <code>rootMargin</code> and the scroll target from that one read; changing it afterwards does not move the strip.</p>
            <p>Keep scrolling — Features's section keeps pulling content while Pricing waits below. The active link should flip exactly when this section's bottom edge crosses the strip's bottom edge, not earlier and not later.</p>
        </section>
        <section id="ml-offset-pricing">
            <h2>Pricing</h2>
            <p>Click either link in the strip to smooth-scroll. The math accounts for the offset so the destination heading is fully visible — not hidden under the strip. The same offset value drives all three concerns: visual position, active-detection rootMargin, and scrollTo target.</p>
            <p>If your real app's fixed nav changes height, update the offset in lockstep. They're two halves of the same layout contract.</p>
            <p>For developers whose top nav is sticky rather than fixed, the offset still applies — sticky elements occupy the same viewport region, so the TOC has to clear them either way.</p>
            <p>Responsive brand-bars (taller on desktop, slimmer on mobile) need one decision rather than a breakpoint ladder: the offset is read once, at initialization, so it does not follow a media query. Pass the DESKTOP height — below the <code>hideBelow</code> breakpoint the strip is not rendered anyway, which is where the slimmer bar lives.</p>
            <p>If the height still differs across the widths where the strip IS shown, pick the taller of the two. Overshooting leaves a gap above the strip; undershooting tucks it under the nav, and the same value also sets where the active link flips — so the pessimistic number is the one that keeps both readable.</p>
            <p>The click-scroll does its own arithmetic — <code>window.scrollTo()</code> with the heading's position minus the offset, minus the strip's height, minus a little breathing room — so <code>scroll-margin-top</code> on the heading has no effect on it. That property is read by the BROWSER for a URL-fragment jump, which is the one path the component is not involved in, so setting it is how you make a shared <code>#anchor</code> land in the same place a click does.</p>
            <p>Mobile-hidden default (<code>hideBelow="sm"</code>) means the offset prop is effectively dormant under 640 px viewports. The strip is display:none, so the offset doesn't push any content. Override with <code>hideBelow="none"</code> if your design has space for the strip on mobile — <code>none</code> is the escape hatch, and any other value falls back to the <code>sm</code> default rather than failing.</p>
            <p>The brand-bar's own <code>z-index</code> stacks above the TOC by default (the strip is sticky inside the article container; the brand-bar is fixed outside). If you flip that order, the TOC briefly disappears under the brand-bar on overlap — usually not desired.</p>
            <p>Pricing-page-specific tip: pixels are as welcome as rems — <code>offset="64px"</code>. Useful when integrating with a third-party fixed-nav whose height isn't exposed as a token. Keep the unit: a bare <code>64</code> passes the prop's own parse and then lands in <code>top: 64</code>, which is not a length, so the browser drops it and the strip pins at the viewport top.</p>
        </section>
        <section id="ml-offset-extras">
            <h2>Extras</h2>
            <p>If your top nav lives outside the article container — a sibling to <code>&lt;main&gt;</code> at the layout root — the offset is the only adjustment the TOC needs. The article body itself sits in normal flow and doesn't need a matching <code>padding-top</code>.</p>
            <p>If your top nav lives INSIDE the article container (as a header preceding the TOC, like the brand-bar in this preview), the article body's padding-top is implicit — the brand-bar takes its own vertical space.</p>
            <p>The offset prop is the single layout primitive: bump it to match whatever sits above the TOC, regardless of whether that thing is fixed, sticky, or in flow.</p>
            <p>For deeply nested layouts (e.g. a sidebar shell with a header bar and a sub-nav per route), add the two heights up yourself and pass the sum as one length — <code>offset="7rem"</code>. The prop takes a number and an optional <code>px</code> / <code>rem</code> / <code>em</code>, nothing more; a <code>calc()</code> is discarded and the strip falls back to <code>0</code> without saying so.</p>
            <p>When the page has both a brand-bar AND a secondary nav row (e.g. category tabs), that sum is the offset. A sub-nav that appears on only some routes is therefore a per-route value: render the strip with the offset that route actually has, rather than expecting one instance to follow a nav that grows after it mounted.</p>
            <p>If your article-body container has its own padding-top (e.g. <code>&lt;main style="padding-top: 2rem"&gt;</code>), that padding doesn't affect the TOC's offset — the strip's sticky position is relative to the viewport, not the article container. The article's padding only affects where the heading text sits, not the strip.</p>
            <p>For pages with anchor-based deep-linking (URL hashes that scroll the reader directly to a section), the jump on page load is the browser's, not the strip's — the component reads no hash and adds no offset to it. Give the headings a <code>scroll-margin-top</code> that matches the offset plus the strip's own height, or a shared <code>#ml-offset-pricing</code> link lands with the heading tucked underneath.</p>
            <p>Print stylesheets typically want the TOC hidden — <code>@media print { .wk-reading-toc { display: none } }</code> achieves that. The article body retains its anchor structure, which prints fine without the strip.</p>
            <p>Final note on the offset contract: it's a one-way push. The TOC clears content ABOVE it via the offset; content BELOW the TOC is unaffected. No magic — just a sticky-top + IO-rootMargin + scroll-target-math triple that all read the same value.</p>
        </section>
    </article>
</x-wirekit::prose>
</x-wirekit::container>
:::

```blade
{{-- 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.

:::preview{title="reading-shell wrapping the marketing TOC", frame="iframe", height="520px", flush="true"}
<x-wirekit::reading-shell :toc="true" :spine="false" :previewMode="true" bookmarkKey="marketing-toc-shell-demo">
    <x-wirekit::section padding="xl" id="mlt-features">
        <x-wirekit::container max="2xl">
            <x-wirekit::stack gap="md">
                <x-wirekit::heading level="2" size="2xl">Features</x-wirekit::heading>
                <x-wirekit::text size="lg" intent="muted">The shell wraps the TOC strip + progress bar in a single tag. Pass <code>:spine="false"</code> so the right-edge sidebar stays off — marketing pages typically don't need it.</x-wirekit::text>
            </x-wirekit::stack>
        </x-wirekit::container>
    </x-wirekit::section>
    <x-wirekit::section padding="xl" background="muted" id="mlt-pricing">
        <x-wirekit::container max="2xl">
            <x-wirekit::stack gap="md">
                <x-wirekit::heading level="2" size="2xl">Pricing</x-wirekit::heading>
                <x-wirekit::text size="lg" intent="muted">Click the "Pricing" link in the TOC strip above to smooth-scroll here.</x-wirekit::text>
            </x-wirekit::stack>
        </x-wirekit::container>
    </x-wirekit::section>
    <x-wirekit::section padding="xl" id="mlt-faq">
        <x-wirekit::container max="2xl">
            <x-wirekit::stack gap="md">
                <x-wirekit::heading level="2" size="2xl">FAQ</x-wirekit::heading>
                <x-wirekit::text size="lg" intent="muted">The bookmark prompt is still active because <code>:bookmark="true"</code> is the shell default.</x-wirekit::text>
            </x-wirekit::stack>
        </x-wirekit::container>
    </x-wirekit::section>
</x-wirekit::reading-shell>
:::

:::source{language="blade"}
<x-wirekit::reading-shell :toc="true" :spine="false" :bookmark-key="'landing-page'">
    {{-- your sections --}}
</x-wirekit::reading-shell>
:::

## 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>`](/components/reading#reading-toc) — full props reference
- [`<x-wirekit::reading-spine>`](/components/reading#reading-spine) — vertical sidebar TOC for dense docs
- [Long-form Article recipe](/blueprints/recipes/long-form-article) — the sibling pattern for blog posts
