NbTableOfContents lists the sections of a long page as links, highlights the section being read as the page scrolls, and scrolls to a section when one is chosen. It comes in two forms: docked, for a page with a gutter beside the content, and floating, a single button that opens the list over a page with no room for one.
Scroll the article below, then choose a section.
<script setup lang="ts">
import { ref } from 'vue'
import type { ITableOfContentsItem } from '@nubisco/ui'
const article = ref<HTMLElement | null>(null)
const sections: ITableOfContentsItem[] = [
{ id: 'overview', label: 'Overview', level: 2 },
{ id: 'install', label: 'Install', level: 3 },
{ id: 'configure', label: 'Configure', level: 3 },
{ id: 'usage', label: 'Usage', level: 2 },
]
</script>
<template>
<article ref="article">
<h2 id="overview">Overview</h2>
<!-- … -->
</article>
<aside>
<NbTableOfContents :items="sections" :root="article" />
</aside>
</template>Items
items is flat, in document order, with each heading's level. The component nests them itself. Nesting is relative, so an h4 straight under an h2 sits one step in, and a skipped level never opens an empty indent.
By default an item's id is the target element's id, and each link points at #id, so a link opened in a new tab lands on the same section. When the targets have no ids (headings an editor rebuilds as they are typed, for example) pass resolveTarget:
<NbTableOfContents
:items="outline"
:resolve-target="(item, index) => headings[index] ?? null"
/>Following the scroll
The section being read is the last one whose heading has scrolled above offset pixels from the top of the scroll container. At the very bottom of the page the last section on screen wins instead, because a short final section can never scroll that far. The scroll container is found for you: the nearest scrolling ancestor of the first target, which in an application shell is usually <main>, not the window.
Keep offset larger than the targets' scroll-margin-top, so the section a link scrolls to is the one that ends up highlighted. While a smooth scroll is on its way, the highlight stays on the chosen section rather than flickering through the ones it passes.
Bind v-model:active to know the current section or set it yourself. Set :spy="false" to turn scroll tracking off.
Choosing a section
A plain click scrolls to the section, marks it with aria-current="location", and moves focus to it (unless it is inside an editable region, where focus would not put the caret there). The address bar gets #id with replaceState, so the URL is shareable and Back still leaves the page. Set :update-hash="false" to leave the address bar alone. A modified click (new tab, new window) is left to the browser.
Set follow-hash to land on the section named in the address bar when the page opens. It waits until that section has rendered, and does it once.
Floating
The floating form is for a page with no gutter: a narrow reading column, or a phone. It starts closed as a single button, opens onto a surface over the page, and closes again when a section is chosen or Escape is pressed. Place it yourself, typically in a corner of the page.
<div class="page">
<article ref="article">…</article>
<NbTableOfContents
class="page__contents"
variant="floating"
:items="sections"
:root="article"
/>
</div>
<style scoped>
.page {
position: relative;
}
.page__contents {
position: absolute;
inset-block-start: 0;
inset-inline-end: 0;
}
</style>Collapsing
A docked contents can be hidden down to its show button, and the choice is the host's to remember: bind v-model:open and store it, so a reader who closes the contents finds it closed on the next page. Set :collapsible="false" for one that is always open.
Keyboard and accessibility
- The list is a
<nav>landmark, named bylabel, holding nested<ol>lists of real links. Every link is in the tab order, unlike the rows of a tree. - The current section carries
aria-current="location". - The show and hide buttons carry
aria-expandedandaria-controls. Hiding the list moves focus to the show button, so a keyboard user is not dropped to the top of the page. - In the floating form, Escape closes the list and returns focus to its button.