@altano/astro-table-of-contents
v0.1.3
Published
Table of Contents components for Astro
Maintainers
Readme
astro-table-of-contents
A set of Astro components to help you make a table of contents.
TableOfContents.astro
Generate the markup for a table of contents from Astro's headings object.
Getting the headings object from Astro
If you're importing your markdown directly (documentation):
---
import {getHeadings} from "./Article.md";
const headings = getHeadings();
---If you're using content collections (documentation):
---
import { getEntry } from "astro:content";
const article = await getEntry("articles", Astro.props.slug);
const { headings } = await article.render();
---Component Props
headings(required) - The heading property from Astro-rendered markdown an automatically-generatedheadingsproperty.class(optional) - A class with styles to pass down to theulin the table of contentsfromDepth(optional) - A minimum depth for the table of contents, which defaults to 1 (i.e. includeh1and above).toDepth(optional) - A maximum depth for the table of contents, which defaults to 6 (i.e. includeh6and below).
Example - Custom Heading Depths
---
import TableOfContents from "@altano/astro-table-of-contents/TableOfContents.astro";
const { headings } = await Astro.props.article.render();
---
<TableOfContents {headings} fromDepth={2} toDepth={5} />This will only include h2-h5 from headings.
Example - Add Styling
---
import TableOfContents from "@altano/astro-table-of-contents/TableOfContents.astro";
const { headings } = await Astro.props.article.render();
---
<style>
nav {
position: sticky;
top: 0;
max-height: 100vh;
overflow-y: auto;
}
.toc {
a {
color: orangered;
/**
* different color based on nav level
*/
&[aria-level="2"] {
color: blue;
}
&[aria-level="3"] {
color: cyan;
}
}
}
</style>
<nav>
<TableOfContentsWithScrollSpy class="toc" {headings} />
</nav>Styling Based On Visibility

To style elements of your table of contents differently depending on whether or not the target section is visible, e.g. color links differently when the link target is visible, you must:
- Wrap your article with an
ArticleSectionVisibilityObserver.astrocomponent, and - Use
TableOfContentsWithScrollSpy.astrofor your table of contents. - Add styles that use the
aria-currentattribute, such as styling active anchors to be a different color or opacity. - Your content must be in sections. If you're using Markdown, you can automatically sectionize (based on headings) with the
remark-sectionizeplugin (example).
e.g. your Astro component should look something like:
---
import ArticleSectionVisibilityObserver from "@altano/astro-table-of-contents/ArticleSectionVisibilityObserver.astro";
import TableOfContentsWithScrollSpy from "@altano/astro-table-of-contents/TableOfContentsWithScrollSpy.astro";
const { headings } = await Astro.props.article.render();
---
<style>
.toc {
a {
color: inherit;
/* Make links to invisible sections faded */
opacity: 0.4;
/**
* The `aria-current` attribute is true when the anchor's target is
* visible. Color such links orangered to make them stand out.
*/
&[aria-current="true"] {
opacity: 1;
color: orangered;
}
}
}
</style>
<div>
<nav>
<TableOfContentsWithScrollSpy {headings} class="toc" />
</nav>
<ArticleSectionVisibilityObserver>
<article>
<section>
<h2>Heading 1</h2>
...
</section>
<section>
<h2>Heading 2</h2>
...
</section>
</article>
</ArticleSectionVisibilityObserver>
</div>This uses a standard custom element (a web component) to monitor the visible article sections (using IntersectionObserver) and another standard custom element to set aria-current=true on anchor elements whose section is visible. The runtime (vanilla) JS added is very tiny, roughly 740 bytes (minified + gzipped). BYTES, not kilobytes.
Live Demo
You can see these components in use on my personal site. The relevannt source code for this site can be found here here and here.
