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:
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.
Print stylesheet
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-progresstarget=null→ tracks viewport scrollreading-spine,reading-bookmark,reading-meta,reading-minimaptarget=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.
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: fixed → position: 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.
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 fixed → absolute 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.
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: reducecollapses 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:
- Long-form article density-overview — paragraphs / headings as
stripes; sits at the article's right edge. Complements (or
replaces)
reading-spinewhen paragraph-level density-overview is wanted over heading-level TOC. - 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 viadata-minimap-label="..."attribute) aria-hidden="true"on the whole component — alternative view of nav already exposed to AT via the underlying primary nav
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>, everyon*handler, everyjavascript:/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
MutationObserveron 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 }}" />
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 fixed → absolute 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:
- The parent must establish a positioning context (
position: relativeis the cheapest choice). - For
reading-progress, the parent must also be a scrolling container (overflow: autoorscroll) with a defined height —position: stickyfalls 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.
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-spinedefaults tohideBelow="md"(hidden md:block), so on screens narrower thanmd(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 inclass="wk-reading-content"— it reserves the inline-end gutter only atmd+ (where the spine is visible) and uses the full width belowmd:<article class="wk-reading-content"> <h2>...</h2> <p>...</p> </article>Override the gutter width per composition with
--reading-content-spine-gutter(default12rem) when the expanded spine is narrower or wider than the default. NEVER hardcode an inlinemax-width: calc(100% - 12rem)orpadding-right: 12rem— an inline style can't@media, so the gutter stays applied belowmdand collapses the article to a single character per line on a phone. -
The horizontal
reading-tocstrip 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
- Token override beats component-prop override. Override
--reading-progress-fillat:root {}for theme-wide changes instead of passingvariantto every instance. - Single shell per page maximum. The shell registers global scroll handlers; two shells produce two sets of listeners.
- Bookmark keys must be unique per article. Use
bookmarkKey="article-{$slug}"with a per-article slug. - 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.
- Prefer
nulltarget 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: relativecontainer. - Filter slot without
x-model="filter"mounts an unconnected input — the spine'sfilterAlpine field never updates. Always bind viax-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
- Long-form Article — canonical blog-post recipe
- Documentation Reader — heavier composition for technical docs
- Design Tokens — full token reference