TableOfContents
TableOfContents reads the h1 to h6 headings of an HTML string and renders a nested list of links to them. Pass
it the rendered content of a CMS item to give long articles and documentation pages an outline.
Package Osirion.Blazor.Cms.Web, namespace Osirion.Blazor.Cms.Web.Components. Source on GitHub.
Install
Sample text for the first section.
Add the package
Sample text for a subsection.
Register the services
Sample text for a second subsection.
Use it
Sample text for the last section.
@using Osirion.Blazor.Cms.Web.Components
@* The headings come from the sample HTML below. It is rendered under the contents so the links have somewhere to go. *@
<TableOfContents Content="@sampleHtml" />
<div class="mt-3">
@((MarkupString)sampleHtml)
</div>
@code {
// A constant written for this demo, not content from a reader, so it is safe to render as markup.
private const string sampleHtml = """
<h2 id="toc-demo-install">Install</h2>
<p>Sample text for the first section.</p>
<h3 id="toc-demo-package">Add the package</h3>
<p>Sample text for a subsection.</p>
<h3 id="toc-demo-register">Register the services</h3>
<p>Sample text for a second subsection.</p>
<h2 id="toc-demo-use">Use it</h2>
<p>Sample text for the last section.</p>
""";
}The demo builds the outline from a small sample HTML string and renders that HTML below it, so the links have headings to go to.
Usage
@using Osirion.Blazor.Cms.Web.Components
<TableOfContents Content="@Item?.Content" />
A sidebar outline limited to the second and third heading levels:
@using Osirion.Blazor.Cms.Domain.Entities
@using Osirion.Blazor.Cms.Web.Components
<aside>
<h2>On this page</h2>
<TableOfContents Content="@Item?.Content"
MinLevel="2"
MaxLevel="3"
EmptyText="This page has no sections." />
</aside>
@code {
[Parameter]
public ContentItem? Item { get; set; }
}
The built-in ArticlePage and DocumentPage render it the same way, under their own TableOfContentsTitle
heading.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
Content |
string? |
null |
The HTML to read the headings from, usually an item's rendered content. Nothing is listed when it is empty. |
MinLevel |
int |
2 |
The lowest heading level included, so the page's h1 is left out by default. |
MaxLevel |
int |
6 |
The highest heading level included. |
MaxDepth |
int |
3 |
The nesting limit. 1 lists only top-level headings; the list renders at most two levels, so a value above 2 changes nothing. |
EmptyText |
string |
"No headings found." |
The text shown when no heading matches. |
TrackActiveHeading |
bool |
true |
Whether the link to the section being read is marked as it scrolls by: it gets the osirion-toc-link-active class and aria-current="location". A script does it, so without script the contents work as plain links. |
Theme |
ThemeMode |
ThemeMode.System |
The component's color mode. Light or Dark writes it to data-theme (data-bs-theme under Bootstrap) on the root element; System writes nothing. |
Class |
string? |
null |
Extra CSS classes for the component's root element. |
Style |
string? |
null |
An inline style for the component's root element. |
Class goes on the nav. Style and Theme are accepted but not written, and other attributes on the tag are
captured but not rendered.
Behavior
- Each link points to
{current path}#{heading id}. The CMS Markdown renderer gives every heading anidand the HTML sanitizer keeps it, so the links work for CMS content. For a heading without anid, the component makes one from the text (lower case, letters and digits, spaces turned into hyphens), and the link only works if your page has an element with that id. - The heading's
idis read wherever it sits among the attributes of the tag, quoted or bare. A heading whose text runs over several lines is found. - The search for headings stops after one second. On HTML that makes it take longer, such as thousands of unclosed heading tags, the outline is empty and a warning is logged.
- Headings are nested by level: an
h3goes under theh2before it. The list shows two levels, the top-level headings and the headings directly under them. Deeper headings are not shown. - Tags inside a heading are stripped and the remaining text is written as text, so it is HTML-encoded. Character
references are not decoded first, so a heading such as
Q&A, which the Markdown renderer writes asQ&A, shows asQ&Ain the outline. - The component always renders a
navwith the accessible name "Table of Contents" (fixed English text), and theEmptyTextparagraph when there is nothing to list. Headings are read on every parameter change, so the outline is part of the static SSR response. - With
TrackActiveHeadingon and at least one heading listed, a small script (loaded through BlazorJSComponents, whichAddOsirionregisters) marks the link of the section being read as the page scrolls: the last heading whose top has passed a line a quarter of the viewport below the top (plus the--osirion-anchor-offsetof a sticky header). At the end of the page the last heading wins. Without script the links work as they are.
Styling
| Class | Element |
|---|---|
osirion-table-of-contents |
The nav, plus your Class |
osirion-toc-list |
The top-level ol |
osirion-toc-sublist |
The nested ol |
osirion-toc-item |
Each li, together with osirion-toc-level-{n} for the heading level |
osirion-toc-link |
Each link; the script adds osirion-toc-link-active to the one being read |
osirion-toc-empty |
The empty-state paragraph |
Links use --osirion-text-secondary and --osirion-action-primary, with --osirion-focus-ring-color for keyboard
focus. The marked link reads --osirion-toc-active-color, --osirion-toc-active-accent and
--osirion-toc-active-background. The stylesheet also has print rules.
Related
- OsirionStickySidebar, to keep the outline in view while reading
- Content View
- CMS navigation components