Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

svelte-toc

janosh1.3kMIT0.6.1TypeScript support: included

Sticky responsive table of contents component written in Svelte

svelte, toc, table of contents, component

readme

Logo
 Svelte ToC

Tests GitHub Pages pre-commit.ci status NPM version Open in StackBlitz REPL

Sticky responsive table of contents component. Live Demo

🔨   Installation

npm install --dev svelte-toc
<slot name="demo-nav" />

📙   Usage

<script>
  import Toc from 'svelte-toc'
</script>

<Toc />

<main>
  <h1>Page Title</h1>
  <h2>Section</h2>
  <h3>Subsection</h3>
  <h2>Next Section</h2>
  <h3 class="toc-exclude">Another Subsection</h3>
</main>

🔣   Props

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 user's 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 user's scroll position on the page).

  4. aside: HTMLElement | undefined = undefined

    The DOM node of the outer-most aside element. This is the element that gets the toc class. Cannot be passed in as a prop, only for external access!

  5. blurParams: BlurParams | undefined = { duration: 200 }

    Parameters to pass to transition:blur from svelte/transition. Set to null or { duration: 0 } to disable blurring.

  6. breakpoint: number = 1000

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

  7. desktop: boolean = true

    true if current window width > breakpoint else false.

  8. 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;
    }
  9. getHeadingIds = (node: HTMLHeadingElement): string => node.id

    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.

  10. 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).

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

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

  12. 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.

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

    CSS selector that matches 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(h2, h3, h4):not(.toc-exclude) excludes h5 and h6 headings as well as any node with a class of toc-exclude. For example <h2 class="toc-exclude">Section Title</h2> will not be listed.

  14. hide: boolean = false

    Whether to render 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 />.

  15. autoHide: boolean = true

    Whether to automatically hide the ToC when it's empty, i.e. when no headings match headingSelector. If true, ToC also automatically un-hides itself when re-querying for headings (e.g. on scroll) and finding some.

  16. keepActiveTocItemInView: boolean = true

    Whether to keep the active ToC item in view when scrolling the page. Only applies to long ToCs that are too high to fit on screen. If true, the ToC container will scroll itself to keep the active item in view and centered (if possible). Requires scrollend event browser support (71% as of 2024-01-22), with Safari the only major browser lacking support.

  17. minItems: number = 0

    Completely prevent the ToC from rendering if it doesn't find at least minItems matching headings on the page. The default of 0 means the ToC will always render, even if it's empty.

  18. nav: HTMLElement | undefined = undefined

    The DOM node of the nav element. Cannot be passed in as a prop, only for external access!

  19. open: boolean = false

    Whether the ToC is currently in an open state on mobile screens. Can be used to externally control the open state through 2-way binding. This value is ignored on desktop.

  20. 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.

  21. 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, especially if you have a lot of elements on the page that are on a separate branch of the DOM tree from the headings you want to list.

  22. reactToKeys: string[] = [`ArrowDown`, `ArrowUp`, ` `, `Enter`, `Escape`, `Tab`]

    Which keyboard events to listen for. The default set of keys closes the ToC on Escape and Tab out, navigates the ToC list with ArrowDown, ArrowUp, and scrolls to the active ToC item on Space, and Enter. Set reactToKeys = false or [] to disable keyboard support entirely. Remove individual keys from the array to disable specific behaviors.

  23. scrollBehavior: 'auto' | 'smooth' = `smooth`

    Whether to scroll the page smoothly or instantly when clicking on a ToC item. Set to 'auto' to use the browser's default behavior.

  24. title: string = `On this page`

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

  25. titleTag: string = `h2`

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

  26. tocItems: HTMLLIElement[] = []

    Array of rendered Toc list items DOM nodes. Essentially the result of document.querySelectorAll(headingSelector). Can be useful for binding.

  27. warnOnEmpty: boolean = true

    Whether to issue a console warning if the ToC is empty.

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;
}

🎰   Slots

Toc.svelte has 3 named slots:

  • slot="toc-item" to customize how individual headings are rendered inside the ToC. It has access to the DOM node it represents via let:heading as well as the list index let:idx (counting from 0) at which it appears in the ToC.

    <Toc>
      <span let:idx let:heading slot="toc-item">
        {idx + 1}. {heading.innerText}
      </span>
    </Toc>
  • slot="title": Title shown above the list of ToC entries. Props title and titleTag have no effect when filling this slot.

  • slot="open-toc-icon": Icon shown on mobile screens which opens the ToC on clicks.

✨   Styling

The HTML structure of this component is

<aside>
  <button>open/close (only present on mobile)</button>
  <nav>
    <h2>{title}</h2>
    <ol>
      <li>{heading1}</li>
      <li>{heading2}</li>
      ...
    </ol>
  </nav>
