Navigation
Scrollspy
An “On this page” table of contents that marks the section being read.
| Class name | Type | Description |
|---|---|---|
scrollspyplf:scrollspy | Component | The table of contents landmark |
scrollspy-titleplf:scrollspy-title | Part | The heading |
scrollspy-listplf:scrollspy-list | Part | A list of links; nests for subsections |
scrollspy-itemplf:scrollspy-item | Part | A list item |
scrollspy-linkplf:scrollspy-link | Part | A link to a section |
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="scroll-area" style="--plf-scroll-area-height: 12rem">
<div class="scroll-area-viewport" id="scrollspy-default-viewport" role="region" tabindex="0" aria-label="Article">
<div class="scroll-area-content prose">
<h3 id="scrollspy-overview">Overview</h3>
<p>Content for the overview. It is long enough to scroll past before the next section starts.</p>
<h3 id="scrollspy-install">Install</h3>
<p>Content for the install section. It is long enough to scroll past before the next section starts.</p>
<h3 id="scrollspy-usage">Usage</h3>
<p>Content for the usage section. It is long enough to scroll past before the next section starts.</p>
<h3 id="scrollspy-faq">FAQ</h3>
<p>Content for the last section.</p>
</div>
</div>
</div>
<nav class="scrollspy" data-plf-scrollspy data-plf-container="scrollspy-default-viewport" aria-labelledby="scrollspy-default-title">
<p class="scrollspy-title" id="scrollspy-default-title">Contents</p>
<ul class="scrollspy-list">
<li class="scrollspy-item"><a class="scrollspy-link" href="#scrollspy-overview">Overview</a></li>
<li class="scrollspy-item"><a class="scrollspy-link" href="#scrollspy-install">Install</a></li>
<li class="scrollspy-item"><a class="scrollspy-link" href="#scrollspy-usage">Usage</a></li>
<li class="scrollspy-item"><a class="scrollspy-link" href="#scrollspy-faq">FAQ</a></li>
</ul>
</nav>
</div><div class="plf:grid plf:grid-cols-1 plf:sm:grid-cols-2 plf:gap-4">
<div class="plf:scroll-area" style="--plf-scroll-area-height: 12rem">
<div class="plf:scroll-area-viewport" id="scrollspy-default-viewport" role="region" tabindex="0" aria-label="Article">
<div class="plf:scroll-area-content plf:prose">
<h3 id="scrollspy-overview">Overview</h3>
<p>Content for the overview. It is long enough to scroll past before the next section starts.</p>
<h3 id="scrollspy-install">Install</h3>
<p>Content for the install section. It is long enough to scroll past before the next section starts.</p>
<h3 id="scrollspy-usage">Usage</h3>
<p>Content for the usage section. It is long enough to scroll past before the next section starts.</p>
<h3 id="scrollspy-faq">FAQ</h3>
<p>Content for the last section.</p>
</div>
</div>
</div>
<nav class="plf:scrollspy" data-plf-scrollspy data-plf-container="scrollspy-default-viewport" aria-labelledby="scrollspy-default-title">
<p class="plf:scrollspy-title" id="scrollspy-default-title">Contents</p>
<ul class="plf:scrollspy-list">
<li class="plf:scrollspy-item"><a class="plf:scrollspy-link" href="#scrollspy-overview">Overview</a></li>
<li class="plf:scrollspy-item"><a class="plf:scrollspy-link" href="#scrollspy-install">Install</a></li>
<li class="plf:scrollspy-item"><a class="plf:scrollspy-link" href="#scrollspy-usage">Usage</a></li>
<li class="plf:scrollspy-item"><a class="plf:scrollspy-link" href="#scrollspy-faq">FAQ</a></li>
</ul>
</nav>
</div>'use client';
import { Scrollspy, ScrollspyItem, ScrollspyLink, ScrollspyList, ScrollspyTitle } from '@pelatform/react';
import { useRef, type CSSProperties } from 'react';
export function Example() {
const viewport = useRef<HTMLDivElement>(null);
return (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="scroll-area" style={{ '--plf-scroll-area-height': '12rem' } as CSSProperties}>
<div className="scroll-area-viewport" ref={viewport} role="region" tabIndex={0} aria-label="Article">
<div className="scroll-area-content prose">
<h3 id="scrollspy-overview">Overview</h3>
<p>Content for the overview. It is long enough to scroll past before the next section starts.</p>
<h3 id="scrollspy-install">Install</h3>
<p>Content for the install section. It is long enough to scroll past before the next section starts.</p>
<h3 id="scrollspy-usage">Usage</h3>
<p>Content for the usage section. It is long enough to scroll past before the next section starts.</p>
<h3 id="scrollspy-faq">FAQ</h3>
<p>Content for the last section.</p>
</div>
</div>
</div>
<Scrollspy container={viewport} aria-labelledby="scrollspy-default-title">
<ScrollspyTitle id="scrollspy-default-title">Contents</ScrollspyTitle>
<ScrollspyList>
<ScrollspyItem><ScrollspyLink href="#scrollspy-overview">Overview</ScrollspyLink></ScrollspyItem>
<ScrollspyItem><ScrollspyLink href="#scrollspy-install">Install</ScrollspyLink></ScrollspyItem>
<ScrollspyItem><ScrollspyLink href="#scrollspy-usage">Usage</ScrollspyLink></ScrollspyItem>
<ScrollspyItem><ScrollspyLink href="#scrollspy-faq">FAQ</ScrollspyLink></ScrollspyItem>
</ScrollspyList>
</Scrollspy>
</div>
);
}'use client';
import { Scrollspy, ScrollspyItem, ScrollspyLink, ScrollspyList, ScrollspyTitle } from '@pelatform/react/prefixed';
import { useRef, type CSSProperties } from 'react';
export function Example() {
const viewport = useRef<HTMLDivElement>(null);
return (
<div className="plf:grid plf:grid-cols-1 plf:sm:grid-cols-2 plf:gap-4">
<div className="plf:scroll-area" style={{ '--plf-scroll-area-height': '12rem' } as CSSProperties}>
<div className="plf:scroll-area-viewport" ref={viewport} role="region" tabIndex={0} aria-label="Article">
<div className="plf:scroll-area-content plf:prose">
<h3 id="scrollspy-overview">Overview</h3>
<p>Content for the overview. It is long enough to scroll past before the next section starts.</p>
<h3 id="scrollspy-install">Install</h3>
<p>Content for the install section. It is long enough to scroll past before the next section starts.</p>
<h3 id="scrollspy-usage">Usage</h3>
<p>Content for the usage section. It is long enough to scroll past before the next section starts.</p>
<h3 id="scrollspy-faq">FAQ</h3>
<p>Content for the last section.</p>
</div>
</div>
</div>
<Scrollspy container={viewport} aria-labelledby="scrollspy-default-title">
<ScrollspyTitle id="scrollspy-default-title">Contents</ScrollspyTitle>
<ScrollspyList>
<ScrollspyItem><ScrollspyLink href="#scrollspy-overview">Overview</ScrollspyLink></ScrollspyItem>
<ScrollspyItem><ScrollspyLink href="#scrollspy-install">Install</ScrollspyLink></ScrollspyItem>
<ScrollspyItem><ScrollspyLink href="#scrollspy-usage">Usage</ScrollspyLink></ScrollspyItem>
<ScrollspyItem><ScrollspyLink href="#scrollspy-faq">FAQ</ScrollspyLink></ScrollspyItem>
</ScrollspyList>
</Scrollspy>
</div>
);
}Each link needs exactly one target, inside the scrolling element when data-plf-container is set.
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="scroll-area" style="--plf-scroll-area-height: 12rem">
<div class="scroll-area-viewport" id="scrollspy-nested-viewport" role="region" tabindex="0" aria-label="Guide">
<div class="scroll-area-content prose">
<h3 id="scrollspy-nested-intro">Introduction</h3>
<p>Content for the introduction. It is long enough to scroll past before the next section starts.</p>
<h4 id="scrollspy-nested-step-one">Step one</h4>
<p>Content for step one. It is long enough to scroll past before the next section starts.</p>
<h4 id="scrollspy-nested-step-two">Step two</h4>
<p>Content for step two. It is long enough to scroll past before the next section starts.</p>
<h3 id="scrollspy-nested-summary">Summary</h3>
<p>Content for the summary.</p>
</div>
</div>
</div>
<nav class="scrollspy" data-plf-scrollspy data-plf-container="scrollspy-nested-viewport" aria-labelledby="scrollspy-nested-title">
<p class="scrollspy-title" id="scrollspy-nested-title">Topics</p>
<ul class="scrollspy-list">
<li class="scrollspy-item"><a class="scrollspy-link" href="#scrollspy-nested-intro">Introduction</a>
<ul class="scrollspy-list">
<li class="scrollspy-item"><a class="scrollspy-link" href="#scrollspy-nested-step-one">Step one</a></li>
<li class="scrollspy-item"><a class="scrollspy-link" href="#scrollspy-nested-step-two">Step two</a></li>
</ul>
</li>
<li class="scrollspy-item"><a class="scrollspy-link" href="#scrollspy-nested-summary">Summary</a></li>
</ul>
</nav>
</div><div class="plf:grid plf:grid-cols-1 plf:sm:grid-cols-2 plf:gap-4">
<div class="plf:scroll-area" style="--plf-scroll-area-height: 12rem">
<div class="plf:scroll-area-viewport" id="scrollspy-nested-viewport" role="region" tabindex="0" aria-label="Guide">
<div class="plf:scroll-area-content plf:prose">
<h3 id="scrollspy-nested-intro">Introduction</h3>
<p>Content for the introduction. It is long enough to scroll past before the next section starts.</p>
<h4 id="scrollspy-nested-step-one">Step one</h4>
<p>Content for step one. It is long enough to scroll past before the next section starts.</p>
<h4 id="scrollspy-nested-step-two">Step two</h4>
<p>Content for step two. It is long enough to scroll past before the next section starts.</p>
<h3 id="scrollspy-nested-summary">Summary</h3>
<p>Content for the summary.</p>
</div>
</div>
</div>
<nav class="plf:scrollspy" data-plf-scrollspy data-plf-container="scrollspy-nested-viewport" aria-labelledby="scrollspy-nested-title">
<p class="plf:scrollspy-title" id="scrollspy-nested-title">Topics</p>
<ul class="plf:scrollspy-list">
<li class="plf:scrollspy-item"><a class="plf:scrollspy-link" href="#scrollspy-nested-intro">Introduction</a>
<ul class="plf:scrollspy-list">
<li class="plf:scrollspy-item"><a class="plf:scrollspy-link" href="#scrollspy-nested-step-one">Step one</a></li>
<li class="plf:scrollspy-item"><a class="plf:scrollspy-link" href="#scrollspy-nested-step-two">Step two</a></li>
</ul>
</li>
<li class="plf:scrollspy-item"><a class="plf:scrollspy-link" href="#scrollspy-nested-summary">Summary</a></li>
</ul>
</nav>
</div>'use client';
import { Scrollspy, ScrollspyItem, ScrollspyLink, ScrollspyList, ScrollspyTitle } from '@pelatform/react';
import { useRef, type CSSProperties } from 'react';
export function Example() {
const viewport = useRef<HTMLDivElement>(null);
return (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="scroll-area" style={{ '--plf-scroll-area-height': '12rem' } as CSSProperties}>
<div className="scroll-area-viewport" ref={viewport} role="region" tabIndex={0} aria-label="Guide">
<div className="scroll-area-content prose">
<h3 id="scrollspy-nested-intro">Introduction</h3>
<p>Content for the introduction. It is long enough to scroll past before the next section starts.</p>
<h4 id="scrollspy-nested-step-one">Step one</h4>
<p>Content for step one. It is long enough to scroll past before the next section starts.</p>
<h4 id="scrollspy-nested-step-two">Step two</h4>
<p>Content for step two. It is long enough to scroll past before the next section starts.</p>
<h3 id="scrollspy-nested-summary">Summary</h3>
<p>Content for the summary.</p>
</div>
</div>
</div>
<Scrollspy container={viewport} aria-labelledby="scrollspy-nested-title">
<ScrollspyTitle id="scrollspy-nested-title">Topics</ScrollspyTitle>
<ScrollspyList>
<ScrollspyItem>
<ScrollspyLink href="#scrollspy-nested-intro">Introduction</ScrollspyLink>
<ScrollspyList>
<ScrollspyItem><ScrollspyLink href="#scrollspy-nested-step-one">Step one</ScrollspyLink></ScrollspyItem>
<ScrollspyItem><ScrollspyLink href="#scrollspy-nested-step-two">Step two</ScrollspyLink></ScrollspyItem>
</ScrollspyList>
</ScrollspyItem>
<ScrollspyItem><ScrollspyLink href="#scrollspy-nested-summary">Summary</ScrollspyLink></ScrollspyItem>
</ScrollspyList>
</Scrollspy>
</div>
);
}'use client';
import { Scrollspy, ScrollspyItem, ScrollspyLink, ScrollspyList, ScrollspyTitle } from '@pelatform/react/prefixed';
import { useRef, type CSSProperties } from 'react';
export function Example() {
const viewport = useRef<HTMLDivElement>(null);
return (
<div className="plf:grid plf:grid-cols-1 plf:sm:grid-cols-2 plf:gap-4">
<div className="plf:scroll-area" style={{ '--plf-scroll-area-height': '12rem' } as CSSProperties}>
<div className="plf:scroll-area-viewport" ref={viewport} role="region" tabIndex={0} aria-label="Guide">
<div className="plf:scroll-area-content plf:prose">
<h3 id="scrollspy-nested-intro">Introduction</h3>
<p>Content for the introduction. It is long enough to scroll past before the next section starts.</p>
<h4 id="scrollspy-nested-step-one">Step one</h4>
<p>Content for step one. It is long enough to scroll past before the next section starts.</p>
<h4 id="scrollspy-nested-step-two">Step two</h4>
<p>Content for step two. It is long enough to scroll past before the next section starts.</p>
<h3 id="scrollspy-nested-summary">Summary</h3>
<p>Content for the summary.</p>
</div>
</div>
</div>
<Scrollspy container={viewport} aria-labelledby="scrollspy-nested-title">
<ScrollspyTitle id="scrollspy-nested-title">Topics</ScrollspyTitle>
<ScrollspyList>
<ScrollspyItem>
<ScrollspyLink href="#scrollspy-nested-intro">Introduction</ScrollspyLink>
<ScrollspyList>
<ScrollspyItem><ScrollspyLink href="#scrollspy-nested-step-one">Step one</ScrollspyLink></ScrollspyItem>
<ScrollspyItem><ScrollspyLink href="#scrollspy-nested-step-two">Step two</ScrollspyLink></ScrollspyItem>
</ScrollspyList>
</ScrollspyItem>
<ScrollspyItem><ScrollspyLink href="#scrollspy-nested-summary">Summary</ScrollspyLink></ScrollspyItem>
</ScrollspyList>
</Scrollspy>
</div>
);
}JavaScript
Scrollspy needs JavaScript to mark the current link. The Full script (pelatform-ui.min.js) starts every component on page load, with no code of your own; with ESM imports, call init() yourself, as in Installation. For markup you add later, see Dynamic content.
<link rel="stylesheet" href="pelatform-ui.min.css">
<script src="pelatform-ui.min.js" defer></script><link rel="stylesheet" href="core/pelatform-ui-core.min.css">
<link rel="stylesheet" href="components/scroll-area.min.css">
<link rel="stylesheet" href="components/scrollspy.min.css">
<script src="core/pelatform-ui-core.min.js" defer></script>
<script src="components/scrollspy.min.js" defer></script>
<script>document.addEventListener('DOMContentLoaded', () => PLF.init());</script><link rel="stylesheet" href="pelatform-ui.prefixed.min.css">
<script src="pelatform-ui.min.js" defer></script><link rel="stylesheet" href="core/pelatform-ui-core.prefixed.min.css">
<link rel="stylesheet" href="components/scroll-area.prefixed.min.css">
<link rel="stylesheet" href="components/scrollspy.prefixed.min.css">
<script src="core/pelatform-ui-core.min.js" defer></script>
<script src="components/scrollspy.min.js" defer></script>
<script>document.addEventListener('DOMContentLoaded', () => PLF.init());</script>| Attribute | Purpose |
|---|---|
data-plf-scrollspy |
Marks the table of contents, named with aria-label or aria-labelledby |
data-plf-container |
Optional: the id of the element that scrolls the sections; the page by default |
href="#…" |
Each link points at exactly one element with that id |
The script sets aria-current="location" on the current link.
Methods
| Method | Behavior |
|---|---|
PLF. |
Returns the instance, creating it for valid markup |
PLF. |
Returns the instance or null |
instance. |
Returns { active, target }: the current link and its section |
instance. |
Marks the current link again, for example after content changed height |
instance. |
Removes listeners and aria-current |
| Event | When |
|---|---|
plf: |
After the current link changes |
The event’s detail:
| Property | Value |
|---|---|
instance |
The Scrollspy instance |
link |
The current link |
target |
The current link’s section |
previous |
The link current before |
reason |
scroll, trigger when a link or the address’s hash is followed, or api from refresh |
trigger |
The clicked link, or null for a scroll, the address’s hash or refresh |
| Key | Action |
|---|---|
| Tab, Shift+Tab | Move between the links |
| Enter | Follows the focused link, which becomes current |
React
Scrollspy from @pelatform/react renders the nav around your links and marks the link of the section being read with aria-current="location", as the script does; each part is a component of its own.
| Prop | Values | Default |
|---|---|---|
container |
The element that scrolls the sections and holds each link’s section, as a ref or the element | The page |
The parts:
| Part | Element |
|---|---|
ScrollspyTitle |
A p with scrollspy-title |
ScrollspyList |
A ul with scrollspy-list |
ScrollspyItem |
An li with scrollspy-item |
ScrollspyLink |
An a with scrollspy-link, whose href="#…" names its section |
Other props are the element’s own, className and ref included.
| Variable | Default |
|---|---|
--plf-scrollspy-line-color |
--plf-color-border-subtle |
--plf-scrollspy-indicator-color |
--plf-color-primary |
--plf-scrollspy-current-color |
--plf-color-text |