Skip to main content
WireKit
Copy for LLM

Reading

The reading-* family — a set of complementary primitives for long-form content pages.

A blog post, a docs page, a long-form article: the family covers "where am I in the article", "navigate the structure", "see the density at a glance", "resume where I left off", "how long is this", and a one-tag composition wrapper.

Viewport-scoped layout The reading-* family of primitives defaults to position: fixed relative to the viewport — they're designed for full-page reading experiences (article pages, docs sites). Avoid nesting one inside a normal-width card, grid, or dashboard tile without the boundary prop: the fixed element ignores its parent's intrinsic width, producing double scrollbars and visual overflow. Pin them at the page level (inside <main> or <article>), never inside a constrained container.

Quick start (the "I just want it to work" path)

One tag wraps everything with sensible defaults:

reading-shell with defaults

This preview is designed for larger viewports. Open this page on a desktop to try the live, interactive demo.

Family contracts

These contracts apply uniformly to every primitive in the family. They are documented once here, not duplicated in each primitive section.

Reduced motion

Every transition (bar fill transform: scaleX, dot stroke dashoffset, spine width / label opacity / tick color-fade, bookmark pill enter / leave, minimap stripe color transitions, smooth-scroll on click / back-to-top) collapses to 0.01ms under prefers-reduced-motion: reduce via the global @media block in dist/wirekit.css. Values still update on scroll — only the interpolating transition is removed; the value itself jumps to current.

All primitives carry a @media print { display: none !important } rule scoped via marker classes (.wk-reading-progress, .wk-reading-spine, .wk-reading-minimap, .wk-reading-bookmark, .wk-reading-meta). Print-friendly for blog-to-PDF workflows — only the article body prints, not the reading chrome.

The target prop convention

Every primitive accepts a target prop that defaults to null. When null, the primitive resolves an internal default per-component:

  • reading-progress target=null → tracks viewport scroll
  • reading-spine, reading-bookmark, reading-meta, reading-minimap target=null → resolves to 'main, article' (first-match wins)

Pass an explicit CSS selector when your article container isn't <main> or <article> (e.g. target="#docs-body").

The wk-reading-* marker classes

Every primitive carries a wk-reading-{name} marker class on its root element. Use these for developer CSS overrides without editing the package source.

Primitives

Reading Progress

A viewport-pinned indicator that fills 0 → 100% as the reader scrolls. Two surfaces share one component: indicator="bar" (default — thin horizontal strip) and indicator="dot" (circular SVG pinned bottom- right). Both reuse the same Alpine state machine; only the rendered DOM differs.

The fill animation uses transform: scaleX (bar) / stroke-dasharray (dot) — both compositor-only properties, GPU-accelerated, no layout or paint cost.

Bar (default)
Dot indicator

Variants

Six canonical values + one runtime-resolved value:

Value Fill
primary (default) --color-wk-accent (brand emphasis)
neutral --color-wk-text-muted (quiet gray)
success / warning / danger semantic color tokens
info visual synonym of primary (matches alert/callout pattern)
auto currentColor fallback — for embedded contexts (iframes, browser extensions) where the bar should match surrounding text color

Every variant resolves through var(--reading-progress-fill, var(--color-wk-X)), so the --reading-progress-fill developer-side token override applies regardless of variant. Set the token at :root for a theme-wide override.

Reading Progress props

Prop Type Default Description
position string 'top' top or bottom viewport edge
height string 'md' sm (2px) / md (3px) / lg (5px)
variant string 'primary' See "Variants" above
intent string|null null Canonical name for the color axis, accepting the same values as variant. Falls back to variant when omitted, and wins over it when both are given.
showAfter int 0 Pixel scroll threshold before bar appears
target string|null null CSS selector or null for viewport scroll
indicator string 'bar' bar or dot
segments array|null null Fractional 0..1 chapter-marker positions
milestones bool false Fire wirekit:reading-progress:milestone event at 25/50/75/100%
boundary 'container'|<css-selector>|null null When set to 'container', swap position: fixedposition: sticky so the bar/dot stays inside the nearest positioned, scrollable ancestor. Pass a CSS selector string (e.g. "#article") to additionally assert at runtime that the matched ancestor exists — the Alpine init logs a console.warn when the selector matches no ancestor. Required for embedding inside modal bodies, sidebar panes, or preview frames. See Scoping a primitive to its parent.
scope string|null null Personalization scope

