Scroll Effects

Scroll Spy Dots

Fixed side dots that track the current section, jump smoothly on click, and expand a label.

An IntersectionObserver tracks how much of each section is visible; the current dot scales up, is marked with a flowing outer ring, and expands a label to the side, and clicking one smoothly scrolls to that section — a good fit for navigating long pages or sectioned scroll containers.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add scroll-spy-dots.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

Active edited usageDefault props
<ScrollSpyDots/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add https://webberui.com/r/scroll-spy-dots.en.json

Or, once registries are configured in components.json, install it as @webberui/scroll-spy-dots.

Usage

import { ScrollSpyDots } from "@/components/ui/scroll-spy-dots";

const sections = [
  { id: "story", label: "Our story" },
  { id: "features", label: "Core features" },
  { id: "contact", label: "Contact us" },
];

// The page must contain section elements with the matching ids
<div className="fixed right-6 top-1/2 -translate-y-1/2">
  <ScrollSpyDots sections={sections} />
</div>

If the sections live inside a custom scroll container, pass the container's ref to containerRef; both observation and jumping will then use that container as the scroll root:

const containerRef = React.useRef<HTMLDivElement>(null);

<div ref={containerRef} className="h-[400px] overflow-y-auto">
  {/* ...sections carrying ids... */}
</div>
<ScrollSpyDots sections={sections} containerRef={containerRef} />

Props

PropTypeDefaultDescription
sections{ id: string; label: string }[]—The list of sections to track; id matches a page element and label is the expanded label
containerRefReact.RefObject<HTMLElement | null>—Ref of a custom scroll container; when omitted, the browser window is the scroll root
labelSide"left" | "right""left"Direction the label expands in (relative to the dot)
alwaysShowLabelsbooleanfalseWhether every label stays visible; by default only the current one and hover/focus show a label
onActiveChange(id: string) => void—Callback fired when the current section changes
classNamestring—Forwarded to the outermost nav

Accessibility

  • Wrapped in a nav landmark labeled aria-label="Section navigation", so assistive technology can jump to it quickly
  • Every dot is a native button with the section label as its aria-label, and the current section is marked with aria-current
  • Keyboard support: Tab moves through them in order, Enter/Space jumps, and focusing also expands the label and shows a focus ring
  • When the user has "reduce motion" enabled at the system level, it switches to an instant jump (not smooth scrolling), and the outer ring, scale-up, and label animations are all disabled
  • The decorative dots, outer rings, and rail line are all marked aria-hidden so they do not interfere with reading