Table of Contents Widget

Add a table of contents that builds itself from the headings already in your article - as a card at the top of the post, a minimal list, a Contents button that opens a panel on phones, or a side panel on wide screens. The section being read is highlighted as visitors scroll, and links land below your sticky header instead of under it.

Website Essentials Website component No trackers, no cookies Free · no ads

Customize your widget

Headings
CSS selector. Blank = article, else main, else .entry-content, else body.
One per line: Heading text | #anchor | level (2-6). Manual mode only.
Look
For example: On this page.
Behaviour
0-400. Headings land this far below the top.
Light or dark
Match page reads the background behind the component.

Live preview

Exactly what your visitors will see

Embed code

<div data-a2z-kit="table-of-contents"></div>
<script async src="https://a2z.tools/embed/kit.js" referrerpolicy="no-referrer"></script>

One small script from A2Z draws the component on your page inside its own Shadow DOM. It loads no social-network or tracking scripts and sets no cookies. How the A2Z kit works.

Works with

How it works

In automatic mode the component reads the h2 and h3 headings (or the levels you choose) inside your article, main element, .entry-content or body, skipping any inside nav, aside, footer, hidden elements, A2Z components and anything marked data-a2z-toc="off". A heading without an id receives one made from its text - lower case, letters and digits of any script kept, everything else turned into hyphens, with -2, -3 added for repeats - because a link needs something to point at; this is the one change made to your markup. Manual mode uses your own lines instead. Clicking an entry scrolls to the heading minus your header offset (also applied as scroll-margin-top on the heading, so a shared #link lands correctly too), updates the address bar with history.replaceState if you allow it, and moves keyboard focus to the heading with a temporary tabindex that is removed again when focus leaves. An IntersectionObserver marks the section currently at the top of the window with aria-current. It runs as a script on your page because a frame from another site cannot read your headings.

Method

  • Headings: h2-h3 by default (h2 only, or h2-h4) inside your selector, else article, else main, else .entry-content, else body; at most 100
  • Skipped: headings inside nav, aside, footer, [data-a2z-toc="off"], [hidden], [aria-hidden="true"] and A2Z components, and headings with no text
  • New id = NFKC lower-case text, apostrophes dropped, runs of non-letters/digits -> '-', trimmed, at most 64 characters, 'section' if empty, -2/-3... if already taken
  • Nesting follows the open headings above each item, so h2 then h4 nests one step; numbers read 1, 1.1, 1.2, 2
  • Jump: scrollTo(heading top + scrollY - offset), smooth unless reduced motion; scroll-margin-top = offset on every linked heading
  • Active entry = the last heading whose top has passed offset + 16 px, re-checked by an IntersectionObserver; marked aria-current="true"
  • Side layout only when the window is at least 1280 px wide and 300 px are free beside the content; otherwise the Contents button

Worked examples

Numbered list from mixed headings

Inputs: numbered on; headings h2 Soil, h3 Compost, h3 Testing, h2 Sowing, h3 Greens

Result: 1 Soil, 1.1 Compost, 1.2 Testing, 2 Sowing, 2.1 Greens

Each h3 is numbered within the h2 above it.

Ids made for headings that have none

Inputs: Headings: "What's new? (2026 edition!)", "Intro", "Intro"

Result: #whats-new-2026-edition, #intro, #intro-2

The apostrophe is dropped, punctuation runs become single hyphens, and the second Intro gets -2 so both links work.

A skipped heading level

Inputs: Headings h2, h4, h4, h3

Result: Numbers 1, 1.1, 1.2, 1.3

The h4s nest one step under the h2 rather than two, so the outline has no empty level.

Limitations

  • Headings added after the page has loaded (lazy-loaded sections) are not picked up until the next page view.
  • Pages that scroll inside an inner container rather than the window jump correctly only when that container is the window.
  • Manual entries pointing at ids that do not exist on the page are still listed but fall back to the browser's normal anchor behaviour.

Where publishers use it

  • A 4,000-word buying guide where readers want to skip straight to 'Best budget option' and the section they are in stays highlighted
  • A WordPress or Ghost blog with a 72 px sticky header, using the offset so headings are not hidden under it after a jump
  • A documentation page that uses the side panel on desktop monitors and the Contents button on phones
  • A landing page that is not built from headings, listing hand-picked sections with manual lines

Questions

Why is this a script on my page instead of an iframe?

An iframe from another website cannot read the headings of the page around it - browsers keep cross-origin frames apart for security - and it could not scroll your page either. The kit therefore draws the list on your page inside a Shadow DOM, which keeps its styles separate from yours.

Does it change my HTML?

Only where it has to: a heading without an id gets one made from its text, so the link has a target. Headings that already have ids keep them. If you set a header offset, scroll-margin-top is set on the linked headings, and a jumped-to heading carries tabindex="-1" only while it has focus.

How do I leave a heading out?

Put it, or a section around it, inside an element with data-a2z-toc="off". Headings in nav, aside and footer elements are skipped automatically, as are hidden ones.

My site has a sticky header - the headings end up underneath it.

Set the offset to your header's height in pixels. It is used for the scroll calculation and applied as scroll-margin-top, so links shared with a #section anchor also land below the header.

Does a table of contents help SEO?

It gives readers and search engines plain anchor links to each section, and Google sometimes shows such links as 'Jump to' links in results. Nobody can guarantee that; write clear headings and treat it as a usability feature first.

Cite or recommend this tool

If you reference this tool in an article, course or documentation, these formats are ready to copy. They are optional - nothing is added to your site unless you paste it.

A2Z Tools Table of Contents
https://a2z.tools/embed/table-of-contents
  • Reading Progress Bar

    Website Essentials Website component New

    A slim bar or small circle that fills as visitors read your article - measured through the article itself.

    Get code
  • Back to Top Button

    Website Essentials Website component New

    A scroll-to-top button that waits until readers scroll, with a progress ring and keyboard-friendly focus.

    Get code
  • Estimated Reading Time

    Website Essentials Website component New

    Shows "6 min read" on your post, counted from the page's own text at a researched 238 words per minute.

    Get code
  • FAQ Accordion

    Page Components Tool New

    Accessible FAQ accordion with search, expand-all, numbering and a copy-as-text button.

    Get code
  • Last Updated Date

    Website Essentials Website component New

    "Updated 3 October 2026" from your page's real modified date - meta tags or JSON-LD - never the load time.

    Get code
  • Listen to Article Button

    Website Essentials Website component New

    A Listen button that reads your article aloud with the browser's built-in voices - no audio files, no account.

    Get code

Preview