Skip to content
Navigation

Table of Contents

The “on this page” rail, with a marker that slides onto the section you are reading.

Walkthrough

1. Basic

The rail follows whatever scrolls — here a panel, on this page the window. Scroll the column on the left and the marker slides onto the section whose heading has gone past the top.

Installation

Enough copy to scroll past, so the rail beside it has something to follow. The marker measures the link it lands on, so it fits a wrapped label as readily as a short one.

With the CLI

Enough copy to scroll past, so the rail beside it has something to follow. The marker measures the link it lands on, so it fits a wrapped label as readily as a short one.

By hand

Enough copy to scroll past, so the rail beside it has something to follow. The marker measures the link it lands on, so it fits a wrapped label as readily as a short one.

Theming

Enough copy to scroll past, so the rail beside it has something to follow. The marker measures the link it lands on, so it fits a wrapped label as readily as a short one.

Design tokens

Enough copy to scroll past, so the rail beside it has something to follow. The marker measures the link it lands on, so it fits a wrapped label as readily as a short one.

Recipes

Enough copy to scroll past, so the rail beside it has something to follow. The marker measures the link it lands on, so it fits a wrapped label as readily as a short one.

<script lang="ts">	import { Toc } from 'omaris';​	const sections = [		{ id: 'demo-install', text: 'Installation', level: 2 },		{ id: 'demo-cli', text: 'With the CLI', level: 3 },		{ id: 'demo-manual', text: 'By hand', level: 3 },		{ id: 'demo-theming', text: 'Theming', level: 2 },		{ id: 'demo-tokens', text: 'Design tokens', level: 3 },		{ id: 'demo-recipes', text: 'Recipes', level: 2 }	];</script>​<!--	One column on a phone: a 12rem rail beside the article leaves the article	about 60px, which is one word a line.--><div class="grid w-full grid-cols-1 gap-8 sm:grid-cols-[minmax(0,1fr)_12rem]">	<!-- svelte-ignore a11y_no_noninteractive_tabindex -->	<div		class="scrollbar-subtle h-80 overflow-y-auto rounded-shape-md border border-border"		tabindex="0"		role="group"		aria-label="Article"	>		<div class="flex flex-col gap-6 p-5">			{#each sections as section (section.id)}				<section class="flex flex-col gap-2">					<h2 id={section.id} class="scroll-mt-5 text-title-sm text-foreground">{section.text}</h2>					<p class="text-body-md text-muted-foreground">						Enough copy to scroll past, so the rail beside it has something to follow. The marker						measures the link it lands on, so it fits a wrapped label as readily as a short one.					</p>				</section>			{/each}		</div>	</div>​	<!-- `offset` is measured from the top of whatever scrolls — here the panel. -->	<Toc items={sections} offset={24} /></div>

2. Levels

Indentation is measured from the shallowest heading in the list, so a page that never uses h2 reads flush instead of indented once.

h2 · Overview

h3 · Props

h4 · The class prop

h4 · The classes prop

h2 · Parts

<script lang="ts">	import { Toc } from 'omaris';​	const items = [		{ id: 'lv-overview', text: 'Overview', level: 2 },		{ id: 'lv-props', text: 'Props', level: 3 },		{ id: 'lv-props-class', text: 'The class prop', level: 4 },		{ id: 'lv-props-classes', text: 'The classes prop', level: 4 },		{ id: 'lv-parts', text: 'Parts', level: 2 }	];​	/** The page beside the rail, indented the same way the rail is. */	const indent = { 2: '', 3: 'ps-5', 4: 'ps-10' } as const;</script>​<!-- One column on a phone; the rail is 12rem and would leave nothing beside it. --><div class="grid w-full grid-cols-1 items-start gap-8 sm:grid-cols-[minmax(0,1fr)_12rem]">	<div class="flex flex-col gap-3 rounded-shape-md border border-border p-5">		{#each items as item (item.id)}			<p id={item.id} class="text-body-md text-foreground {indent[item.level as 2 | 3 | 4]}">				h{item.level} · {item.text}			</p>		{/each}	</div>​	<Toc {items} spy={false} active="lv-props-classes" label="Mixed levels" /></div>

3. Controlled

spy={false} hands the highlight over: bind:active is then the only thing that moves the marker.

active: ct-usage

<script lang="ts">	import { Toc, Button } from 'omaris';​	const items = [		{ id: 'ct-overview', text: 'Overview', level: 2 },		{ id: 'ct-usage', text: 'Usage', level: 2 },		{ id: 'ct-api', text: 'API', level: 2 }	];​	let active = $state('ct-usage');</script>​<!-- One column on a phone; the rail is 11rem and would leave nothing beside it. --><div class="grid w-full grid-cols-1 items-start gap-8 sm:grid-cols-[minmax(0,1fr)_11rem]">	<div class="flex flex-col items-start gap-4">		<div class="flex flex-wrap gap-2">			{#each items as item (item.id)}				<Button					id={item.id}					size="sm"					variant={active === item.id ? 'filled' : 'outlined'}					onclick={() => (active = item.id)}				>					{item.text}				</Button>			{/each}		</div>		<p class="text-body-sm text-muted-foreground">active: {active}</p>	</div>​	<Toc {items} spy={false} bind:active /></div>

4. Overridden

Every part is reachable: classes reaches the rail, the marker and the links, so the same component can be a dashed outline with a thick marker and no heading at all.

Summary

Detail

Notes

<script lang="ts">	import { Toc } from 'omaris';​	const items = [		{ id: 'ov-summary', text: 'Summary', level: 2 },		{ id: 'ov-detail', text: 'Detail', level: 3 },		{ id: 'ov-notes', text: 'Notes', level: 3 }	];</script>​<div class="flex items-start gap-10">	<Toc		{items}		spy={false}		active="ov-detail"		showLabel={false}		size="sm"		class="w-44 max-w-full"		classes={{			rail: 'border-dashed border-primary/40',			marker: 'w-1 rounded-none',			link: 'py-2 tracking-wide uppercase',			item: 'border-b border-border/60 last:border-b-0'		}}	/>​	<div class="flex flex-col gap-2">		{#each items as item (item.id)}			<p id={item.id} class="text-body-sm text-muted-foreground">{item.text}</p>		{/each}	</div></div>