A lightweight Vue 3 component to render a Table of Contents (TOC) for your articles or documentation, with smooth scrolling and nested sections support.
- Features
- Installation
- Usage of Styles
- Quick Start (SPA)
- Nuxt 3 / SSR Usage
- Component Registration Options
- Props
- Composable: useToc
- Customization (Styles)
- SSR Notes
- Examples
- Development
- Contributing
- License
- Simple and focused Table of Contents (TOC) component for Vue 3.
- Supports nested sections via children links.
- Smooth scrolling to headings using
scrollIntoView. - URL hash is updated using
history.pushStatefor better navigation and shareable links. - Works in SPA (Vite, Vue CLI) and Nuxt 3 (with client-side rendering constraints).
- Ships with minimal, customizable styles.
Using npm:
npm install @todovue/tv-tocUsing yarn:
yarn add @todovue/tv-tocUsing pnpm:
pnpm add @todovue/tv-tocImport the CSS generated by the library in your main entry file:
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import '@todovue/tv-toc/style.css'
import { TvToc } from '@todovue/tv-toc'
const app = createApp(App)
app.component('TvToc', TvToc)
app.mount('#app')Add the library's CSS to your Nuxt configuration:
// nuxt.config.ts
export default defineNuxtConfig({
modules: [
'@todovue/tv-toc/nuxt'
]
})Global registration (main.js / main.ts):
import { createApp } from 'vue'
import App from './App.vue'
import TvToc from '@todovue/tv-toc'
createApp(App)
.component('TvToc', TvToc)
.mount('#app')Local import inside a component:
<script setup>
import { TvToc } from '@todovue/tv-toc'
const toc = {
title: 'On this page',
links: [
{ id: 'introduction', text: 'Introduction' },
{
id: 'getting-started',
text: 'Getting started',
children: [
{ id: 'installation', text: 'Installation' },
{ id: 'basic-usage', text: 'Basic usage' },
],
},
{ id: 'api', text: 'API Reference' },
],
}
</script>
<template>
<div class="page-layout">
<main class="page-content">
<h2 id="introduction">Introduction</h2>
<!-- ... -->
<h2 id="getting-started">Getting started</h2>
<h3 id="installation">Installation</h3>
<h3 id="basic-usage">Basic usage</h3>
<!-- ... -->
<h2 id="api">API Reference</h2>
</main>
<aside class="page-toc">
<TvToc :toc="toc" />
</aside>
</div>
</template>Create a plugin file: plugins/tv-toc.client.ts (client-only because it uses document and history under the hood when scrolling):
import { defineNuxtPlugin } from '#app'
import TvToc from '@todovue/tv-toc'
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.component('TvToc', TvToc)
})Use anywhere (recommended inside <client-only> when using Nuxt 3):
<template>
<client-only>
<TvToc :toc="toc" />
</client-only>
</template>Direct import (no plugin):
<script setup>
import { TvToc } from '@todovue/tv-toc'
</script>
<template>
<TvToc :toc="toc" />
</template>| Approach | When to use |
|---|---|
Global via app.component('TvToc', TvToc) |
Frequent use across the app |
Local named import { TvToc } |
Isolated/code-split contexts |
Direct default import import TvToc from ... |
Single use or manual registration |
| Name | Type | Default | Description | Required |
|---|---|---|---|---|
| toc | Object | - | TOC configuration: title and list of links (with optional nested children) | true |
type TocLink = {
id: string
text: string
children?: TocLink[]
}
type Toc = {
title?: string
links: TocLink[]
}title: Optional title shown at the top of the TOC (h3).links: Array of top-level sections.id: Must match theidattribute of the target heading in your content.text: Label shown in the TOC.children: Optional array of sub-sections, rendered as nested list.
This composable is used internally by TvToc but can also be imported directly if needed.
import { useToc } from '@todovue/tv-toc'
const { formatId, scrollToId } = useToc()| Name | Type | Description |
|---|---|---|
| formatId | (id: string) => string |
Returns a hash-based id string (e.g. "#section-id"). |
| scrollToId | (id: string) => void |
Smooth scrolls to the element with that id and updates hash. |
Note:
scrollToIdaccessesdocumentandhistory, so it should run only in the browser (e.g. in event handlers or insideonMounted).
The component ships with minimal default styles, exposed through the built CSS file and scoped CSS classes.
Main CSS classes:
.tv-toc— Root<nav>container..tv-toc-title— Title of the TOC..tv-toc-list— Top-level list container..tv-toc-item— Top-level list item..tv-toc-link— Anchor for top-level items..tv-toc-sublist— Nested list container for children..tv-toc-subitem— Nested list item..tv-toc-sublink— Anchor for nested items.
You can override these styles in your own global stylesheet:
/* example overrides */
.tv-toc {
font-size: 0.9rem;
}
.tv-toc-link,
.tv-toc-sublink {
color: #4b5563;
}
.tv-toc-link:hover,
.tv-toc-sublink:hover {
color: #111827;
}If you are using SCSS, you can also rely on your own design tokens and overrides. The package itself internally uses SCSS (see src/assets/scss/_variables.scss and src/assets/scss/style.scss).
- The component can be rendered on the server (template is static), but scrolling behavior uses browser APIs.
scrollToIdusesdocument.getElementByIdandhistory.pushState; these are only invoked in event handlers on the client.- When using Nuxt 3, prefer registering
TvTocin a*.client.tsplugin or wrap usages in<client-only>to avoid hydration edge cases in environments with stricter SSR.
This repository includes a small demo application built with Vite.
- Basic TOC example.
- Blog-like layout with nested headings.
To run the demo locally, see the Development section.
git clone https://github.com/TODOvue/tv-toc.git
cd tv-toc
npm install
npm run dev # run local demo
npm run build # build libraryThe local demo is served with Vite using index.html and examples in src/demo.
To build the standalone demo used for documentation:
npm run build:demoPRs and issues are welcome. See CONTRIBUTING.md and CODE_OF_CONDUCT.md.
MIT © TODOvue
Crafted for the TODOvue component ecosystem