Reading Spine

Looking for canonical layout-wiring? The on-page-toc recipe shows the Stripe / Linear / Vercel / shadcn-style sidebar pattern end-to-end: reading-spine next to <main>, CSS variables for sticky offset, integration with a sticky header + breadcrumb, and a working preview.

A sidebar mini-TOC that auto-builds from page headings, tracks the reader's scroll position via IntersectionObserver, and expands on hover or focus.

Default spine

This preview is designed for larger viewports. Open this page on a desktop to try the live, interactive demo.

Filter slot — canonical worked example

Pass any input as the filter slot; two-way-bind via x-model="filter". Items hide when their text doesn't match (case-insensitive substring).

<x-wirekit::reading-spine>
    <x-slot:filter>
        <x-wirekit::input
            x-model="filter"
            placeholder="Filter sections..."
            size="sm"
        />
    </x-slot:filter>
</x-wirekit::reading-spine>

Reading Spine props

Prop Type Default Description
target string|null null Scroll container to scan; null falls back to main, article
levels string '2,3' Comma-separated heading levels to include
position string 'right' left or right viewport edge
expand string 'hover' hover / focus / always / always-md
offset string '6rem' Active-detection threshold
hideBelow string 'md' Tailwind responsive prefix (sm/md/lg/xl/none)
numbered bool false Hierarchical 1, 2.1, 2.2 labels
fillSections bool false Per-tick progress fill via CSS gradient
sectionEvents bool true Dispatch wirekit:reading-spine:section-changed
backToTop bool false Back-to-top pill at the spine bottom
boundary 'container'|null null When set to 'container', swap Tailwind fixedabsolute so the spine stays inside the nearest positioned ancestor instead of the viewport edge. See Scoping a primitive to its parent.
scope string|null null Personalization scope

Reading TOC

Horizontal sticky-strip TOC for marketing landing pages. Sibling to reading-spine — same data source (auto-builds from page headings via the same plugin pattern, IntersectionObserver tracks the active section), different rendered shape (a flat row of links across the top or bottom of the article container) and different responsive defaults (mobile-hidden by default).

When to use: marketing landing pages with 3-4 anchor sections (Hero, Features, Pricing, FAQ) where a vertical sidebar feels excessive. The strip pins to the top of the viewport on scroll, and the link for the currently-visible section highlights via aria-current="location".

When NOT to use: dense documentation pages with many headings — a horizontal strip overflows fast at viewport widths < 1024px. Reach for <x-wirekit::reading-spine> with hideBelow="none" instead, since its collapsed-tick mode works at narrow widths where a horizontal strip cannot.

Typical range is three or four anchored sections — a landing page rather than a reference page.

<x-wirekit::reading-toc offset="4rem" />

Works inside your own scroll region If your shell scrolls an inner container rather than the page — a fixed header with overflow-y: auto beneath it — the jump finds that container on its own. Nothing to configure.

The offset prop pushes the sticky position down by the developer's own fixed nav height (e.g. 4rem to clear a 64px header bar above the TOC). The same value is passed through to the IntersectionObserver rootMargin so active-section detection lines up with the visual position of the strip.

The reading family in one page

This preview is designed for larger viewports. Open this page on a desktop to try the live, interactive demo.

A11y / motion / print

  • Real <a href="#section-id"> links inside a <nav aria-label="Page sections"> landmark — keyboard-navigable via Tab + Enter; screen readers announce the strip as a Navigation landmark.
  • Active link carries aria-current="location" (matches spine pattern).
  • prefers-reduced-motion: reduce collapses the smooth-scroll on link click to instant (matches family convention).
  • Printed via @media print { display: none !important } — strip is screen-only chrome.

