Noctis

Table of contents

An "On this page" sidebar for long documents. Give Toc.Root a flat list of headings and it draws a depth-indented list of anchor links over a curved spine, sliding an accent marker to the heading currently in view as the reader scrolls. Toc.Footer adds an optional column of page-action rows.

Basic

Toc.Root takes the items — a flat { title, url, depth }[], the shape every markdown-TOC pipeline already emits. Inside it, Toc.Header is the "On this page" label and Toc.Items is the engine that renders the links and tracks the active heading as the reader scrolls. Every heading currently in view is highlighted, so the marker spans the whole on-screen range rather than snapping to one line.

Nested headings

Depth is purely visual: a deeper depth indents the row and steps the spine one level right, drawing a small diagonal connector at each change. The line clamps at one level of nesting (so it never marches off to the right) while the text keeps indenting. The accent marker is masked to the spine, so the active stretch of the tree lights up.

In a layout

The real shape: a long article with a sticky table of contents beside it. By default the active marker tracks against the window as the page scrolls. When the document scrolls inside its own container instead — a height-capped panel, a dialog body — pass that container's ref as scrollRoot and tracking observes it rather than the viewport (the example below is capped so it scrolls in place). The sticky positioning and the column width belong to your layout, not the component.

Installation

Add @stridge/noctis to your app and import the framework-neutral stylesheet once at the root. There's no build step and no styling framework to adopt.

Provider

Wrap your tree in NoctisProvider — it seeds the theme, locale, and direction every component reads from, so a single root configures the whole interface.

Theming

Noctis derives every surface from one seed through the OKLCH theme engine, so a single accent retunes the entire interface without touching component code.

Tokens

Semantic tokens — background, foreground, accent, border — are the only values you reference; the primitive ramp stays private to the engine.

Theme engine

The engine resolves contrast per mode, lifting surfaces toward white as they elevate and holding text legible at any seed.

Dark mode

Dark is the default; light mode is derived from the same seed rather than maintained as a second palette.

Components

Every component ships from its own deep import — @stridge/noctis/button, @stridge/noctis/dialog — as precompiled, framework-neutral CSS keyed off data-slot.

Internationalization

Strings, direction, and number formatting flow through @stridge/noctis-intl, so right-to-left and locale support are structural rather than bolted on.

Motion

Restrained, named motion scales drive every transition, and each one respects prefers-reduced-motion out of the box.

Accessibility

Keyboard operation, focus rings, and ARIA are built into every primitive — the documentation site dogfoods the same components it describes.

Colour

color sets the active link and marker fill: primary (the default — the neutral white signal that carries most of the interface) or the vivid accent held for emphasis. Everything else stays muted either way, so the active heading is the only thing that pops.

Page actions

Toc.Footer is a generic, composable region — a labelled <nav> holding a column of Toc.Action rows. Each action is a ghost button by default, or an anchor via render; place a leading glyph and a <span> label inside and wire the behaviour (edit, copy, scroll-to-top, feedback…) to your app.

Controlled active state

By default the active heading is tracked automatically. Pass activeIds to control it instead — the built-in observer steps aside — and onActiveChange to observe the set the observer would compute. activeMode chooses between highlighting the whole on-screen range (multi, the default) or clamping it to the topmost heading in view (single).

Accessibility

Toc.Root is a <nav> landmark named "On this page" (localized; pass aria-label to override). The links are real in-page anchors, so they are keyboard reachable and carry native hash history; the active link is marked data-active (a styling hook, not an ARIA state). The spine, its connectors, and the moving marker are decorative (aria-hidden / role="none"). Toc.Footer is a second landmark named "Page actions", and each Toc.Action is a real <button>/<a> with a focus-visible ring. The whole component mirrors under RTL by construction.

Anatomy

Compose the table of contents from its parts. Toc.Root owns the data; Toc.Items owns the rendering and the runtime engine.

  • Toc.Root — the <nav> landmark; takes items, activeMode, color, the optional scrollRoot, and the optional controlled activeIds/onActiveChange.
  • Toc.Header — the "On this page" heading; override via children.
  • Toc.Items — the scrolling viewport that draws the spine, the moving marker, and the link rows, and tracks the active heading.
  • Toc.Footer — the optional page-actions region (a labelled <nav>).
  • Toc.Action — one page-action row; a ghost <button> by default, composable onto an <a> via render.

Every rendered part carries a data-slot (noctis-toc on the root, noctis-toc-link on a link, and so on) for host-side styling. The link's depth indent, the spine mask, and the marker position are runtime values written as inline style, since they depend on measured layout.

On surfaces

The same table of contents re-tuned across the elevation scopes — the root canvas, an elevated panel, a menu, and a sunken well. It stays legible on every layer.

Design tokens

Generated from the component's declaration — the same graph that mints the CSS, so a variable name or its resolution default can't drift. The minted tokens are the public override seam: set one on any ancestor and every table of contents in that region retunes — e.g. .docs { --noctis-toc-link-padding-block: var(--noctis-space-3); } opens up the rows. Colours aren't minted — the link reads the muted/foreground roles directly, the spine reads border, and the active link + marker read primary/accent keyed off the color axis — so a retheme propagates for free. See Customization for the full override ladder and Tokens for the whole graph.

Token

API reference

Generated from the component's types — every prop, type, default, and description comes straight from the source. Each part gets its own table. Expand a row for the full type and description.

Toc.Root

Prop

Toc.Header

Prop

Toc.Items

Prop

Toc.Footer

Prop

Toc.Action

Prop

AttributeDescription
data-slotThe slot marker on every rendered part (`noctis-toc`, `noctis-toc-link`, …).
data-colorThe active-fill colour axis — `primary` (default) | `accent`, stamped on the root; the active link + thumb key off it.
data-activePresent as `"true"` on the active link (and `"false"` otherwise); `toc.css` lifts it to the active colour.
aria-currentSet to `"location"` on the active link so assistive tech announces it as the current section.
data-depthThe heading depth of a link (2 = h2, 3 = h3, …), stamped on `Toc.Link` so the spine geometry can be derived.
data-trim-startPresent on a link's spine `marker` when the depth differs from the link above — trims the divider's top to meet the diagonal.
data-trim-endPresent on a link's spine `marker` when the depth differs from the link below — trims the divider's bottom to meet the diagonal.