Skip to content

TOC Active Highlight

A copy-paste scroll-driven component in pure HTML & CSS. Zero dependencies, framework-agnostic, MIT-licensed.

Scroll-DrivenHTMLCSSany framework

Intro section content fills this space as you scroll.

Specs section sits further down the scroller.

Notes section is near the bottom of the panel.

Copy into your project

HTML
<!-- Nav dots light up as their matching section scrolls into view -->
<!-- Uses view-timeline-name on each section + timeline-scope on the
     shared ancestor so a nav dot outside the section can reference it -->
<nav class="nuda-sd2-toc__nav">
  <span class="nuda-sd2-toc__item">Intro<span class="nuda-sd2-toc__dot nuda-sd2-toc__dot--1"></span></span>
  <span class="nuda-sd2-toc__item">Specs<span class="nuda-sd2-toc__dot nuda-sd2-toc__dot--2"></span></span>
  <span class="nuda-sd2-toc__item">Notes<span class="nuda-sd2-toc__dot nuda-sd2-toc__dot--3"></span></span>
</nav>
<section class="nuda-sd2-toc__section nuda-sd2-toc__section--1">…</section>
<section class="nuda-sd2-toc__section nuda-sd2-toc__section--2">…</section>
<section class="nuda-sd2-toc__section nuda-sd2-toc__section--3">…</section>
CSS
.nuda-sd2-toc {
  height: 160px;
  border: 1px solid rgba(255,255,255,.08);
  border-radius: 10px;
  background: #09090b;
  overflow: hidden;
}

.nuda-sd2-toc__scroller {
  height: 100%;
  overflow: auto;
  timeline-scope: --sd2sec1,--sd2sec2,--sd2sec3;
}

.nuda-sd2-toc__nav {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  gap: .3rem;
  padding: .5rem .4rem;
  background: #09090b;
  border-bottom: 1px solid rgba(255,255,255,.08);
}

.nuda-sd2-toc__item {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .3rem;
  color: #a1a1aa;
  font-size: .62rem;
  text-transform: uppercase;
  letter-spacing: .04em;
}

.nuda-sd2-toc__dot {
  width: 16px;
  height: 3px;
  border-radius: 2px;
  background: #e4ff54;
  opacity: 0;
  transform: scaleX(.3);
  animation-timing-function: linear;
  animation-fill-mode: both;
}

.nuda-sd2-toc__dot--1 {
  animation-name: nuda-sd2-toc-dot;
  animation-timeline: --sd2sec1;
}

.nuda-sd2-toc__dot--2 {
  animation-name: nuda-sd2-toc-dot;
  animation-timeline: --sd2sec2;
}

.nuda-sd2-toc__dot--3 {
  animation-name: nuda-sd2-toc-dot;
  animation-timeline: --sd2sec3;
}

.nuda-sd2-toc__section {
  padding: 1.6rem 1rem;
  color: #a1a1aa;
  font-size: .75rem;
  line-height: 1.6;
}

.nuda-sd2-toc__section p {
  margin: 0;
}

.nuda-sd2-toc__section--1 {
  view-timeline-name: --sd2sec1;
}

.nuda-sd2-toc__section--2 {
  view-timeline-name: --sd2sec2;
}

.nuda-sd2-toc__section--3 {
  view-timeline-name: --sd2sec3;
}

@keyframes nuda-sd2-toc-dot {
  0%,100% {
    opacity: 0;
    transform: scaleX(.3);
  }
  40%,60% {
    opacity: 1;
    transform: scaleX(1);
  }
}

@supports not (animation-timeline:view()) {
  .nuda-sd2-toc__dot--1 {
    opacity: 1;
    transform: scaleX(1);
  }
}

@media (prefers-reduced-motion:reduce) {
  .nuda-sd2-toc__dot {
    animation: none;
    animation-timeline: none;
    opacity: 0;
    transform: scaleX(.3);
  }
  .nuda-sd2-toc__dot--1 {
    opacity: 1;
    transform: scaleX(1);
  }
}

How to use TOC Active Highlight

Paste the HTML where you need it and the CSS into a global stylesheet (or a <style> tag). Every class is prefixed nuda- so it never collides with Tailwind or your own styles. Tweak the CSS custom properties to match your design system.

Works in React, Vue, Svelte, Astro, Next.js, Nuxt, Laravel Blade, Django, Rails — or a single .html file. No npm install, no build step.

More scroll-driven components

← Browse all NudaUI components