Reading TOC props

Prop Type Default Description
target string|null null Heading-host container; null resolves to main, article (family default)
levels string '2' Comma-separated heading levels — usually just '2' for landing pages, '2,3' for nested-section sites
position string 'top' top or bottom (sticky to viewport top or bottom)
offset string '0' CSS offset to clear developer-side fixed chrome (e.g. '4rem')
hideBelow string 'sm' Tailwind responsive hide (sm/md/lg/xl/none) — mobile-hidden by default
flush bool false Drop the inline padding so the link text runs flush to the viewport edge — for a TOC sitting under edge-to-edge page chrome. The hover background keeps its padding on the opposite edge.
scope string|null null Personalization scope

Reading Minimap

Every-item density overview of a scrollable container. Sibling primitive to reading-spine — same family contracts, different visual and interaction model. Two canonical use cases:

  1. Long-form article density-overview — paragraphs / headings as stripes; sits at the article's right edge. Complements (or replaces) reading-spine when paragraph-level density-overview is wanted over heading-level TOC.
  2. Sidebar navigation density-overview — nav links as stripes; sits at the sidebar's right edge. Designed for long sidebars where scanning for one specific item is friction.

Reading Minimap behavior

  • Renders each matched item as a 1-2px stripe at proportional vertical position (offsetTop / scrollHeight)
  • Translucent viewport-overlay rectangle tracks the host's visible region; rAF-throttled scroll updates
  • Click stripe → smooth-scroll target so the item is centered in the viewport (instant under prefers-reduced-motion: reduce)
  • Drag overlay → pan host scroll position proportionally
  • Hover stripe (non-touch) → tooltip with the item's label (default item.textContent, override via data-minimap-label="..." attribute)
  • aria-hidden="true" on the whole component — alternative view of nav already exposed to AT via the underlying primary nav
Rendered mode — literal scaled-down preview

This preview is designed for larger viewports. Open this page on a desktop to try the live, interactive demo.

Minimap stripes + viewport overlay tracking scroll

This preview is designed for larger viewports. Open this page on a desktop to try the live, interactive demo.

Block-style stripes — :itemStyle=block (content-texture, skeleton-style rectangles)

This preview is designed for larger viewports. Open this page on a desktop to try the live, interactive demo.

Custom-themed minimap — accent color, rounded blocks, bordered column

This preview is designed for larger viewports. Open this page on a desktop to try the live, interactive demo.

Reading Minimap props

