Table of contents

An "On this page" list of links that marks the section the reader has scrolled to.

<tp-table-of-contents>
Table of contents
Default density Compact density Restart from the initial state Show the copyable source

Overview

Northwind Sync keeps customer records in your CRM and your billing system in step. Every change on either side becomes an event, and the sync service applies it to the other side within a few seconds.

This guide walks through connecting both systems, choosing which fields to map, and rolling the integration out to your whole team without interrupting billing.

Connect accounts

Open Settings → Integrations and choose Northwind Sync. You will be asked to sign in to both systems with an administrator account; the service stores only scoped tokens, never passwords.

Once both accounts are connected, a test record is created and deleted on each side to confirm permissions. If the check fails, the most common cause is a CRM role without API access.

Field mapping

Standard fields such as company name, billing email and tax ID are mapped automatically. Custom fields appear in a second list where you choose a target and a direction.

Use one-way mappings for values that have a single owner, such as plan tier from billing. Two-way mappings resolve conflicts by keeping the most recent edit.

Rollout

Start with a single sales region. The sync dashboard shows applied, skipped and failed events per hour, so you can confirm the mapping before widening it.

When the error rate stays under one percent for a week, enable the remaining regions. Historical records are backfilled overnight in batches of five thousand.

Troubleshooting

Failed events are retried with exponential backoff for up to 24 hours. After that they move to the review queue, where you can edit the payload and replay it.

If records drift apart, run a reconciliation from the dashboard. It compares both systems field by field and lists every difference before changing anything.

Usage

Each item names its target with href="#id"; the component never scans the page for headings. It finds the scroll container that holds every target, the document or a Scroll area as here, and marks the innermost target whose region crosses the reading line. Place it in a sticky column inside that same container.

When the list is longer than its column, the current link is kept in view inside the column only.

An item names one element by href="#id", resolved in the item’s own tree scope and then the document, or by the target property, which takes an element reference (useful inside shadow roots or for elements without ids). Headings, sections, cards and figures all work the same way. Hidden targets are skipped until they show again; unresolved targets raise a tp-diagnostic.

Set scroll-root (an id) or scrollRoot (an element; a component with a viewportElement, such as Scroll area, resolves to its viewport) to choose it explicitly.

Activating an item scrolls only the content’s container and keeps that item current until the reader scrolls again. The default navigation="fragment" also records the target in the URL as a history entry, so Back returns to the previous section. Use navigation="scroll", as these examples do, when your application routes on the fragment or the list sits in an embedded panel.

scroll-behavior chooses smooth (default), instant, or auto (the container’s CSS scroll-behavior); reduced motion is always instant. Focus does not move.

Links in the content work like the items: a heading’s own # anchor or a cross-reference such as <a href="#theming"> scrolls the same way and makes its target current. Back and forward through history and a fragment present at load do the same.

Depth and title

Nested items
Default density Compact density Restart from the initial state Show the copyable source
Overview Connect accounts Field mapping Rollout Troubleshooting

depth only indents an item; it is never used for tracking. label replaces the default “On this page” title, and a title slot accepts richer content. This list follows the article above: scroll it and both lists update.

Nested sections

To keep a group current while the reader is inside one of its subsections, make the group a section that wraps them and point its item at the section. The indicator then spans the group and the subsection together. This list sits in the leading column and also sets scroll-behavior="instant", so items jump without animation, and scroll-debounce="120", so the current item settles only once scrolling pauses.

Nested sections
Default density Compact density Restart from the initial state Show the copyable source

People

Everyone in the workspace has one account, shared across every project they can reach. Owners manage membership from Settings → People.

Invitations

Invite by email or share a join link limited to your company domain. Pending invitations expire after fourteen days and can be resent at any time.

Roles

Members edit the projects they belong to, admins also manage people and integrations, and owners control billing. A workspace always keeps at least one owner.

Billing

Plans are billed per active member each month. Members who have not signed in for thirty days stop counting until they return.

Invoices

Invoices are emailed to the billing contact on the first of the month and stay available as PDFs under Billing → History.

Tax details

Add a VAT or GST number before the next invoice is issued. Changes are not applied to invoices that were already sent.

Offboarding

Removing a member transfers their private projects to the owner who removed them. Their comments and history stay in place.

Depth still only indents. The grouping comes from the sections themselves, so a flat list over the same page tracks the same way.