</aside>

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

  • aside.toc
    • z-index: var(--toc-z-index, 1): Applies on both mobile and desktop.
  • aside.toc > nav
    • overflow: var(--toc-overflow, auto)
    • min-width: var(--toc-min-width)
    • max-width: var(--toc-desktop-max-width)
    • width: var(--toc-width)
    • max-height: var(--toc-max-height, 90vh): Height beyond which ToC will use scrolling instead of growing vertically.
    • padding: var(--toc-padding, 1em 1em 0)
    • font-size: var(--toc-font-size)
  • aside.toc > nav > ol > .toc-title
    • padding: var(--toc-title-padding)
    • margin: var(--toc-title-margin)
  • aside.toc > nav > ol
    • list-style: var(--toc-ol-list-style, none)
    • padding: var(--toc-ol-padding, 0)
    • margin: var(--toc-ol-margin)
  • aside.toc > nav > ol > li
    • border-radius: var(--toc-li-border-radius)
    • padding: var(--toc-li-padding, 2pt 4pt)
    • margin: var(--toc-li-margin)
    • border: var(--toc-li-border)
    • color: var(--toc-li-color)
  • aside.toc > nav > ol > li:hover
    • color: var(--toc-li-hover-color, cornflowerblue): Text color of hovered headings.
    • background: var(--toc-li-hover-bg)
  • aside.toc > nav > ol > li.active
    • color: var(--toc-active-color, white): Text color of the currently active heading (the one nearest but above top side of current viewport scroll position).
    • background: var(--toc-active-bg, cornflowerblue)
    • font-weight: var(--toc-active-font-weight)
    • border: var(--toc-active-border)
    • border-width: var(--toc-active-border-width): Allows setting top, right, bottom, left border widths separately.
    • border-radius: var(--toc-active-border-radius, 2pt)
  • aside.toc > button
    • color: var(--toc-mobile-btn-color, black): Menu icon color of button used as ToC opener on mobile.
    • background: var(--toc-mobile-btn-bg, rgba(255, 255, 255, 0.2)): Background of padding area around the menu icon button.
    • padding: var(--toc-mobile-btn-padding, 2pt 3pt)
    • border-radius: var(--toc-mobile-btn-border-radius, 4pt)
  • aside.toc.mobile
    • bottom: var(--toc-mobile-bottom, 1em)
    • right: var(--toc-mobile-right, 1em)
  • aside.toc.mobile > nav
    • width: var(--toc-mobile-width, 18em)
    • background: var(--toc-mobile-bg, white): Background color of the nav element hovering in the lower-left screen corner when the ToC was opened on mobile screens.
    • box-shadow: var(--toc-mobile-shadow)
    • border: var(--toc-mobile-border)
  • aside.toc.desktop
    • margin: var(--toc-desktop-aside-margin): Margin of the outer-most aside.toc element on desktops.
  • aside.toc.desktop > nav
    • margin: var(--toc-desktop-nav-margin)
    • top: var(--toc-desktop-sticky-top, 2em): How far below the screen's top edge the ToC starts being sticky.
    • background: var(--toc-desktop-bg)

Example:

<Toc
  --toc-desktop-aside-margin="10em 0 0 0"
  --toc-desktop-sticky-top="3em"
  --toc-desktop-width="15em"
/>

🧪   Coverage

Statements Branches Lines
Statements Branches Lines

🆕   Changelog

View the changelog.

🙏   Contributing

Here are some steps to get you started if you'd like to contribute to this project!

changelog

Changelog

All notable changes to this project will be documented in this file. Dates are displayed in UTC.

v0.6.1

18 May 2025

  • add many more style props to Toc component 9cf5697

v0.6.0

13 April 2025

  • Svelte 5 Migration #61

v0.5.9

12 June 2024

  • Ignore spurious scrollend events on page load before any actual scrolling in Chrome #58

v0.5.8

21 March 2024

  • Add reactToKeys prop to Toc component and on_keydown handler to enable navigating ToC with keyboard #55
  • When opening ToC on mobile, ensure active ToC item is scrolled into view #54

v0.5.7

22 January 2024

  • Replace hacky window.setTimeout(50) callback with scrollend event to keepActiveTocItemInView #53
  • package.json add "types": "./dist/index.d.ts" and default --toc-overflow to auto #49
  • expose Toc aside and nav HTMLElements for external access fc8806d

v0.5.6

12 September 2023

  • Add prop blurParams: BlurParams | null = { duration: 200 } #47
  • Copy buttons #43

v0.5.5

20 April 2023

  • DRY GitHub Actions #40
  • fix svelte a11y warning about <li> tabindex and role aa1cd50

v0.5.4

16 March 2023

  • Fix new timeout callback for keepActiveTocItemInView=true breaking page scrolling #39

v0.5.3

12 March 2023

  • Fix ToC scroll abort #37
  • Add var(--toc-overflow, auto scroll) #34
  • add src/routes/(demos)/left-border-active-li/+page.md powered by mdsvexamples 6be66f0
  • tweak readme prop docs 84c1854
  • document new CSS variables in readme 5df7767
  • add test 'subheadings are indented' 06da853
  • add var(--toc-ol-list-style, none) and var(--toc-ol-padding, 0) ade5425

v0.5.2

