A page's headings already describe its structure: an <h2> sits under the <h1> above it, an <h3> under that <h2>, and so on. Given a root element, walk its subtree in document order, find every heading (h1 through h6), and build a nested tree that mirrors that hierarchy — the same kind of outline a docs site renders in its sidebar.
type TocNode = { text: string; level: number; children: TocNode[] };
function tableOfContents(root: Element): TocNode[];
Each node carries the heading's text (its textContent), its level (the number in the tag: h2 is level 2), and a children array of headings that nest beneath it. The return value is an array of the top-level entries.
const root = document.createElement('div');
root.innerHTML = '<h1>Guide</h1><h2>Setup</h2><h2>Usage</h2>';
tableOfContents(root);
// → [
// { text: 'Guide', level: 1, children: [
// { text: 'Setup', level: 2, children: [] },
// { text: 'Usage', level: 2, children: [] },
// ] },
// ]
root.innerHTML = '<h1>A</h1><h3>Deep</h3>';
tableOfContents(root);
// → [{ text: 'A', level: 1, children: [
// { text: 'Deep', level: 3, children: [] }, // h3 nests under h1
// ] }]
root.innerHTML = '<p>no headings here</p>';
tableOfContents(root); // → []
h1 followed directly by an h3 (no h2) nests the h3 under the h1. Do not invent a missing level.h3, an h2 is not a child of that h3 — it climbs back to attach under the nearest shallower heading.h2 that appears before any h1.text is the full textContent, including the text of any inline markup.Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.