Targets are ordered by position. Each one covers from its start to the next target’s start, or to its own end when that is further. A heading therefore covers the text below it, a section covers itself, and a section that contains other targets contains their regions, so the containing item stays active and the indicator spans the chain.

Building items from your content

collectTargets(root, selector, options) from @tweakpad/ui returns a record for each element your selector matches; render one item per record and give it the element as target. Targets need no ids and need not be headings. Run it again when the content changes, as the button does here.

Collected targets
Default density Compact density Restart from the initial state Show the copyable source
Create a project

Start from a blank board or a template. Templates bring their own columns, labels and a few example tasks you can delete.

Import tasks

Upload a CSV from your previous tool. Columns are matched by name, and anything unmatched becomes a custom field you can rename later.

Set up automations

Move a task to Review when its pull request opens, or assign a reviewer when a due date is two days away.

It has no default selector, assigns no ids and returns a depth only when you supply a depth function.

import { collectTargets } from '@tweakpad/ui'; const toc = document.querySelector('tp-table-of-contents'); toc.replaceChildren( ...collectTargets(document.querySelector('main'), 'h2, h3', { depth: (heading) => (heading.tagName === 'H3' ? 2 : 1), }).map(({ element, label, depth }) => { const item = document.createElement('tp-table-of-contents-item'); item.target = element; item.depth = depth ?? 1; item.textContent = label; return item; }), );

The heading selector and depth rule are the page’s choice here, not the library’s.

Compact

variant="compact" draws less attention: one short line per top-level item, with the current one marked. Hovering the lines opens the full list in a card after open-delay, and close-delay keeps it open briefly after the pointer leaves; the lines are also one Button for keyboard users. side is the direction the card grows: inline-start for lines on the end edge of the page, inline-end for lines on the start edge.

Compact
Default density Compact density Restart from the initial state Show the copyable source

Highlights

Version 4.2 adds shared workspaces, a faster search index and a new export pipeline. Existing projects upgrade automatically the next time they open.

Shared workspaces

Invite collaborators to a workspace instead of to each project. Permissions follow the workspace, and owners can still restrict a single project to a smaller group.

Results now arrive as you type, ranked by recent activity. Filters for owner, type and date can be combined and saved.

Export

Exports run in the background and notify you when they finish. PDF exports keep links and the document outline.

A deeper item counts toward the line before it, so a section’s line stays highlighted while you read its subsections. The current section’s line takes the indicator color, another active section’s line the muted foreground, and the rest the border color. Only the color changes, never the size of the strip.

Pressing the lines, or Enter or Space while they have focus, toggles the card and moves focus to the current link. Escape closes the card and returns focus to the lines. Activating a link scrolls as in the list and closes the card.

The card covers the lines, aligned with their outer edge and centered on them. When the card does not fit on that side it grows the other way, and it shifts to stay in the viewport. A card taller than the viewport scrolls and keeps the current link in view.

Reading line

The reading line defaults to the scroll container’s scroll-padding-block-start, the same CSS you already use to clear a sticky header for anchor links, so a jump always lands on the item it marks. activation-offset overrides it with pixels or a percentage. For busy pages, scroll-throttle and scroll-debounce reduce how often the current item updates.

Fragment navigation lands targets exactly there, after their own scroll-margin-block-start, so a jump always makes its target current.

html { scroll-padding-block-start: 4rem; } /* a sticky header */

There is always a current item. At the top, and whenever the reading line is still above the first target (an introduction, padding, or overscroll), the first target is current. Over the last screen of scrolling the reading line moves down to the end edge, so short final sections still become current in order and the last is current at the bottom, including overscroll.

Performance

Many components can react to the same scrolling without adding listeners. Scroll observation is a shared service: each scroll container has one native listener per event type, the window one ResizeObserver, and each observed root one MutationObserver, however many components subscribe (observeScroll, observeResize, observeSubtree from @tweakpad/ui).

Targets are measured only when layout changes: their size, the container’s size or scroll height, or the DOM under the container. A scroll alone recomputes from the scroll position, with no layout reads of the targets.

Each subscriber chooses its timing. The table of contents updates once per animation frame by default; scroll-throttle limits updates to one per interval (with a trailing update), and scroll-debounce updates only after scrolling pauses.

Accessibility

The root is one nav landmark named by its title, and the items form a list. Only the current link carries aria-current="location". The rail and indicator are hidden from assistive technology. Scrolling never moves focus or announces anything.