12 January 2023

  • add auto changelog d7eaeea
  • add contributing.md a313e7b
  • add many new CSS variables in Toc.svelte badbe2f
  • add coverage badges to readme 8a24e2b
  • add vite alias $root to clean up package.json, readme|contributing|changelog.md imports 76427ee

v0.5.1

20 December 2022

  • use margin instead transform: translateX for indented ToC items 49d43d7
  • pnpm add -D @vitest/coverage-c8 bab57dc

v0.5.0

3 December 2022

  • Breaking: hide ToC if empty #30
  • Deploy docs to GitHub Pages #29

v0.4.1

6 November 2022

  • Only trigger keyup event handler on enter/space keys #28
  • yarn to pnpm #27
  • use code fences in readme to document prop, types and defaults b7f6f49
  • test CSS variables in readme are in sync with actual component ecf994b
  • test that readme documents no non-existent props d08eb59
  • add test 'ToC lists expected headings' b18b003
  • improve readme doc on CSS class toc-exclude in default heading selector 9e1476a

v0.4.0

13 September 2022

  • Better readme test #24
  • Fix ToC preventing page scrolling beyond active heading when zoomed into page #23

v0.3.2

11 September 2022

  • Add prop titleTag allowing to change HTML tag used for ToC title #21

v0.3.1

10 September 2022

  • fix: exclude header of table itself by class toc-exclude #20

v0.3.0

10 September 2022

  • Mutation observer #19
  • Fix cases where node.offsetTop is returning 0 #16
  • add playwright testing in tests/toc.test.ts a52aa29
  • mv .github/workflows/{publish,test}.yml and have it run yarn test in CI 42a232d
  • update deps and address sveltekit breaking changes e023246

v0.2.12

23 August 2022

  • Export headings and desktop and add hide prop #14

v0.2.11

18 August 2022

v0.2.10

17 July 2022

  • [pre-commit.ci] pre-commit autoupdate #13
  • [pre-commit.ci] pre-commit autoupdate #11
  • update deps dd64696
  • replace scrollIntoViewIfNeeded() with scrollIntoView({ block: `nearest` }) 3466e0f

v0.2.9

2 April 2022

  • fix page.subscribe(requery_headings) causing 'Function called outside component initialization' f8ad29f

v0.2.8

23 March 2022

  • Fix afterNavigate() runtime error #10

v0.2.7

13 March 2022

  • Exclude headings with class .toc-exclude #9
  • replace onClickOutside action with svelte:window click listener da0cd75

v0.2.6

19 February 2022

  • fix scroll heading into view on ToC click f0b6f5f

v0.2.5

14 February 2022

  • add css vars --toc-active-bg and --toc-active-font-weight a2ad8ef
  • fix scroll heading into view on ToC click 983596b

v0.2.4

13 February 2022

  • much simpler active heading logic inspired by sveltekit docs ToC 1bab517
  • add .github/workflows/publish.yml, update readme mention afterNavigate lifecycle hook, use mdsvex v0.10.5 TS globals b3feb92

v0.2.3

22 January 2022

v0.2.2

10 January 2022

  • add bool prop keepActiveTocItemInView f3e1dea

v0.2.1

7 January 2022

  • change default ToC title: Contents -> On this page, rename heading, bump node 16.1 to 17.3, add types of error + layout pages d52329f
  • drop custom Heading type, use HTMLHeadingElement directly (-15 LoC), add package default export d012ab7
  • [pre-commit.ci] pre-commit autoupdate bf6fa1a

v0.2.0

31 December 2021

  • fix erratic highlighting of active heading near page bottom + keep active heading in ToC scrolled into view + some new CSS variables (closes #2) #2

v0.1.11

30 December 2021

  • make blur transition local to not show on unmount due to page navigation (closes #3) #3
  • prettier drop svelteBracketNewLine, sort imports 69922f8

v0.1.10

13 November 2021

v0.1.9

20 October 2021

  • add pre-commit hooks dfc78ab
  • update deps, seems to fix janosh/svelte-bricks#1 414628d
  • use rehype-autolink-headings test to not link <h1> 56ba0cb

v0.1.8

17 July 2021

v0.1.7

12 July 2021

v0.1.6

9 July 2021

  • convert package to typescript 1438b75
  • git rm --cache auto-generated src/docs.svx fe4abdb
  • update URL hash on ToC clicks d460c76

v0.1.5

22 June 2021

  • republish to fix default export 0980c3f

v0.1.4

22 June 2021

  • convert file structure from yarn workspaces to svelte-kit package 647fd32

v0.1.3

19 June 2021

  • Toc.svelte add props title, openButtonLabel, breakpoint, flashClickedHeadingsFor a12e071

v0.1.2

16 June 2021

v0.1.1

2 June 2021

  • animate GitHubCorner arm-wave on hover a13c6b2
  • site fix wide table, add GitHubCorner.svelte 523d100
  • Toc.svelte set slugs of clicked headings as url hash 1ab9791

v0.1.0

19 May 2021