Sticky table of contents in WordPress: keep it in the sidebar while readers scroll
Last updated: 28 October 2026
On a long post, a table of contents that stays next to the text lets readers jump around without scrolling back to the top. WordPress has no table of contents block of its own, and most table of contents plugins put the list inside the post, where it cannot stay in view. This guide covers where the list has to live, which plugins and blocks can put it in a sidebar, the CSS that keeps it there, and the three things that usually go wrong: other widgets under it, a list taller than the window, and links that land under a fixed header.
We make a sticky widget plugin, Quicksloth Sticky Widget, so we are not neutral; most of what follows needs no plugin. The CSS behaviour was checked on 28 October 2026 in a small test page in Chromium 153, one case at a time, and so was our plugin's script (version 1.0.0, as downloaded from WordPress.org). What we say about the table of contents plugins comes from their pages and their code on WordPress.org, read the same day; we did not install them on a site. We did not repeat the test page in Firefox or Safari.
Short version
- Put the table of contents in the sidebar (a widget, or the side column of a block theme template), not in the post: a sticky element never leaves its parent box.
- Make it the last item in the sidebar, then add
position: sticky; top: 20px;to it. - If the list can be taller than the window, add
max-height: calc(100vh - 40px); overflow-y: auto;so it scrolls inside itself. - With a fixed header, give the headings
scroll-margin-topso a click does not hide them under the header.
1. The list has to be in the sidebar, not in the post
A sticky element stays inside its parent: it is "stuck" only until it meets the opposite edge of its containing block (MDN, position). Table of contents plugins usually insert the list into the post content, before the first heading or after the first paragraph. Made sticky there, it stays inside the text column: in our test page a list inside the post, with position: sticky; top: 20px, did stay at the top of the window, but on top of the paragraphs it was supposed to help with.
So the list has to sit in a box that runs next to the text: the widget area of a classic theme, or the narrow column of a block theme template. Most plugins below offer a widget for that; with a sidebar widget you will often switch off automatic insertion in the post, or the list appears twice.
2. Which table of contents plugins can go in a sidebar
Some of the most used options on WordPress.org, with the figures from its plugin directory on 28 October 2026. "Own sticky option" means a setting in the free plugin that keeps the list in view.
| Plugin (version) | Active installs | In a sidebar | Own sticky option |
|---|---|---|---|
| Easy Table of Contents (2.0.89) | 600,000+ | Widgets Table of Contents and Sticky Sidebar Table of Contents | Yes, two (below) |
| Table of Contents Plus (202609.1) | 200,000+ | The TOC+ widget | None found in its front-end code |
| LuckyWP Table of Contents (2.1.14) | 90,000+ | Widget Table of Contents | None found in its front-end code |
| Joli Table Of Contents (3.0.4) | 7,000+ | Block and shortcode | "Sticky Sidebar TOC" is listed among its Pro features |
| SimpleTOC (7.4.1) | 10,000+ | Block | No: its page says the highlighting "does not make the table of contents sticky" |
| Rank Math SEO (1.0.280) | 4,000,000+ | Block Table of Contents by Rank Math, made for the post | No |
| Yoast SEO (28.8) | 10,000,000+ | Its Table of contents block is marked as a Premium block in the free plugin | No |
WordPress itself has no table of contents block. There is none among the core blocks of WordPress 7.1.3 or of the current development version. The Gutenberg plugin (24.2.0) includes a Table of Contents block, still marked as experimental, so it appears only on sites that run the Gutenberg plugin. Block collections such as Kadence Blocks, Spectra and Ultimate Blocks also have a table of contents block.
The Rank Math block and the Gutenberg block save the list of headings inside the block (a headings attribute in their block.json), built in the editor from the post you are writing; that is why they belong in the post. We have not tested them in a template or widget area, so check that the list shows the current post's headings before you rely on it there.
3. Easy Table of Contents: two ways to stick, both done with a script
From the code of version 2.0.89, the free plugin offers:
- The Sticky Sidebar Table of Contents widget. It sticks by itself, through a script (the jQuery plugin Sticky-kit) rather than CSS; its Fixed Top Position field sets the gap from the top (30 px by default). In windows 768 px wide or narrower, and on phones and tablets recognised from the browser, it does not stick and shows the list as an overlay that opens with a button instead.
- The Table of Contents widget with Affix or pin the widget. This checkbox appears in the widget only after you fill in Widget Affix Selector under Settings → Table of Contents → Advanced with your theme's sidebar class or ID (for example
.widget-areaor#sidebar). The plugin warns: "If you choose to affix the widget, do not add any other widgets on the sidebar."
The Sticky TOC tab of the same settings page is something else: a table of contents that floats at the side of the window and opens with a button, not a sidebar widget. If either widget option does not stay in view on your theme, the CSS below works on Easy Table of Contents' plain widget as on any other.
4. Make the sidebar widget sticky with CSS
Find the widget's ID: right-click the list on the page, choose Inspect, and look for the box around it with an ID such as ezw_tco-2 (Easy Table of Contents), toc-widget-2 (Table of Contents Plus), lpwtoc_widget-2 (LuckyWP) or block-5 (a block in a widget area); the number changes from site to site. Put the rule on that box, not on the list inside it, and add it under Appearance → Customize → Additional CSS (classic themes):
#toc-widget-2 {
position: sticky;
top: 20px;
}
Then move the widget to the end of the sidebar. In our test page, a sticky list as the last widget stayed 20 px from the top and stopped where the sidebar ended, 20 px above the footer. As the first widget, with two other widgets below it, it also stayed at 20 px, and the widget under it slid up behind it: 156 px of that widget were covered after 500 px of scrolling, because a sticky element is drawn above ordinary boxes. That is the same advice Easy Table of Contents gives for its pinned widget.
If nothing happens at all, the theme is breaking one of the conditions CSS sticky needs: an overflow rule on a parent, a sidebar no taller than its widgets, or a missing top. Our page on sticky sidebar not working in WordPress shows how to find each one in DevTools and the fix.
5. A list taller than the window
A post with many headings gives a long list. In our test page, a sticky list of 40 links (1,108 px, in an 800 px window) stayed at 20 px from the top, and its last links stayed below the bottom edge of the window. Give the list a maximum height and let it scroll inside itself:
#toc-widget-2 {
position: sticky;
top: 20px;
max-height: calc(100vh - 40px);
overflow-y: auto;
}
With these two lines the same list still stuck at 20 px, ended 20 px above the bottom of the window, and scrolled inside its own box down to the last link. The overflow here is on the sticky element itself, which is fine; it is overflow on a parent that stops sticky from working. If you also want fewer entries, most plugins let you choose which heading levels to include: listing only the main headings often makes the list short enough.
6. Links that land under a fixed header
With a fixed header, a click on a link in the list scrolls the heading to the very top of the window, behind the header. In our test page, with an 80 px fixed header, the heading of "Section 5" ended at 0 px, under the header. Adding scroll-margin-top to the headings fixed it: the heading stopped at 100 px, 20 px below the header.
.entry-content h2,
.entry-content h3 {
scroll-margin-top: 100px; /* 80 px header + 20 px gap */
}
Use the class of your theme's post content (.entry-content in many classic themes, .wp-block-post-content in block themes). The sticky list needs the same gap: top: 100px instead of 20px. Plugins with their own smooth scrolling may move the page themselves; Easy Table of Contents has Smooth Scroll Offset (30 px by default) and Mobile Smooth Scroll Offset under its Advanced settings for that; they apply to the jump to a heading only when its Smooth Scroll option (General settings) is on.
7. Block themes: a Group with the Sticky setting
The table of contents blocks have no sticky setting of their own (the Gutenberg block and the Rank Math block declare no position support in their block.json). WordPress offers Position → Sticky only on the Group block, and only on themes that turn it on: in our tests with WordPress 7.1.3 it worked on Twenty Twenty-Five, Astra and Kadence, also in the narrow column of a Columns block, and was missing on GeneratePress, OceanWP and Twenty Twenty-One. To use it, put the list inside a Group, in the side column, and set the Group to Sticky. The limits are the ones in our guide to making a widget sticky: the Group sticks at the very top of the window with no field for a gap (so a fixed header covers it), and it moves on only when the end of its column reaches it. A long list in a Group also needs the max-height of section 5, added as custom CSS. A Group set to Sticky inside the post is still inside the text column (section 1).
When CSS is not enough
Sometimes the theme's overflow is needed, the sidebar cannot be stretched, or you want the list to stop before the footer rather than at the end of its column. Then a script has to measure the page. We ran the script of Quicksloth Sticky Widget 1.0.0 on the same test page, with its default settings:
- the list as the last widget stuck 10 px from the top (the default Margin top) with CSS sticky, and stopped above the footer;
- with
overflow-x: hiddenon a page wrapper, where CSS sticky fails, it switched to its fixed-position fallback, stayed 10 px from the top and stopped right at the footer; - with an 80 px fixed header and Margin top set to 100, it stayed at 100 px;
- the 40-link list taller than the window was left alone, by design: pinned, it would hide its own end. With the
max-heightandoverflow-ylines from section 5 it fitted the window and stuck.
In a classic theme you tick Sticky (stays in view while scrolling) in the table of contents widget under Appearance → Widgets; for a block, turn on Sticky in its Sticky panel; any other element can be listed by ID or class under Custom sticky elements. The plugin is free in the WordPress.org Plugin Directory. We have not tested it on a site with each of the table of contents plugins above.
Quick check list
| What you see | Why | Fix |
|---|---|---|
| The list sticks on top of the text | It is inside the post content | Use the plugin's sidebar widget or block, and turn off insertion in the post |
| Other widgets slide under the list | The list is not the last widget | Move it to the end of the sidebar |
| The last links are off screen | The list is taller than the window | max-height: calc(100vh - 40px); overflow-y: auto |
| Clicked headings hide under the header | The header is fixed | scroll-margin-top on the headings, top on the list |
| Nothing sticks at all | A theme rule breaks CSS sticky | The six causes and fixes |
More on what our plugin does and does not do and how to make any widget sticky; if you use Fixed Widget (q2w3) today, see the Fixed Widget alternative page. Still stuck with your theme? Write to support@quicksloth.dev with a link to the page.