In the compact variant the landmark is named by the title text and holds a button with the same name that reports whether the card is expanded. The lines are hidden from assistive technology. The card is a dialog named by the title and holds the same list; its links are in the focus order while it is open.

API Reference

Every element also accepts motion-policy.

Attributes

AttributePropertyTypeDefaultDescriptionvaluevaluestring | null—The current target's identifier; setting it (even to null) controls the component.default-valuedefaultValuestring | nullnullThe target that is current before the first measurement.scroll-rootscrollRootIdstring | nullnullThe id of the scroll container to observe, resolved in this tree scope, then the document.activation-offsetactivationOffsetstring | number | nullnullThe reading line, in pixels or a percentage of the scroll container; without it, the container's scroll-padding-block-start.navigationnavigation'fragment' | 'scroll''fragment'fragment uses native fragment navigation; scroll leaves the URL untouched.scroll-behaviorscrollBehaviorScrollBehavior'smooth'How the content scrolls when navigating; auto follows the scroll root's CSS.scroll-throttlescrollThrottlenumber0Milliseconds between scroll updates; 0 updates once per animation frame.scroll-debouncescrollDebouncenumber0Milliseconds of quiet scrolling before an update; wins over scrollThrottle.labellabelstring''The visible title; the title slot replaces it, and without either the title message shows.variantvariant'list' | 'compact''list'list shows the title and list in place; compact shows marks that open them in a card.sideside'inline-start' | 'inline-end''inline-start'Compact: the side of the trigger the card grows toward; it flips when it does not fit.open-delayopenDelaynumber300Compact: hover delay before the card opens, in milliseconds.close-delaycloseDelaynumber0Compact: delay before the card closes after the pointer leaves, in milliseconds.

Properties

PropertyTypeDefaultDescriptionscrollRootElement | nullnullThe scroll container to observe, when it is not the nearest one holding every target; a Scroll area resolves to its viewport.messagesTableOfContentsMessages{}Replaces built-in text, such as the default "On this page" title.onValueChange((event: TpValueChangeEvent<string | null>) => void) | undefined—Called with each value proposal.activeValuesread-onlyreadonly string[]—Identifiers of the targets whose regions contain the reading line, by position.resolvedScrollRootread-onlyHTMLElement | null—The resolved scroll root the spy observes.tocMessagesread-onlyRequired<TableOfContentsMessages>—The text in effect: messages over the built-in defaults.

Events

EventTypeDescriptiontp-value-changeTpValueChangeEvent<string | null>Non-cancelable value proposal (scroll, link-press).

Methods

MethodReturnsDescriptionnavigate(value)voidScrolls to an item's target and makes it current, as activating its link does.refresh()voidRecalculates after changes the spy cannot observe, such as transforms.

Slots

SlotDescriptiondefaulttp-table-of-contents-item elements.titleTitle content; replaces label and the title message.

CSS parts

PartDescription::part(table-of-contents)The nav landmark.::part(title)The visible name.::part(list)The list carrying the rail.::part(rail)The decorative line on the inline-start edge.::part(indicator)The mark spanning the active items.::part(trigger)Compact: the composed Button opening the popover.::part(marks)Compact: the decorative column of marks.::part(mark)Compact: one top-level item's mark; data-active, data-current.::part(popover)Compact: the composed Popover holding the title and list.

<tp-table-of-contents-item>

Attributes

AttributePropertyTypeDefaultDescriptionhrefhrefstring | nullnullThe target as a fragment link, such as #install, resolved in this item's tree scope, then the document.depthdepthnumber1The indentation level, from 1; it does not change which item is current.

Properties

PropertyTypeDefaultDescriptiontargetElement | nullnullThe target as an element reference, for elements without ids or inside shadow roots; wins over href.activebooleanfalseSet by the table of contents: the target's region contains the reading line.currentbooleanfalseSet by the table of contents: the innermost active target.resolvedTargetread-onlyElement | null—The target element, resolved in this item's tree scope first, then its document.fragmentread-onlystring | null—The decoded fragment of href, or null when it names no fragment.keyread-onlystring—The value this item publishes: the target's identifier, or a stable generated one.linkElementread-onlyHTMLAnchorElement | null—The rendered link, once rendered.

Events

EventTypeDescriptiontp-table-of-contents-item-changeEventBubbles when depth changes, so the table of contents redraws its compact marks.

Slots

SlotDescriptiondefaultThe link label.

CSS parts

PartDescription::part(link)The native link; data-active, data-current.