Repository navigation
Stabilize the Table of Contents Block #42229
Description
Note: this Issue was updated by @getdave to reflect evolved requirements for stablising the block as agreed with contributors.
Older description (pre-2026-07 rework)
What problem does this address?
The table of content blocks was first released in Gutenberg 13.3 as an experimental block. A few releases later, no major bugs have been reported and we should start considering taking the block out of experimental.
What is your proposed solution?
Taking the block out of experimental would bring the block closer to shipping in WordPress 6.1.
Apart from removing the experimental flag in block.json it would be good to stabilize the dependent selector __experimentalGetGlobalBlocksByName, first introduced in #39610.
- @getdave do you see any reason not to stabilize the mentioned selector?
- @ZebulanStanphill @gziolo do you see any blocker not to stabilize the ToC block or other steps that should be done?
Pending items
- Stabilize the dependent selector
__experimentalGetGlobalBlocksByName: Rename __experimentalGetGlobalBlocksByName to getBlocksByName #58156 - Table of Contents: support usage outside of Post Content #41173: Table of Contents: Try dynamic rendering #72110
- Table of Contents: fix undo history quirk #41031: Table of Contents: Try dynamic rendering #72110
- Add an aria-label attribute to the wrapping nav element: Table of Contents: Add aria label to the nav element #71586
- Table of contents block – links don't work on initial page save #43595: Table of Contents: Try dynamic rendering #72110
- Use the same block toolbar controls as the List block (being able to switch between bullet styles): Table of Contents: Update toolbar controls #71587
What
The goal of this issue is to stabilize the Table of Contents block, targeting WordPress 7.2.
Stabilized, the block should always reflect the actual headings for whatever it's showing: the headings currently in the editor while a post is being edited, and the headings saved into post content wherever the block appears on the front end, including when it sits on a template shared across many posts, where the specific post those headings come from depends on which page a visitor is viewing.
Why
The block has been experimental since Gutenberg 13.3, and it's one people genuinely need. The main thing standing between it and stabilization is that it isn't dynamic enough to cope with the dynamic contexts it needs to work in.
Today it generates its output by saving static markup at edit time, built from whatever headings exist in the editor at that moment. That works for a single post, but breaks down anywhere there isn't one fixed set of headings to save at save time:
- when the block sits in a template rather than a single post, so the "right" headings depend on whichever post a visitor is viewing
- when headings live inside a referenced entity embedded in the content - a synced pattern, a template part, or similar - rather than directly in the post, or come from a block other than the core Heading block
- when the editor and the front end calculate headings using separate code, so there's no guarantee a reader ends up seeing the same table of contents the author saw while editing
Solving these makes the block more flexible, usable, robust and reliable, and puts it in a position where we can mark it stable.
How
Stabilizing the block means moving it to a server-side rendering model: one shared piece of logic decides which headings a table of contents shows, and the editor and the front end both call it, rather than each maintaining separate logic that can drift apart.
Each item below now has its own linked sub-issue - see the Sub-issues list on this issue for the full detail behind each one.
- Style settings: give the block color, typography, list-style, and dimension controls consistent with other core blocks (#43363, #43368, #43509, #63578, #71587, merged)
- Heading level filtering: let authors limit which heading levels the block displays, so a longer page can show only its main sections (#69063, merged)
- Dynamic server render: render the block dynamically from the server, using the post currently being viewed, rather than saved static markup (#80318, merged)
- Heading resolver class: extract heading discovery into a shared class on the server, so editor and front end can both call the same code, and so we can deliberately scope its public surface rather than exposing the algorithm wholesale (#80372, open)
- Template context: make the block behave correctly when placed in a template, not just a single post - see template-placement e2e cases below
- Stop saving markup: stop saving the block's static markup entirely (#80404, open)
- Referenced entity headings: extend the heading resolver class to resolve headings that live inside referenced entities embedded in the content - synced patterns, template parts, and similar - rather than only headings serialized directly in the post itself - see supporting-content e2e cases below
- REST endpoint: add a REST endpoint that exposes the heading resolver class, so the editor can retrieve headings the same way the front end does
- Editor consumes REST endpoint: update the editor to fetch headings from that REST endpoint instead of collecting them itself
- Third-party heading sources: extend the heading resolver class to support blocks registered by themes/plugins as heading sources
- Filterable resolver class: make the heading resolver class filterable, so themes and plugins can adjust its output to suit their own needs
User Stories
The following user stories reflect the desired behaviour from a user standpoint.
Writing and editing (content authors)
- As a content author, I should see the table of contents automatically update as I add, remove, or reorder headings, in order that I never have to build or maintain navigation by hand.
- As a content author, I should see an accurate preview of the table of contents while I'm still editing, in order that I can trust what I see before I publish.
- As a content author, I should be able to limit which heading levels appear - for example, only main sections, not sub-points - in order that the table of contents stays focused and useful on longer pages.
- As a content author, I should be able to choose between a numbered and a bulleted style, in order that the table of contents matches the look of the rest of my content.
- As a content author, I should see a clear message when my page has no headings yet, in order that I understand why the block looks empty rather than assuming it's broken.
Reading and navigating (site visitors)
- As a reader, I should be able to click any item in a table of contents and land exactly on that section, in order that I can move through long content quickly.
- As a reader, I should never click a table of contents link and end up on a missing or broken page, in order that I trust the site's navigation.
- As a reader, I should be taken to the right page of a multi-page article, even if the section I clicked is on a different page than the one I'm currently reading, in order that pagination doesn't break navigation.
- As a reader, I should see the table of contents laid out to match the structure of the page, with sub-sections nested under their section, in order that I can judge how the content is organised before I start reading.
Accessibility (readers using assistive technology)
- As a reader using a screen reader, I should be able to recognise the table of contents as a distinct, named region of the page, in order that I can jump straight to it or skip past it, rather than listening through it by accident.
- As a reader navigating by keyboard, I should be able to reach and move through the table of contents links in a predictable order, in order that I'm not stuck or disoriented.
- As a reader using a screen reader, I should be able to tell the table of contents apart from the site's main menu, in order that I don't confuse the two when there's more than one navigation region on the page.
Placing the block across a site (site builders)
- As a site builder, I should be able to add a table of contents once to a shared template, in order that it automatically appears on every post using that template, without adding it to each post individually.
- As a site builder, I should see a clear explanation in the editor when the table of contents can't show a live example while I'm editing a template directly, in order that I understand what it will actually do for visitors rather than assuming it's broken.
- As a site builder, I should be confident that a table of contents only ever lists headings from the specific post or page being viewed, never headings from the template itself or from any header, footer, or other reusable template part it includes, in order that every entry reliably takes a reader to part of the actual post.
- As a site builder, when I place the table of contents on a template that doesn't show one specific post - such as an archive, a search results page, or a "page not found" page - I should see an explanatory placeholder while editing, and it should quietly produce nothing on the actual page, in order that it never tries to show something meaningless to a reader.
Supporting content built with other blocks (authors and developers)
- As a content author, I should have my headings count towards the table of contents no matter which block I used to create them, in order that I'm not forced to change how I write just to get working navigation.
- As a theme or plugin developer, I should be able to register my own block as a heading source, in order that content built with my block isn't silently left out of visitors' tables of contents.
Known edge cases
To avoid edge cases being tracked only in scattered GitHub comments, they're codified as end-to-end tests marked test.fixme. All of these should be resolved before this merges for WordPress 7.2. If a new edge case is identified, add it as an e2e test first, to document it in code, then resolve it via a PR.
Currently tracked in table-of-contents.spec.js - 9 cases, in two groups that map onto the heading resolver class work above.
9 known edge cases
Placing the block across a site (template context)
- A table of contents added once to a shared template uses each viewed post's own heading list
- A table of contents in a shared template links to post headings created without a post-level table of contents
- Template editing explains when a live example can't be shown, while the front of site still uses the viewed post
- The block only lists headings from the viewed post's content, never from the template or its parts
- Templates that don't render post content (archives, search, 404) show an editor placeholder and render nothing to readers
Supporting content built with other blocks (extensible heading sources)
- A customised synced pattern's heading appears in the front-of-site table of contents
- Registered pattern block headings appear in the front-of-site table of contents
- Heading elements created by non-Heading blocks appear in the editor and after publish
- A plugin-registered heading-source block appears in the editor and after publish, while a plain heading-like block is ignored
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fields📋 Iteration/Tracking Issues
Activity
ZebulanStanphill commentedon Jul 7, 2022
My main concern with stabilizing the Table of Contents block is that, if the ideas in #41173 are implemented, it could drastically alter both the attribute schema and saved markup of the block. We might end up creating another backwards compatibility nightmare if we stabilize the block in its current form. I haven't had time to work on prototyping this idea, unfortunately.
The more I think about it, the more I realize that using a Table of Contents outside of the post content is actually the primary use-case for some website designs, so it feels almost wrong to have it be stabilized while it remains unusable there.
There's also the undo history bug described in #41031, which is just rather annoying to deal with. I have no clue how to fix it.
bph commentedon Jul 16, 2022
While you are looking at another iteration on the Table of Content Block,
I am pulling the issue of missing options to modify how the headlines are displayed from the many comments from the original PR .
While testing in PR stage, I felt strongly that there needs to be a way for a user to change the style of the list created. Not everyone appreciates an ordered list, and would rather use bullet points or arrows or something else entirely.
During the various iterations it started out as bullet list.
After initial discussions about missing options, it was actually changed to an ordered list. Although, there were other voices to advocate for more options for the display of the headlines, it did not receive any further consideration,
During the discussion, it was not accepted as a needed feature, and argued that it was in the theme developer's domain.
I still feel that an end user should be able to change the nature of the Table of Content list, via an option in the Sidebar, rather having the need to connect with a developer to modify the theme css.
In one comment it was mentioned that such settings should be in the Global Styles and deferred to a future interation.
To be consistent, most core blocks have myriad of options applied to each instance of the block, in addition to the theme.json settings for Global Styles.
For now, the current Table of Contents block doesn't provide neither block based options nor a theme.json or Global Style implementation to change the display of the Headlines.
With more options it would shine so much brighter, especially if it makes it into the next major WordPress release.
ndiego commentedon Jul 18, 2022
I 100% agree with @bph and @ZebulanStanphill assessments. This block, while a great start, could use some additional love. To echo some of the previous comments:
getdave commentedon Jul 20, 2022
As far as I'm aware there is no reason not to stablise this selector. Also noting it is not widely used.
Mamaduka commentedon Jul 20, 2022
I think styling enhancements aren't necessary blockers for stabilization. These can be done after.
The "Table of Contents outside of the post content" is nice to have but not a blocker, in my opinion. My best guess is that we'll have to use dynamic rendering and parse content blocks to achieve this.
The "undo bug" breaks editor history, so it probably should be fixed before we consider stabilizing the block — recent findings from @kevin940726 on this issue - #41031 (comment).
P.S. It would be nice to stress test the block on large posts and see if it affects typing.
ndiego commentedon Jul 20, 2022
Yeah, I am all for stabilizing. I just don't think we should include it in Core until the block is a bit more full-featured. While a great step forward, it feels incomplete.
gziolo commentedon Jul 21, 2022
Technically speaking, the issue is about exposing this block for WordPress Core by removing the experimental flag that ensures Table of Contents block can be used only with the Gutenberg plugin.
We need to make the decision whether this block should be general purpose and work everywhere or we limit its usage to the post editor. One way to approach it could by setting in
block.jsontheancestorfield tocore/post-content. It probably won't work out of the box, but it shouldn't be too difficult to implement.In general, it's a challenge to ensure that the Table of Contents block stays in sync when some dynamic blocks change outside of the place where the table of contents gets updated. It's also an issue when using the post editor and you have a Reusable block with heading that can be modified in every place on the site.
joedolson commentedon Jul 21, 2022
The table of contents block also needs to add an
aria-labelattribute to the wrappingnavelement. There will almost always be other nav elements present on the page (the main navigation, at minimum), so this needs to be made clear. The obvious choice for single post views is just 'Table of Contents', but if it's used outside of a single post view, it'll need further differentiation from other tables of contents.apeatling commentedon Aug 8, 2022
cc @priethor to see if this is still important for 6.1.
ZebulanStanphill commentedon Aug 8, 2022
I should note that due to being busy with several other things, it is unlikely that I will be able to help much with stabilizing the Table of Contents block, besides reviewing PRs from other people. If anyone wants to tackle the aforementioned issues, feel free to do so, because I simply don't have the time available to do so right now.
If you ask me, the switch back to dynamic rendering and the question of a bullet style option are two things that must be tackled prior to the block being considered stable.
priethor commentedon Aug 16, 2022
I think it is still worth exploring having the ToC block in 6.1. I don't think being limited to the Post Editor is a blocker; to me, it's an opportunity to include it earlier with a limited scope and iterate later on it.
ZebulanStanphill commentedon Aug 16, 2022
My primary concern is not the limitations it would have if merged now, but rather the backwards-compatibility burden it would cause later on, if (or most likely, when) the implementation changes. I've already seen several recent cases of core blocks having to make a bunch of compromises due to an inability to update existing static-rendered blocks on the front-end. For example:
<div>from the Button block's markup. Update the buttons block to use the new color support flag. #21266, Revert the button block to the previous markup #21923wp-block-headingclass to the Heading block. [Heading block] Add a wp-block-heading CSS class #42122110 remaining items
getdave commentedon Jul 28, 2026
@t-hamano @Mamaduka As you mentioned it was important to have a clear shared understanding of the work to be done I've
I've tried to keep these all user-centered and aligned with existing discussions and agreements. That said I would value a confidence check to ensure we're all aligned.
I think this should provide a solid foundation for contributors to proceed to stablise the block.
getdave commentedon Jul 28, 2026
In light of feedback, I've created #80794 which tracks the need to investigate the feasibility of server side render to ensure we do not needlessly overengineer the block.
This now sits higher up the task priority list so as to unblock the future engineering direction. Existing issues have a warning in place to ensure they don't proceed before this question has been addressed.
getdave commentedon Sep 8, 2026
Update
This effort is now targeting WordPress 7.2.
Style settings, heading level filtering, dynamic server-side rendering (#80318), and dropping the block's saved static markup (#80404) have all merged.
The remaining work is to determine where the Editor's heading level calculation algorithm should run and to weight the trade offs of the various approaches we've uncovered - namely:
Each has pros and cons and it's important we fully understand these before making a decision.
Template context, referenced-entity headings, third-party heading sources, and the filterable resolver (#80787, #80788) all depend on it and haven't started.
Where we're blocked
Discussion on the REST endpoint sub-issue (#80786) raised a question about whether a dedicated endpoint is the right call, versus reusing the editor's existing server-side render mechanism.
That produced a feasibility spike (#80794) and a working PR (#80796) that reuses the existing server-side rendering hooks for the editor preview.
Whilst appealing in its simplicity, unfortunately that spike surfaced a UX problem I don't think we can accept. Because it reads from the post as saved in the database, the block's preview only updates on save, not as the author edits. This manifests as the following:
Update.This doesn't feel like an acceptable UX to me.
Another option is maintaining a duplicate heading level algorithm on the Editor side. Whilst the overhead might be low it still raises the possibility of implementation drift over time. More importantly however, all extensibility would then need to be implemented twice (in both JS and PHP) which would then also require 3rd party authors to register extensions twice. This is clearly suboptimal.
The final option, is having the editor send the post content to the server via dedicated REST endpoint and reusing the same heading algorithm code as used by the PHP front of site rendering to provide a response for the Editor to render.
This solves the stale rendering and extensibility problems outlined above, but comes with a potential latency cost resulting from having to send post content over the network on every debounced key stroke.
My recommendation
I am still inclined towards the last option of a custom REST endpoint as it provides the best user (and 3rd party developer) experience even if it is harder for us as Core developers to implement.
I fully recognise the latency issue but we may be able to offset this by
In order for this block to move forward we need to come to a decision on which route to take. I'd very much appreciate others reviewing the above and offering their recommendation - @t-hamano, @Mamaduka, @tyxla, @scruffian.
Mamaduka commentedon Sep 8, 2026
Thanks, @getdave!
It's not clear to me how a dedicated REST endpoint can fix this case. It should read the post from somewhere to parse the headings, and usually that's the post from the DB, which will have the same issue.
Yes, we can send edited content over the wire, but we can do the same with the existing SSR. Both have to use
POSTrequests to account for the large payload, which is often a problem withGET.adrianduffell commentedon Sep 8, 2026
Excited to see this progressing. Thanks, @getdave !
With all the options seeming to have tradeoffs, I like the spike’s approach best for its simplicity. I think polling the REST API on every debounced keystroke could be a sizeable network/performance tax to pay, especially for large posts. I imagine there's further unknown complexities around this.
It might be possible to alleviate the UX concerns somewhat with a short status message for the block, to at least give users better framing around it. Eg:
tyxla commentedon Sep 9, 2026
I agree with @Mamaduka here. I think this is a problem with the spike, not with server-side rendering. Unless I'm missing something, the preview is stale because we pass
post_idand read the saved post content. Theblock-rendererroute already accepts POST, andServerSideRenderhas anhttpMethodprop, so we can send the unsaved content there too. I'd say both server-side options cost ~ the same in latency.So the question for me is how much new public API we want to add. I'd very much prefer to reuse what we have. The one thing to keep in mind is that the endpoint validates
attributeswithadditionalProperties: false, so we need a plan for sending the content - perhaps a good topic for #80786 before we build a new endpoint.Regarding extensibility: if #80787 stays declarative (based on block name), we could expose it to the editor, and third parties would register only once. Only #80788 really needs two implementations. So the end result may be simpler than it looks at first glance.
getdave commentedon Sep 9, 2026
It would need to come from the Editor post content otherwise (as you say) it would just have the same issue as the SSR approach.
You are welcome 👍 Thanks for jumping in here too.
My personal opinion (lightly held) is that this would be sub par UX. The ideal user experience should be a block that just works and doesn't require reading status messages to understand why it's not working.
I wasn't aware of that. I'd like to see the spike PR updated with that approach to see if it resolves the issue with stale content. I'm not entirely clear how it might resolve it yet, but I trust your instinct here and therefore it's worth exploring.
💯 agreed on this. I don't want to add new APIs if SSR work in a way which can provide an optimal user experience.
Yes that should work unless I"m missing another extensibility requirement? AFAIK there should be no huge need to replace the algorithm wholesale so perhaps this will prove sufficient.
Recommended next step: modify the current spike PR to test @tyxla's idea on SSR.