
Table of Content is a Web Component that adds a compact vertical content rail with floating previews for the selected section.
The interaction resembles the slim history navigator in ChatGPT. Pointer or touch movement scrubs across the rail, nearby ticks expand, and the preview card follows the current item.
When your visitor moves a pointer or touch across the rail, ticks morph smoothly to show the active neighborhood, and a card pops out to display the selected section’s title and description.
Keyboard navigation uses the same vertical slider pattern, and every interaction (pointer, touch, keyboard)converges on the same spring‑driven animation that respects the visitor’s reduced motion preferences.
Features:
- Continuous pointer and touch scrubbing across a vertical rail.
- Spring-based tick magnification around the active position.
- Floating cards that resize to fit title and description copy.
- Keyboard control through Arrow, Home, End, and Escape keys.
- Container-responsive card sizing with public CSS variables.
- Shadow Parts for targeted styling inside the open shadow root.
- Reduced-motion and forced-colors support.
Use Cases:
- Long documentation pages gain a narrow visual index for previewing and selecting sections.
- Release histories and activity feeds can map the rail to dated entries or product milestones.
- AI chat archives can expose conversation checkpoints through a familiar sidebar interaction.
- Product tours and process viewers can connect each tick to a slide, panel, or application state.
How To Use It:
1. Download the package and load table-of-content-model.js before table-of-content.js.
<script src="./table-of-content-model.js"></script> <script src="./table-of-content.js"></script>
2. Add the custom element, then assign a non-empty array to its items property. Each item accepts an id, title, and description. The component emits toc-change whenever the selected index changes. This example connects each rail item to a matching document section. Pointer scrubbing or keyboard navigation moves the page to the selected section.
<table-of-content
id="guide-toc"
label="Guide sections"
value="0"
></table-of-content>
<article>
<section id="overview">
<h2>Overview</h2>
<p>Introduce the component and its primary interaction.</p>
</section>
<section id="configuration">
<h2>Configuration</h2>
<p>List the public properties and styling controls.</p>
</section>
<section id="events">
<h2>Events</h2>
<p>Connect selection changes to application behavior.</p>
</section>
</article>
<script src="./table-of-content-model.js"></script>
<script src="./table-of-content.js"></script>
<script>
const toc = document.querySelector('#guide-toc');
toc.items = [
{
id: 'overview',
title: 'Component overview',
description: 'Review the vertical rail and floating preview behavior.',
},
{
id: 'configuration',
title: 'Configuration',
description: 'Adjust state, labels, colors, and text limits.',
},
{
id: 'events',
title: 'Event handling',
description: 'Connect selection changes to page navigation.',
},
];
toc.addEventListener('toc-change', (event) => {
const target = document.getElementById(event.detail.item.id);
target?.scrollIntoView({ block: 'start' });
});
</script>
3. Available Attributes And Properties:
label/.label(string): Sets the accessible name for the internal slider. The default label is"Table of content".value/.value(number): Sets the zero-based selected index. Values are rounded and clamped to the available item range.open/.open(boolean): Pins the preview card open while the attribute is present or the property istrue..items(array): Replaces the section records. The array must contain at least one item and supports up to 200 items.
4. API methods:
const toc = document.querySelector('table-of-content');
// Select the fourth item, open its card, and emit toc-change.
toc.select(3);
// Update the selected item silently and keep the preview closed.
toc.select(1, {
open: false,
emit: false,
});
// Clear the pinned state and close the preview.
toc.close();
The open attribute takes precedence over options.open. Calling .close() removes the pinned state and closes the card.
5. Event handlers:
const toc = document.querySelector('table-of-content');
// Fires when the selected index changes.
toc.addEventListener('toc-change', (event) => {
console.log(event.detail.index);
console.log(event.detail.item);
});
// Fires when the preview card opens.
toc.addEventListener('toc-open', (event) => {
console.log(event.detail.index);
});
// Fires when the preview card closes.
toc.addEventListener('toc-close', (event) => {
console.log(event.detail.index);
});
6. Sync The Rail With Visible Sections:
const toc = document.querySelector('#guide-toc');
const sections = [...document.querySelectorAll('article section[id]')];
const observer = new IntersectionObserver(
(entries) => {
const visibleSection = entries.find((entry) => entry.isIntersecting);
if (!visibleSection) return;
const index = toc.items.findIndex(
(item) => item.id === visibleSection.target.id,
);
if (index >= 0) {
toc.select(index, {
open: false,
emit: false,
});
}
},
{
rootMargin: '-20% 0px -65%',
},
);
sections.forEach((section) => observer.observe(section));
7. The component exposes inherited CSS custom properties on the <table-of-content> host:
| Custom property | Default |
|---|---|
--toc-background | #fff |
--toc-surface | #fff |
--toc-ink | oklch(22% 0.008 250) |
--toc-copy | oklch(56% 0.008 250) |
--toc-line | oklch(88% 0.006 250) |
--toc-accent | oklch(51% 0.09 251) |
--toc-title-size | 1rem |
--toc-description-size | 1rem |
--toc-title-lines | 2 |
--toc-description-lines | 4 |
Set either line-count property to unset when the card should show the full field. The accessible value text always includes the complete title and description.
table-of-content {
--toc-background: transparent;
--toc-surface: #111827;
--toc-ink: #f9fafb;
--toc-copy: #cbd5e1;
--toc-line: #334155;
--toc-accent: #38bdf8;
--toc-title-size: 1.0625rem;
--toc-description-size: 0.9375rem;
--toc-title-lines: 1;
--toc-description-lines: 3;
}
The open shadow root exposes these parts:
viewport, stage, rail, ticks, tick, card, card-content, title, and description.
Use ::part() for structural details that CSS variables do not cover.
table-of-content::part(card) {
border-radius: 0.75rem;
backdrop-filter: blur(14px);
}
table-of-content::part(tick) {
height: 0.25rem;
}
table-of-content::part(title) {
letter-spacing: 0;
}
Alternatives:
- Create Dynamic Navigation Menus (TOC) for Long Content
- Highly Customizable Table Of Contents Generator – Tocbot
- Floating Table Of Contents In JavaScript – Ootliner
- Collapsible Side Table Of Contents In JavaScript – DocumentOutline
FAQs:
Q: Does Table of Content generate items from page headings?
A: No. Assign the item records through .items, then connect toc-change to the matching sections, routes, panels, or application states.
Q: Why does the classic script version throw a model error?
A: table-of-content-model.js must load before table-of-content.js.
Q: Can the component run in React, Vue, or an SSR framework?
A: SSR applications must delay the import until browser APIs such as customElements, HTMLElement, and Shadow DOM are available.
Q: How many sections can the rail display?
A: The items property accepts 1 to 200 records. A shorter, grouped list usually produces a clearer rail for long histories.
Q: Can the preview card stay open?
A: Add the open attribute or set .open = true. Call .close() when the application should return to hover, touch, and focus behavior.