Prop Type Default Description
target string|null null Scroll container; null auto-resolves nearest scrollable ancestor
itemSelector string 'a, h2, h3, [data-minimap-item]' CSS selector matching items to render as stripes
width string '60px' Column width
side string 'right' left or right edge of the host
draggable bool true Enable click + drag-pan on the viewport overlay
hideBelow string 'lg' Tailwind responsive hide (sm/md/lg/xl/none) — desktop-only by default
scope string|null null Personalization scope
mode string 'stripes' 'stripes' (default, abstract bars) or 'rendered' (literal scaled-down page preview)
itemStyle string 'line' Stripe-mode visual: 'line' (thin 2px stripes per item) or 'block' (skeleton-style rectangles whose height tracks each source item's natural height — content-texture view)
renderTarget string|null null CSS selector for the source to clone in rendered-mode (defaults to target)
hoverPreview bool false Extension E1 — magnified popover near cursor on minimap hover
showBookmarks bool true Extension E2 — render a marker line for a sibling <x-wirekit::reading-bookmark>
headingAnchors bool false Extension E3 — clickable mini-anchors on the outboard edge, one per h2/h3
headingLevels string '2,3' Extension E3 — CSV of heading levels to surface as anchors
autoFadeIdle bool true Extension E4 — fade after 3 s of inactivity; hover restores full opacity

Rendered mode

Set mode="rendered" to swap the abstract stripe column for a literal scaled-down clone of the article DOM inside a sandboxed iframe. Reads as a true page preview — you can see where headings, code blocks, images, and figures sit relative to one another rather than just "density of items".

<x-wirekit::reading-minimap
    render-target="article"
    mode="rendered"
    :hover-preview="true"
    :heading-anchors="true"
/>

How it works:

  • Lazy-init via IntersectionObserver — the iframe is only constructed when the minimap actually enters the viewport.
  • Source DOM is sanitized (every <script>, every on* handler, every javascript: / data: URI, every nested <iframe>, every inline <style>) before the srcdoc payload is written.
  • Same-origin stylesheets are injected into the iframe head via <link> so the clone inherits the host theme.
  • A MutationObserver on the source watches for content changes (Livewire morph, hot-reload edits) and rebuilds the iframe with a 250 ms debounce.
  • Above 5 000 source-DOM tags, rendered-mode silently falls back to stripe-mode to keep memory + first-paint cost bounded.

The iframe itself is fully decorative: aria-hidden="true", tabindex="-1", sandbox="allow-same-origin" (no allow-scripts). All interaction lives on the wrapping element — clicks translate to parent-window scrollTo(), hover triggers the optional preview popover.

Extensions

Four optional extensions layer on top of either rendering mode:

Extension Prop What it does
Hover preview :hover-preview="true" Magnified 25%-scale popover follows the cursor on minimap hover, anchored near the pointer + flipped to stay inside the viewport
Bookmark marker :show-bookmarks="true" (default on) Auto-detects a sibling <x-wirekit::reading-bookmark> via its data-reading-bookmark-key attribute and renders a horizontal marker at the saved offset; updates live via custom event + browser storage event for cross-tab consistency
Heading anchors :heading-anchors="true" Walks the source DOM for h2/h3 (configurable via headingLevels) and emits a clickable <a href="#id"> on the outboard edge for each one; density guard collapses overlapping labels (< 16 px gap), which surface again on minimap hover
Auto-fade idle :auto-fade-idle="true" (default on) Fades the minimap to --reading-minimap-idle-opacity after 3 s of pointermove/scroll/pointerdown inactivity; any activity restores full opacity, hover overrides instantly

All four extensions honor prefers-reduced-motion: reduce — fade transitions collapse to 0.01 ms so users with vestibular sensitivities see an instant state change instead of motion.

Decision tree — TOC vs Spine vs Minimap

Now that all three navigation primitives have been introduced, the quick pick-list:

Use case Pick
Marketing landing page, 3-4 flat sections reading-toc
Long-form blog post or docs page with nested headings reading-spine
Long article OR sidebar-nav density overview reading-minimap

The three primitives are intentionally orthogonal: reading-toc is horizontal + flat, reading-spine is vertical + hierarchical, reading-minimap is every-item density. Pick by the dominant shape of your content, not by the use case alone.

Reading Bookmark

Persists the reader's scroll position to localStorage and surfaces a "Resume reading?" pill on return-visit when conditions are met (previous-session dwell time ≥ minDwellSeconds, scroll moved past threshold * scrollHeight).

localStorage-only by design. Cross-device sync is explicitly out of scope — that's a backend feature, not a UX affordance. Developers needing cross-device persistence build their own server-side bookmark backed by Eloquent + a few endpoints; the WireKit primitive stays client-side simple.

<x-wirekit::reading-bookmark key="article-{{ $post->slug }}" />
Resume-reading pill triggered by a seeded prior visit

Reading Bookmark props

Prop Type Default Description
key string required localStorage key (must be unique per article)
target string|null null Scroll container; null falls back to main, article
threshold float 0.1 Fraction of scrollHeight before saving
promptOnReturn bool true Show resume pill on return-visit
minDwellSeconds int 30 Minimum dwell time before bookmark saves
previewMode bool false Sandbox-renderer escape hatch — disables localStorage IO
boundary 'container'|null null When set to 'container', swap Tailwind fixedabsolute so the resume-prompt pill stays inside the nearest positioned ancestor. See Scoping a primitive to its parent.
scope string|null null Personalization scope

Reading Meta

Time-to-read estimate. Computes word count from the target's textContent on mount, skipping nodes that don't read at prose pace (pre, code, figure, figcaption, img, picture, svg, [data-language]).

CJK-aware: when more than 40% of text is CJK ideographs (Chinese / Japanese kana / Korean Hangul), falls back to character-based estimation with a configurable cjkCharsPerMinute baseline (default 500) — Chinese / Japanese / Korean text has no spaces between logographic characters, so whitespace tokenization underestimates word count by an order of magnitude.

<x-wirekit::reading-meta :showRemaining="true" />

perParagraph mode (Medium-style inline annotations)

When :perParagraph="true", the component injects small <span class="wk-reading-meta-paragraph">3 min</span> annotations immediately before each <p> in the target with at least paragraphMinWords words (default 30). Annotations show estimated remaining-time FROM that paragraph onward, re-computed on scroll.

<x-wirekit::reading-meta :perParagraph="true" />

Annotations carry aria-hidden="true" — the canonical screen-reader text remains the total/remaining display. Developers fully restyle via the .wk-reading-meta-paragraph class.

Reading Meta props

Prop Type Default Description
target string|null null Container to scan; null falls back to main, article
wpm int 225 Words per minute (English baseline)
showRemaining bool false Show "X min remaining" beside total
perParagraph bool false Inject inline per-paragraph remaining-time annotations
totalLabel string 'min read' Total-estimate suffix
remainingLabel string 'min remaining' Remaining-estimate suffix
paragraphLabelTemplate string '{n} min' Template for per-paragraph annotation text ({n} substitutes minutes)
paragraphMinWords int 30 Minimum word count for a paragraph to receive an annotation
cjkCharsPerMinute int 500 CJK-fallback character-pace baseline
scope string|null null Personalization scope

Composition: <x-wirekit::reading-shell>

The 80% case: one tag wraps the article body and renders sensible defaults for progress + spine + bookmark. Density preset adjusts the feature set; per-primitive toggles override.

<x-wirekit::reading-shell :bookmark-key="'article-' . $slug">
    {{ $slot }}
</x-wirekit::reading-shell>

Density presets

Density Progress Spine Minimap Meta Spine expand
comfortable (default) md height on off off hover
compact sm height on off off always-md
minimal sm height off off off hover

Density is the starting-point shape. Per-primitive toggles override the preset's baseline:

{{-- density="minimal" hides spine by default; :spine="true" turns it back on --}}
<x-wirekit::reading-shell density="minimal" :spine="true" :minimap="true">
    {{ $slot }}
</x-wirekit::reading-shell>

Reading Shell props

Prop Type Default Description
bookmarkKey string|null null Required to enable bookmark
progress bool|null null Toggle (null = use density default)
spine bool|null null Toggle (null = use density default)
minimap bool|null null Toggle (null = use density default)
toc bool|null null Toggle (null = use density default; off in comfortable and compact, so opt in with :toc="true")
bookmark bool|null null Toggle (null = on if bookmarkKey set)
meta bool|null null Toggle (null = use density default; default density off)
density string 'comfortable' comfortable / compact / minimal
previewMode bool false Sandbox-renderer escape hatch
boundary 'container'|null null Pass-through to every composed primitive (reading-progress, reading-spine, reading-bookmark). Default null keeps the viewport-pinned shape; 'container' scopes every composed primitive to the nearest positioned ancestor. See Scoping a primitive to its parent.
scope string|null null Personalization scope

Power-user composition (skip the shell)

When toggles + density don't cover the customization need, compose primitives directly:

<x-wirekit::reading-progress variant="success" indicator="dot" :milestones="true" />
<x-wirekit::reading-spine :levels="'2,3,4'" :numbered="true" :backToTop="true">
    <x-slot:filter>
        <x-wirekit::input x-model="filter" placeholder="Filter..." size="sm" />
    </x-slot:filter>
</x-wirekit::reading-spine>
<x-wirekit::reading-minimap target="#article" item-selector="h2, h3" />
<x-wirekit::reading-bookmark key="article-{{ $slug }}" :minDwellSeconds="60" />
<x-wirekit::reading-meta :wpm="200" :showRemaining="true" :perParagraph="true" />

{{ $slot }}

The shell covers the common defaults case, and power-users opt out to the primitives entirely — that's the design intent.

Scoping a primitive to its parent — the boundary prop

reading-progress, reading-spine, and reading-bookmark accept an optional boundary prop. Three accepted shapes:

Value Behavior
null (default) Viewport-pinned. Every existing developer sees zero change.
'container' Scope to the nearest positioned ancestor (sticky / Tailwind absolute).
'<css-selector>' (any string) Same scoped shape as 'container', AND the Alpine init asserts an ancestor matching the selector exists — emits a console.warn when the selector matches none. Useful when the contained surface is deep inside a layout and you want a runtime sanity check.

Two requirements when using any non-null boundary:

  1. The parent must establish a positioning context (position: relative is the cheapest choice).
  2. For reading-progress, the parent must also be a scrolling container (overflow: auto or scroll) with a defined height — position: sticky falls back to static positioning in a non-scrolling parent, and the bar would disappear from view.
{{-- 1. The contained reading surface — relative + scrollable. --}}
<div class="relative max-h-[24rem] overflow-y-auto" style="position: relative;">
    {{-- 2. Inside boundary, the progress bar stays put. --}}
    <x-wirekit::reading-progress boundary="container" />

    <article>
        <h2>Article body</h2>
        <p>The progress bar lives inside this card and never escapes.</p>
    </article>
</div>

The same pass-through is wired on <x-wirekit::reading-shell> — when the shell carries boundary="container", every composed primitive (reading-progress, reading-spine, reading-bookmark) inherits it.

Reading-progress contained inside a card via boundary='container'

This preview is designed for larger viewports. Open this page on a desktop to try the live, interactive demo.

Mobile behavior

The reading family is responsive by design — the chrome adapts to the viewport so the article body always gets the full reading width on a phone:

  • Vertical spine hides below md. reading-spine defaults to hideBelow="md" (hidden md:block), so on screens narrower than md (48rem / 768px) the right-edge spine is not rendered at all. A vertical tick column is a pointer affordance — it has no place on a touch phone where there is no hover to expand it.

  • The article must reclaim that width on mobile. Because the spine is gone below md, the article must NOT reserve the spine gutter there. Wrap the article in class="wk-reading-content" — it reserves the inline-end gutter only at md+ (where the spine is visible) and uses the full width below md:

    <article class="wk-reading-content">
        <h2>...</h2>
        <p>...</p>
    </article>
    

    Override the gutter width per composition with --reading-content-spine-gutter (default 12rem) when the expanded spine is narrower or wider than the default. NEVER hardcode an inline max-width: calc(100% - 12rem) or padding-right: 12rem — an inline style can't @media, so the gutter stays applied below md and collapses the article to a single character per line on a phone.

  • The horizontal reading-toc strip stays visible on mobile. Unlike the vertical spine, the horizontal TOC strip is a mobile-appropriate affordance (it sits above the article and its links wrap/pan). Use it instead of the spine when the page genuinely needs an on-mobile table of contents (e.g. a documentation page with a breadcrumb).

Best practices

  1. Token override beats component-prop override. Override --reading-progress-fill at :root {} for theme-wide changes instead of passing variant to every instance.
  2. Single shell per page maximum. The shell registers global scroll handlers; two shells produce two sets of listeners.
  3. Bookmark keys must be unique per article. Use bookmarkKey="article-{$slug}" with a per-article slug.
  4. Don't mix shell + primitive ejection on the same page. Either use the shell OR compose primitives directly; not both. Two competing IntersectionObservers on the same target produce undefined active-section behavior.
  5. Prefer null target with internal fallback. Defaults absorb future framework changes (e.g. Laravel ships <article> instead of <main> as the content root).

Keyboard Interaction

The reading-* family is purely supplementary content / chrome — keyboard interaction is delegated to the underlying article HTML (headings remain <h2>/<h3>, bookmark prompt button is keyboard- focusable, spine entries are real <a href> links). Minimap is explicitly aria-hidden="true" and skipped from focus order — its purpose is visual aid; primary nav stays keyboard-driven.

Pitfalls

  • Two reading-progress components with milestones on one page fire duplicate analytics events at each threshold. Mount one, not two.
  • Bookmark key with whitespace breaks the URL fragment matching used internally. Use slugs ('docs-getting-started'), not titles ('Docs: Getting Started').
  • Minimap mounted without a relative parent positions itself against the viewport instead of the host. Wrap the host in a position: relative container.
  • Filter slot without x-model="filter" mounts an unconnected input — the spine's filter Alpine field never updates. Always bind via x-model="filter" exactly.

Design Tokens

The reading-* family ships its own family-prefixed CSS variables so the chrome can be themed independently of the surrounding palette. The complete token surface lives in the central Design Tokens reference page (under "Reading Component Tokens"); the family-level summary below covers the most common knobs.

Token Used for
--reading-progress-height-sm / --reading-progress-height-md / --reading-progress-height-lg Progress-bar thickness per size prop
--reading-progress-dot-size Milestone dot size on the progress bar
--reading-progress-fill Progress-bar fill color (defaults to --color-wk-accent)
--reading-spine-color-active / --reading-spine-color-idle Spine tick colors
--reading-spine-tick-height / --reading-spine-gap Spine tick height + gap
--reading-spine-padding-x / --reading-spine-padding-y Inner padding of the spine's <ol> — the gutter between the ticks and the panel's edges. padding-x defaults to 0.5rem (gutter from left + right aside edges). padding-y defaults to 0.875rem (top + bottom breathing room — slightly larger because the panel's vertical extent stacks multiple heading rows whose typographic height needs more daylight from the panel rim than the horizontal margins do).
--reading-spine-offset-top boundary="container" only — the spine pins this far (default 1rem) below the top of the scroll container instead of scrolling away with the article.
--reading-toc-link-max-width Per-link truncation width
--reading-toc-padding-x / --reading-toc-padding-y / --reading-toc-gap TOC link padding + gap
--reading-meta-color / --reading-meta-text-size Meta line color + size
--reading-minimap-stripe-height / --reading-minimap-stripe-gap Minimap stripe geometry
--reading-minimap-rendered-scale / --reading-minimap-rendered-opacity / --reading-minimap-rendered-radius / --reading-minimap-rendered-placeholder-bg Rendered-mode iframe-clone visual tuning
--reading-minimap-preview-scale / --reading-minimap-preview-size / --reading-minimap-preview-bg / --reading-minimap-preview-border / --reading-minimap-preview-shadow Extension E1 (hover preview popover)
--reading-minimap-bookmark-marker-color / --reading-minimap-bookmark-marker-height Extension E2 (bookmark marker line)
--reading-minimap-anchor-text-color / --reading-minimap-anchor-text-color-hover / --reading-minimap-anchor-font-size Extension E3 (heading anchors)
--reading-minimap-idle-opacity / --reading-minimap-idle-delay / --reading-minimap-idle-transition Extension E4 (auto-fade idle)

One worked override example:

/* Theme the entire family in one file: progress bar in success-green,
   spine ticks in muted neutral, minimap stripes also muted-neutral. */
:root {
    --reading-progress-fill: var(--color-wk-success);
    --reading-spine-color-active: var(--color-wk-success);
    --reading-minimap-viewport-bg: color-mix(in oklab, var(--color-wk-success) 15%, transparent);
}

See Also