Scroll TOC

Before the session

Joni Mitchell tuned every guitar to its own open tuning, so the first hour of a session was spent on strings rather than songs.

Engineers learned to leave that hour alone. It set the room, the tempo of the day and which instruments would be in reach.

Choosing the room

A dead room keeps the voice close. A live one lets the guitar ring into the corners and come back a little late, which is its own kind of harmony.

Microphones

One ribbon above the sound hole, one condenser near the twelfth fret, and a single valve microphone for the voice, set a hand apart from the lips.

Tracking

Most takes were full performances. Stopping to punch in a line broke the thread of the tuning and the song together, so the band played through.

When a take was close, the next one was usually worse. The rule became three passes, then a walk around the block.

Headphone mixes

Every player had their own mix, and every mix had more of the voice than anyone admitted in the control room.

Mixing

The mix was a matter of taking things away. Reverb was the room itself, and anything added later had to sound as if it had always been there.

Tom Waits once described a good mix as the moment you stop hearing the speakers. That was the only test that mattered.

Tape and air

Half-inch tape at thirty inches per second kept the top end open. The hiss was part of the record, like the paper under a drawing.

Afterwards

The reels went into a cupboard labelled with the date and a single word for the mood of the day. Years later, those words were still right.

Some of the best songs on the record were the ones nobody planned to keep.

About this component

Scroll TOC

A table of contents that follows the reader: the current section is the last heading past a line near the top (the last one once the page bottoms out, the topmost in view before any has passed), and a marker slides and resizes onto it along a thin rail. Entries come from an items list, from headings collected out of a container, or from the content passed as children, which lays the list and its own scroll area side by side. Clicks scroll smoothly, respect scroll-margin, move focus to the heading and hold the marker steady while the page travels; deeper levels indent and can collapse to the current section. Works on the page or inside any scroll container.

Usage

The import and the props worth knowing about, in one place.

import { ScrollToc } from "@/components/beste/component/scroll-toc";

// Beside an article on the page: headings are collected from it
<aside className="sticky top-24">
  <ScrollToc containerSelector="article" offset={96} />
</aside>

// Explicit entries, inside a scroll container
<ScrollToc
  root="#docs-scroller"
  items={[
    { id: "install", title: "Install", level: 2 },
    { id: "props", title: "Props", level: 2 },
    { id: "events", title: "Events", level: 3 },
  ]}
  collapse          // deeper levels only under the current section
  updateHash
/>

// Content and contents together, in one scroll area
<ScrollToc className="h-[32rem]">
  <article>...</article>
</ScrollToc>

Keyboard and gestures

Every one of these is in the component already. They are listed because a props table cannot mention a gesture, so nothing else on this page can tell you they exist.

Scroll the articleThe marker slides to the section whose heading last passed the line near the top.
Click an entryScrolls to its heading and moves focus there.
TabMoves through the entries; collapsed ones are skipped.

Props

Read from the component's own type, so this cannot drift from what it accepts.

PropTypeDefaultDescription
itemsScrollTocItem[]—The entries. Left out, headings are collected from `containerSelector`, or from `children`.
containerSelectorstring—Where to collect headings from when there are no `items`, e.g. "article".
headingSelectorstring—Which headings to collect. @defaultValue "h2, h3"
rootstring | HTMLElement | React.RefObject<HTMLElement | null> | null—The element that scrolls: a selector, an element or a ref. Left out, the page. With `children` it is the built-in scroll area.
offsetnumber96Distance from the top of the scroll area, in pixels, where a heading becomes the current one. Headings without a `scroll-margin-top` land just above it on click.
titlestring—Heading above the list. Pass an empty string to drop it. @defaultValue "On this page"
collapseboolean—Hide deeper levels except under the current section. @defaultValue false
smoothboolean—Smooth scrolling on click, dropped for reduced motion. @defaultValue true
updateHashboolean—Write the heading's id to the address bar on click. @defaultValue false
onActiveChange(id: string | null) => void—
childrenReact.ReactNode—Content to read. When given, the list and a scroll area holding this content are laid out side by side and the headings come from it. Give the root a height.
tone"muted" | "outline" | "ghost""ghost"
size"sm" | "default" | "lg""default"
classNamestring—
aria-labelstring—