Scroll Spy
Tracks scroll position and highlights the active navigation link.
<arc-scroll-spy> Overview
>
ScrollSpy is a sticky table-of-contents component that watches the viewport and highlights whichever navigation link corresponds to the currently visible section. It uses an IntersectionObserver under the hood, so tracking is efficient and does not block the main thread even on long pages with dozens of sections. Place it in a sidebar column next to your content and it handles the rest.
Each entry in the table of contents is declared with an `<arc-spy-link>` child element whose `target` attribute matches the `id` of a section on the page. ScrollSpy reads these declarative links from its slot, builds the IntersectionObserver, and renders a compact navigation list in its shadow DOM with an "On this page" heading. When the user scrolls a target section into view, the corresponding link receives an `aria-current="true"` attribute and a blue accent background highlight.
Clicking a link triggers a smooth scroll to the target element and immediately updates the active state. The component dispatches an `arc-change` custom event with the active section ID whenever the highlighted link changes, so you can synchronize other UI (like a progress bar or breadcrumb) with the current reading position. The `offset` prop lets you fine-tune the scroll detection threshold to account for sticky headers of varying heights.Guidelines
When to use
- Place ScrollSpy in a sidebar-right or sticky aside column next to the scrollable content
- Give every target section a unique id attribute that matches the spy-link target
- Set the offset prop to match the height of your sticky header or TopBar
- Listen for the arc-change event to synchronize breadcrumbs, analytics, or URL hash updates
- Keep spy-link labels short — they should match or abbreviate section headings
When not to use
- Do not use ScrollSpy for primary site navigation — it is for in-page section tracking only
- Do not forget to import arc-spy-link; ScrollSpy depends on it to collect its link definitions
- Do not place ScrollSpy inside a scrollable container other than the document — the observer watches document-level intersections
- Do not add dozens of spy-links to a single ScrollSpy; more than 10-12 links make the list hard to scan
- Do not omit the target attribute on spy-links — they will be silently ignored by the observer
Features
- IntersectionObserver-based scroll tracking with zero scroll-event overhead
- Declarative link registration via <arc-spy-link target="id"> children
- Sticky positioning with automatic height capping to prevent overflow
- Smooth-scroll click navigation to target sections
- Active link highlighting with accent-primary background and aria-current="true"
- Configurable offset prop to account for sticky headers of varying heights
- `arc-change` custom event dispatched when the active section changes
- Thin scrollbar styling for long tables of contents
Preview
Usage
This component requires JavaScript. No pure HTML/CSS version is available — use the Web Component directly or a framework wrapper.
<arc-scroll-spy>
<arc-spy-link target="section-1">Section 1</arc-spy-link>
<arc-spy-link target="section-2">Section 2</arc-spy-link>
</arc-scroll-spy> import { ScrollSpy, SpyLink } from '@arclux/arc-ui-react';
export default function Example() {
return (
<ScrollSpy>
<SpyLink target="section-1">Section 1</SpyLink>
<SpyLink target="section-2">Section 2</SpyLink>
</ScrollSpy>
);
} <script setup>
import { ScrollSpy, SpyLink } from '@arclux/arc-ui-vue';
</script>
<template>
<ScrollSpy>
<SpyLink target="section-1">Section 1</SpyLink>
<SpyLink target="section-2">Section 2</SpyLink>
</ScrollSpy>
</template> <script>
import { ScrollSpy, SpyLink } from '@arclux/arc-ui-svelte';
</script>
<ScrollSpy>
<SpyLink target="section-1">Section 1</SpyLink>
<SpyLink target="section-2">Section 2</SpyLink>
</ScrollSpy> import { Component } from '@angular/core';
import { ScrollSpy, SpyLink } from '@arclux/arc-ui-angular';
@Component({
imports: [ScrollSpy, SpyLink],
template: `
<arc-scroll-spy>
<arc-spy-link target="section-1">Section 1</arc-spy-link>
<arc-spy-link target="section-2">Section 2</arc-spy-link>
</arc-scroll-spy>
`,
})
export class MyComponent {} import { ScrollSpy, SpyLink } from '@arclux/arc-ui-solid';
export default function Example() {
return (
<ScrollSpy>
<SpyLink target="section-1">Section 1</SpyLink>
<SpyLink target="section-2">Section 2</SpyLink>
</ScrollSpy>
);
} import { ScrollSpy, SpyLink } from '@arclux/arc-ui-preact';
export default function Example() {
return (
<ScrollSpy>
<SpyLink target="section-1">Section 1</SpyLink>
<SpyLink target="section-2">Section 2</SpyLink>
</ScrollSpy>
);
} API
-
ringRadiusnumber6 - Ring geometry. r drives the dasharray, so the two cannot drift apart.
-
activestring'' - The id of the currently active section. Reflects to an attribute and updates automatically as the user scrolls.
-
offsetnumber80 - Pixel distance from the top of the viewport at which a section counts as current. Increase it to account for taller sticky headers.
-
progress'none' | 'ring' | 'read' | 'both''none' - How the reader's position through the document is shown. `ring` draws a progress arc beside the heading; `read` recedes the entries already scrolled past; `both` does each. Defaults to `none`, and an unrecognized value lands there too.
Events
-
arc-change - Fired when the active spy target changes during scroll
SpyLink
<arc-spy-link> Navigation anchor that highlights when its target section is in view.
-
label -
targetstring'' - ID of the section to observe
-
levelnumber0 - Nesting depth for visual indentation. Level 0 links render at default size; level 1+ links are indented and use a smaller font size.
See Also
- Scroll To Top Floating button that appears after scrolling and smoothly returns the user to the top of the page.
- Tabs Tabbed content navigation with keyboard support and ARIA roles.
- Navigation Menu Horizontal navigation bar with hover-triggered dropdown sub-menus and full keyboard accessibility. Designed for marketing sites, documentation hubs, and product landing pages where top-level sections expand into categorised link lists.