Svelte ToC
 Svelte ToC

Tests NPM version Docs status Open in StackBlitz

Sticky responsive table of contents component. Live demo


npm add -D svelte-toc
pnpm add -D svelte-toc


  import Toc from 'svelte-toc'

<Toc />

  <h1>Top Heading</h1>


Full list of props and bindable variables for this component (all of them optional):

  1. activeHeading: HTMLHeadingElement | null = null

    The DOM node of the currently active (highlighted) heading (based on the users scroll position on the page).

  2. activeHeadingScrollOffset: number = 100

    Distance in pixels to top edge of screen at which a heading jumps from inactive to active. Increase this value if you have a header that makes headings disappear earlier than the viewport’s top edge.

  3. activeTocLi: HTMLLIElement | null = null

    The DOM node of the currently active (highlighted) ToC item (based on the users scroll position on the page).

  4. breakpoint: number = 1000

    At what screen width in pixels to break from mobile to desktop styles.

  5. desktop: boolean = true

    true if current window width > breakpoint else false.

  6. flashClickedHeadingsFor: number = 1500

    How long (in milliseconds) a heading clicked in the ToC should receive a class of .toc-clicked in the main document. This can be used to help users immediately spot the heading they clicked on after the ToC scrolled it into view. Flash duration is in milliseconds. Set to 0 to disable this behavior. Style .toc-clicked however you like, though less is usually more. For example, the demo site uses

    :is(h2, h3, h4) {
      transition: 0.3s;
    .toc-clicked {
      color: cornflowerblue;
  7. getHeadingIds = (node: HTMLHeadingElement): string =>

    Function that receives each DOM node matching headingSelector and returns the string to set the URL hash to when clicking the associated ToC entry. Set to null to prevent updating the URL hash on ToC clicks if e.g. your headings don’t have IDs.

  8. getHeadingLevels = (node: HTMLHeadingElement): number =>
      Number(node.nodeName[1]) // get the number from H1, H2, ...

    Function that receives each DOM node matching headingSelector and returns an integer from 1 to 6 for the ToC depth (determines indentation and font-size).

  9. getHeadingTitles = (node: HTMLHeadingElement): string =>
      node.textContent ?? ``

    Function that receives each DOM node matching headingSelector and returns the string to display in the TOC.

  10. headings: HTMLHeadingElement[] = []

    Array of DOM heading nodes currently listed and tracked by the ToC. Is bindable but mostly meant for reading, not writing. Deciding which headings to list should be left to the ToC and controlled via headingSelector.

  11. headingSelector: string = `:is(h1, h2, h3, h4):not(.toc-exclude)`

    CSS selector string that should return all headings to list in the ToC. You can try out selectors in the dev console of your live page to make sure they return what you want by passing it into [...document.querySelectorAll(headingSelector)]. The default selector :is(h1, h2, h3, h4):not(.toc-exclude) excludes h5 and h6 headings as well as any node with a class of toc-exclude. For example <h1 class="toc-exclude">Page Title</h1> will not be listed.

  12. hide: boolean = false

    Whether to render or hide the ToC. The reason you would use this and not wrap the component as a whole with Svelte’s {#if} block is so that the script part of this component can still operate and keep track of the headings on the page, allowing conditional rendering based on the number or kinds of headings present (see PR#14). To access the headings <Toc> is currently tracking, use <Toc bind:headings={myHeadings} />.

  13. keepActiveTocItemInView: boolean = true

    Whether to scroll the ToC along with the page.

  14. open: boolean = false

    Whether the ToC is currently in an open state on mobile screens. This value is ignored on desktops.

  15. openButtonLabel: string = `Open table of contents`

    What to use as ARIA label for the button shown on mobile screens to open the ToC. Not used on desktop screens.

  16. pageBody: string | HTMLElement = `body`

    Which DOM node to use as the MutationObserver root node. This is usually the page’s <main> tag or <body> element. All headings to list in the ToC should be children of this root node. Use the closest parent node containing all headings for efficiency.

  17. title: string = `On this page`

    ToC title to display above the list of headings. Set title='' to hide.

  18. titleTag: string = `h2`

    Change the HTML tag to be used for the ToC title. For example, to get <strong>{title}</strong>, set titleTag='strong'

  19. tocItems: HTMLLIElement[] = []

    Array of rendered Toc list items DOM nodes. Essentially the result of passing aside.toc > nav > ul > li to document.querySelectorAll().

To control how far from the viewport top headings come to rest when scrolled into view from clicking on them in the ToC, use

/* replace next line with appropriate CSS selector for all your headings */
:where(h1, h2, h3, h4) {
  scroll-margin-top: 50px;


Toc.svelte has 2 named slots:


The HTML structure of this component is

  <button>open/close (only present on mobile)</button>

Toc.svelte offers the following CSS variables which can be passed in directly as props:


  --toc-desktop-aside-margin="10em 0 0 0"

Want to contribute?

To submit a PR, clone the repo, install dependencies and start the dev server to try out your changes.

git clone
cd svelte-toc
pnpm install
pnpm dev

Test Page Navigation