Skip to content

Stabilize the Table of Contents Block #42229

Open
1 of 9 issues completed
Open

Description

@priethor

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

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

Activity

  1. ZebulanStanphill commented on Jul 7, 2022

    @ZebulanStanphill
    Member

    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.

  2. bph commented on Jul 16, 2022

    @bph
    Contributor

    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.

  3. ndiego commented on Jul 18, 2022

    @ndiego
    Member

    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:

    1. The largest area of concern to me is the fact that it does not work outside of post content, thereby making the block unusable in the Site Editor. Without a fix here, I anticipate a lot of user confusion and frustration. You are able to add the TOC block to templates, yet it doesn't display anything.
    2. The block needs more style settings to be consistent with other Core blocks. Color, typography, list style, and dimensions would be a good start.
    3. There needs to be a way to control the headings that are displayed. A common use case is only to display H2 and H3s, but not the H4, H5, etc.
  4. getdave commented on Jul 20, 2022

    @getdave
    Contributor

    @getdave do you see any reason not to stabilize the mentioned selector?

    As far as I'm aware there is no reason not to stablise this selector. Also noting it is not widely used.

  5. Mamaduka commented on Jul 20, 2022

    @Mamaduka
    Member

    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.

  6. ndiego commented on Jul 20, 2022

    @ndiego
    Member

    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.

  7. gziolo commented on Jul 21, 2022

    @gziolo
    Member

    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.

    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.

    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.

    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.

    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.json the ancestor field to core/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.

  8. joedolson commented on Jul 21, 2022

    @joedolson
    Contributor

    The table of contents block also needs to add an aria-label attribute to the wrapping nav element. 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.

  9. apeatling commented on Aug 8, 2022

    @apeatling
    Contributor

    cc @priethor to see if this is still important for 6.1.

  10. ZebulanStanphill commented on Aug 8, 2022

    @ZebulanStanphill
    Member

    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.

  11. priethor commented on Aug 16, 2022

    @priethor
    ContributorAuthor

    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.

  12. ZebulanStanphill commented on Aug 16, 2022

    @ZebulanStanphill
    Member

    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:

  13. 110 remaining items

  14. getdave commented on Jul 28, 2026

    @getdave
    Contributor

    @t-hamano @Mamaduka As you mentioned it was important to have a clear shared understanding of the work to be done I've

    • revised the Issue description
    • created sub Issues for each task

    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.

  15. getdave commented on Jul 28, 2026

    @getdave
    Contributor

    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.

  16. moved this to 🏗️ In Progress in WordPress 7.2 Editor Taskson Aug 13, 2026
  17. moved this from 🏗️ In Progress to 📋 Iteration/Tracking Issues in WordPress 7.2 Editor Taskson Aug 13, 2026
  18. getdave commented on Sep 8, 2026

    @getdave
    Contributor

    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:

    • run server side and render via editor's existing server-side render hooks
    • run on Editor (client) side
    • run server side using a custom REST endpoint and have the editor send its current, unsaved content to get the headings

    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:

    • A new, unsaved post shows no preview until first save.
    • A saved draft's preview stays stale after an edit until autosave or a manual save catches up,
    • A published post's preview can stay stale until the author clicks 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

    • sending only a filtered version of the contents (only registered headings?)
    • being aggressive with our debouncing (instant previews for the ToC block aren't necessary)

    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.

  19. Mamaduka commented on Sep 8, 2026

    @Mamaduka
    Member

    Thanks, @getdave!

    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

    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 POST requests to account for the large payload, which is often a problem with GET.

  20. adrianduffell commented on Sep 8, 2026

    @adrianduffell

    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:

    State Status Message
    A new, unsaved post shows no preview until first save Table of Contents will be generated when the post is saved as a draft, or published
    A saved draft’s preview stays stale after an edit until autosave or a manual save catches up Table of Contents last generated 30 seconds ago
    A published post’s preview can stay stale until the author clicks Update Table of Contents will be re-generated when the post is updated
  21. tyxla commented on Sep 9, 2026

    @tyxla
    Member

    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_id and read the saved post content. The block-renderer route already accepts POST, and ServerSideRender has an httpMethod prop, 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 attributes with additionalProperties: 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.

  22. getdave commented on Sep 9, 2026

    @getdave
    Contributor

    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.

    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.

    Excited to see this progressing. Thanks, @getdave !

    You are welcome 👍 Thanks for jumping in here too.

    It might be possible to alleviate the UX concerns somewhat with a short status message for the block

    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.

    ServerSideRender has an httpMethod prop, so we can send the unsaved content there too

    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.

    So the question for me is how much new public API we want to add

    💯 agreed on this. I don't want to add new APIs if SSR work in a way which can provide an optimal user experience.

    Regarding extensibility: if #80787 stays declarative (based on block name), we could expose it to the editor,

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions