---
title: Reading Sidebar
description: A sticky reading sidebar that fuses a table of contents, an active-section highlight and an article-scoped progress bar into one "where am I" affordance.
visibility: guest
draft: false
related:
  - /components/reading
  - /components/sticky-panel
  - /blueprints/recipes/on-page-toc
  - /blueprints/recipes/long-form-article
---

# Reading Sidebar

A sticky vertical reading sidebar whose progress + TOC track **one article**, not the whole page. Reach for this over the [on-page TOC](/blueprints/recipes/on-page-toc) recipe when the reading surface is **scoped to a container** rather than the document — an article embedded in a dashboard panel, one of several articles on a page, a modal/drawer reading view, or a Livewire pane. The distinction is the progress math: page-scoped progress fills as you scroll the whole document; article-scoped progress fills as you scroll just the bounded article.

## The canonical wiring

One tag does it: `<x-wirekit::reading-shell boundary="container">` composes the progress bar + the sidebar TOC + the bookmark, and `boundary="container"` scopes every composed piece to the shell's nearest positioned ancestor instead of the viewport. Wrap your article body inside it; the spine reads the `<h2>` / `<h3>` headings, the progress bar tracks scroll through that container, and the active link updates as the reader moves.

:::preview{title="Article-scoped reading sidebar", frame="iframe", height="640px", flush="true"}
<x-wirekit::container max="2xl" padding="lg">
<x-wirekit::reading-shell :previewMode="true" boundary="container" bookmarkKey="reading-sidebar-recipe-demo" style="--reading-spine-width-expanded: 12rem;">
    <x-wirekit::prose density="compact">
        <article class="wk-reading-content">
            <h2 id="rs-intro">Introduction</h2>
            <p>The progress bar and the sidebar TOC here are scoped to <em>this</em> article container — not the whole page. Scroll down: the progress fills as you reach the end of the article, and the active link tracks the section in view.</p>
            <p>This is the right shape when the reading surface is a bounded region — one article among several on a page, a panel inside a dashboard, or a modal reading view — where page-scoped progress would mislead the reader.</p>

            <h2 id="rs-setup">Setup</h2>
            <p>Wrap the article body in a single <code>&lt;x-wirekit::reading-shell boundary="container"&gt;</code>. The <code>boundary</code> prop swaps the composed primitives from viewport-pinned to container-scoped, so the progress math measures this container's scroll rather than the document's.</p>

            <h3 id="rs-prerequisites">Prerequisites</h3>
            <p>The container needs a defined height and its own scroll (<code>overflow: auto</code>) for container-scoped progress to have something to measure. Heading IDs must be present so the TOC's scroll-to-anchor links land cleanly.</p>

            <h2 id="rs-customization">Customization</h2>
            <p>Override <code>--reading-spine-width-expanded</code> for the expanded sidebar width, and the <code>--reading-spine-color-active</code> token for the active-link color. Both cascade through CSS-variable inheritance from the wrapping element.</p>

            <h2 id="rs-notes">Notes</h2>
            <p>For a page-scoped sidebar (progress tracks the whole document), use the <a href="/blueprints/recipes/on-page-toc">on-page TOC</a> recipe instead — same primitives, viewport-pinned rather than container-scoped.</p>
        </article>
    </x-wirekit::prose>
</x-wirekit::reading-shell>
</x-wirekit::container>
:::

:::source{language="blade"}
{{-- # 1. reading-shell composes progress + sidebar TOC + bookmark in one tag --}}
{{-- # 2. boundary="container" scopes them to THIS container, not the viewport --}}
{{-- # 3. The scroll container needs its own height + overflow:auto; label it as --}}
{{--      a keyboard-reachable region (WCAG 2.1.1) so it is operable by keyboard --}}
<div role="region" aria-label="Article" tabindex="0" style="height: 100vh; overflow: auto;">
    <x-wirekit::reading-shell boundary="container" bookmarkKey="article-42">
        <article>
            <h2 id="intro">Introduction</h2>
            <p>…</p>

            <h2 id="setup">Setup</h2>
            <h3 id="prerequisites">Prerequisites</h3>
            <p>…</p>
        </article>
    </x-wirekit::reading-shell>
</div>
:::

## Always-expanded sidebar

The default sidebar is narrow at rest and expands on hover. On a dedicated reading layout where the reader expects the TOC, compose the primitives directly instead of the shell: place a `<x-wirekit::reading-progress target="#article" boundary="container" />` and a `<x-wirekit::reading-spine target="#article" expand="always" boundary="container" />` next to your `<main id="article">`. The `expand="always"` prop keeps the spine permanently open, and `boundary="container"` keeps both scoped to the article container.

## CSS variables

| Variable | Default | Purpose |
|---|---|---|
| `--reading-spine-width-expanded` | `16rem` | Expanded sidebar width |
| `--reading-spine-offset-top` | `1rem` | Sticky offset from the container top |
| `--reading-spine-color-active` | `var(--color-wk-text)` | Active-link color |

For the full token reference, see [`/theming`](/theming).

## See also

- [`<x-wirekit::reading-shell>`](/components/reading) — the one-tag wrapper used above.
- [`<x-wirekit::reading-spine>`](/components/reading) — the sidebar TOC primitive.
- [`<x-wirekit::reading-progress>`](/components/reading) — the progress bar (supports `target` + `boundary` for article scope).
- [`<x-wirekit::sticky-panel>`](/components/sticky-panel) — a generic sticky shell if you want to hand-roll the sidebar container.
- [on-page TOC recipe](/blueprints/recipes/on-page-toc) — the page-scoped (viewport-pinned) variant.
