Components / Layout & Navigation / Scrollspy

Scrollspy stable

A contents list that watches the page: the section you are reading is the one it lights.

Preview

Installation

Load the tokens once, then copy the part. Nothing in the catalog needs a build step, a framework or a package manager — the files are the product.

Tokens

Every color, radius and motion curve is a custom property. Parts read the tokens; they never name a color of their own, which is what makes themes a one-file change.

Parts

A part is a folder: the markup, the CSS, and the small script if it has one. The page for each part shows all three, ready to copy.

Blocks

Blocks are parts already assembled — a login, a pricing table, an app shell. Take the whole block, then take it apart with the catalog.

Themes

A theme moves the accent only. Neutrals, spacing and the vermillion core never change, so a rebrand cannot quietly become a redesign.

<div class="k-spy" data-spy>
  <nav aria-label="On this page">
    <a href="#spy-install" class="on" aria-current="location">Installation</a>
    <a href="#spy-tokens">Tokens</a>
    <a href="#spy-parts">Parts</a>
    <a href="#spy-blocks">Blocks</a>
    <a href="#spy-themes">Themes</a>
  </nav>
  <div class="k-spy-body">
    <section id="spy-install"><h4>Installation</h4><p>Load the tokens once, then copy the part. Nothing in the catalog needs a build step, a framework or a package manager — the files are the product.</p></section>
    <section id="spy-tokens"><h4>Tokens</h4><p>Every color, radius and motion curve is a custom property. Parts read the tokens; they never name a color of their own, which is what makes themes a one-file change.</p></section>
    <section id="spy-parts"><h4>Parts</h4><p>A part is a folder: the markup, the CSS, and the small script if it has one. The page for each part shows all three, ready to copy.</p></section>
    <section id="spy-blocks"><h4>Blocks</h4><p>Blocks are parts already assembled — a login, a pricing table, an app shell. Take the whole block, then take it apart with the catalog.</p></section>
    <section id="spy-themes"><h4>Themes</h4><p>A theme moves the accent only. Neutrals, spacing and the vermillion core never change, so a rebrand cannot quietly become a redesign.</p></section>
  </div>
</div>
/* KATA Scrollspy — the contents list that keeps its place. */
.k-spy{width:100%;display:grid;grid-template-columns:168px 1fr;gap:22px;font-family:var(--k-font);color:var(--k-ink)}
.k-spy nav{display:flex;flex-direction:column;gap:2px;align-self:start}
.k-spy nav a{font-size:13px;color:var(--k-ink-2);text-decoration:none;padding:6px 10px;border-radius:7px;border-inline-start:2px solid var(--k-line)}
.k-spy nav a:hover{color:var(--k-ink);background:var(--k-bg-soft)}
.k-spy nav a.on{color:var(--k-ink);font-weight:600;border-inline-start-color:var(--k-core);background:var(--k-bg-soft)}
.k-spy-body{height:264px;overflow:auto;border:1px solid var(--k-line);border-radius:12px;padding:16px 18px;scroll-behavior:smooth}
.k-spy-body section{margin-bottom:22px}
.k-spy-body h4{font-size:14px;font-weight:600;margin-bottom:5px}
.k-spy-body p{font-size:13px;line-height:1.8;color:var(--k-ink-2)}
@media (max-width:560px){.k-spy{grid-template-columns:1fr}.k-spy nav{flex-direction:row;flex-wrap:wrap}}
// KATA Scrollspy — IntersectionObserver against the real scroll container.
document.querySelectorAll('[data-spy]').forEach(function (root) {
  var body = root.querySelector('.k-spy-body');
  var links = {};
  root.querySelectorAll('nav a[href^="#"]').forEach(function (a) { links[a.getAttribute('href').slice(1)] = a; });
  var io = new IntersectionObserver(function (entries) {
    entries.forEach(function (en) {
      if (!en.isIntersecting) return;
      Object.values(links).forEach(function (a) { a.classList.remove('on'); a.removeAttribute('aria-current'); });
      var a = links[en.target.id];
      if (a) { a.classList.add('on'); a.setAttribute('aria-current', 'location'); }
    });
  }, { root: body, rootMargin: '0px 0px -62% 0px', threshold: 0 });
  body.querySelectorAll('section[id]').forEach(function (s) { io.observe(s); });
});

Installation

CLI

node kata-ui.mjs add scrollspy

Writes the part’s CSS and JS into your project. New to the CLI? See the CLI doc.

Manual

  1. Load tokens.css once, before any part.
  2. Copy the CSS tab into your stylesheet.
  3. Copy the HTML where it belongs. The part has a script — include it after the markup; it initializes itself.

Usage

For long pages with a table of contents — docs, guides, policies. The observer marks the section near the top of the viewport; clicking still uses plain anchors, so the back button and deep links keep working. In an app, the scroll container is whatever actually scrolls.

Accessibility

The links are plain anchors to real ids, and the current one carries aria-current="location" — the highlight is the same information for every reader, not a color alone.

Anatomy

The public surface of this part, read from its own source. Class names are the API — the CSS tab above is the full contract.

.k-spyclass
.onclass
.k-spy-bodyclass
data-spydata hook
aria-labelARIA
aria-currentARIA

Related parts