svelte-reading-toc Svelte Themes

Svelte Reading Toc

An experimental sticky, animated table of contents for Svelte articles and docs.

Svelte Reading TOC

A sticky, animated table of contents for Svelte articles and docs. It renders heading links, tracks the active section while scrolling, and shows a vertical reading progress bar.

By default, the component scans your rendered content for headings. You can also pass heading metadata from your own Markdown, MDsveX, CMS, or content pipeline.

Install

bun add @smit4k/svelte-reading-toc
npm install @smit4k/svelte-reading-toc

Usage

For plain rendered HTML, point the component at the article content:

<script lang="ts">
    import { ReadingTableOfContents } from '@smit4k/svelte-reading-toc';
</script>

<div class="post-shell">
    <div class="toc-slot">
        <ReadingTableOfContents contentSelector=".content" />
    </div>

    <article class="content">
        <h2>Intro</h2>
        <p>...</p>
        <h2>Setup</h2>
        <p>...</p>
        <h3>Details</h3>
        <p>...</p>
    </article>
</div>

The component scans h2, h3, and h4 by default. If a heading does not have an id, one is generated so the sidebar links work.

If you already collect headings while rendering Markdown, pass them explicitly. Provided headings, including an empty array, are used instead of scanning the DOM:

<script lang="ts">
    import { ReadingTableOfContents, type TocHeading } from '@smit4k/svelte-reading-toc';

    const headings: TocHeading[] = [
        { id: 'intro', text: 'Intro', level: 2 },
        { id: 'setup', text: 'Setup', level: 2 },
        { id: 'details', text: 'Details', level: 3 }
    ];
</script>

<div class="post-shell">
    <div class="toc-slot">
        <ReadingTableOfContents {headings} contentSelector=".content" />
    </div>

    <article class="content">
        <h2 id="intro">Intro</h2>
        <p>...</p>
        <h2 id="setup">Setup</h2>
        <p>...</p>
        <h3 id="details">Details</h3>
        <p>...</p>
    </article>
</div>

Props

Prop Type Default Description
headings TocHeading[] undefined Optional heading metadata. If provided, DOM scanning is skipped.
contentSelector string 'article' Element used to calculate reading progress.
headingSelector string 'h2, h3, h4' Headings to scan when headings is not provided.
generateIds boolean true Add IDs to scanned headings that do not already have one.
idPrefix string 'heading' Fallback prefix when a scanned heading has no usable text.
minHeadings number 2 Hide the TOC until at least this many headings exist.
hideBelow number 900 Hide the TOC below this viewport width in pixels.
ariaLabel string 'Table of contents' Accessible label for the <aside>.
class string '' Extra class names for the root element.
export type TocHeading = {
    id: string;
    text: string;
    level: number;
};

Styling

The component ships with neutral defaults and exposes CSS variables for theming.

.toc-slot {
    --toc-width: 14rem;
    --toc-top: 1rem;
    --toc-text: #898989;
    --toc-text-hover: #d6d6d6;
    --toc-text-active: #ffffff;
    --toc-active-bg: rgba(255, 255, 255, 0.1);
    --toc-hover-bg: rgba(255, 255, 255, 0.04);
    --toc-progress-bg: #242424;
    --toc-progress-fill: #ffffff;
}

For a left sidebar that does not shift article content:

.post-shell {
    position: relative;
    max-width: 65ch;
    margin: 0 auto;
    padding: 0 1rem;
}

.toc-slot {
    position: absolute;
    top: 0;
    right: calc(100% + 1.25rem);
    bottom: 0;
    width: 14rem;
}

Markdown Integration

You can let the component scan rendered Markdown output:

<ReadingTableOfContents contentSelector=".content" />

<article class="content">
    {@html htmlFromMarkdown}
</article>

Or generate headings wherever you already turn Markdown into HTML. In explicit mode, every heading object must have an id matching the rendered heading:

<h2 id="setup">Setup</h2>
const headings = [{ id: 'setup', text: 'Setup', level: 2 }];

Development

bun install
bun run check
bun run package

Top categories

Loading Svelte Themes