Components / Layout & Navigation / Scrollspy
Scrollspy stable
A contents list that watches the page: the section you are reading is the one it lights.
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
- Load
tokens.cssonce, before any part. - Copy the CSS tab into your stylesheet.
- 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-spy | class |
.on | class |
.k-spy-body | class |
data-spy | data hook |
aria-label | ARIA |
aria-current | ARIA |
Related parts
Accordion beta
Disclosure built on native <details>. Zero JavaScript, keyboard and find-in-page included.
Bottom Navigation stable
The mobile app tab bar: three to five destinations under the thumb, the current one lit with the core.
Breadcrumbs beta
Where you are, as a path. The last crumb is the page itself — not a link.
Collapsible stable
One disclosure on its own: a trigger row and the region it folds away. The Accordion is this, grouped.