https://github.com/fsegurai/scrollspy
Scrollspy is a lightweight JS library that highlights active navigation links based on scroll position. Supports nested menus and custom events for seamless single-page navigation.
https://github.com/fsegurai/scrollspy
accessibility javascript library lightweight scroll scrollspy tableofcontents toc typescript
Last synced: 3 months ago
JSON representation
Scrollspy is a lightweight JS library that highlights active navigation links based on scroll position. Supports nested menus and custom events for seamless single-page navigation.
- Host: GitHub
- URL: https://github.com/fsegurai/scrollspy
- Owner: fsegurai
- License: mit
- Created: 2025-07-09T22:27:07.000Z (12 months ago)
- Default Branch: main
- Last Pushed: 2025-07-10T03:02:08.000Z (12 months ago)
- Last Synced: 2025-07-10T04:46:44.526Z (12 months ago)
- Topics: accessibility, javascript, library, lightweight, scroll, scrollspy, tableofcontents, toc, typescript
- Language: JavaScript
- Homepage: https://fsegurai.github.io/scrollspy/
- Size: 335 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
Awesome Lists containing this project
README
**A library for scrollspy functionality**
`@fsegurai/scrollspy` is a dependency-free, lightweight scrollspy library that highlights navigation links based on
scroll position. Perfect for documentation sites, blogs, and landing pages with sticky tables of contents.
---
## ๐ Table of Contents
- [๐ Features](#-features)
- [๐ฆ Installation](#-installation)
- [NPM](#npm)
- [CDN / HTML](#cdn--html)
- [๐ง Usage](#-usage)
- [HTML Example](#html-example)
- [JavaScript Example](#javascript-example)
- [TypeScript Example](#typescript-example)
- [๐งช Demo Integration](#-demo-integration)
- [โ๏ธ Options](#๏ธ-options)
- [๐ก Events](#-events)
- [`gumshoeactivate`](#gumshoeactivate)
- [`gumshoedeactivate`](#gumshoedeactivate)
- [Type-Safe Event Listeners](#type-safe-event-listeners)
- [๐ Dynamic Content Support](#-dynamic-content-support)
- [๐ API](#-api)
- [๐ฏ TypeScript Support](#-typescript-support)
- [โ
Browser Support](#-browser-support)
- [๐งผ License](#-license)
---
## ๐ Features
- โก๏ธ Lightweight (no dependencies)
- ๐ **100% TypeScript** with full type definitions
- ๐ Intelligent scroll-based section detection
- ๐งฉ Nested navigation support
- ๐งญ Works with dynamic or static content
- ๐ฏ Scroll offset for fixed headers
- ๐ Automatic DOM mutation observer (optional)
- ๐ Type-safe custom activation events
- ๐งผ Clean API with init/refresh/destroy
---
## ๐ฆ Installation
### NPM
```bash
npm install @fsegurai/scrollspy
```
### CDN / HTML
```html
import ScrollSpy from '@fsegurai/scrollspy';
const spy = new ScrollSpy('#toc');
```
---
## ๐ง Usage
### HTML Example
```html
Intro
...
Install
...
Usage
Basic
...
Advanced
...
```
### JavaScript Example
```js
import ScrollSpy from '@fsegurai/scrollspy';
const spy = new ScrollSpy('#toc', {
offset: 80,
nested: true,
nestedClass: 'parent-active',
reflow: true,
events: true,
observe: true
});
// Listen for activation events
document.addEventListener('gumshoeactivate', (event) => {
console.log('Activated:', event.detail.target.id);
});
```
### TypeScript Example
```ts
import ScrollSpy, {type ScrollSpyEvent, type ScrollSpyOptions} from '@fsegurai/scrollspy';
const options: ScrollSpyOptions = {
offset: 80,
nested: true,
nestedClass: 'parent-active',
reflow: true,
events: true,
observe: true
};
const spy = new ScrollSpy('#toc', options);
// Fully typed event listener
document.addEventListener('gumshoeactivate', (event: Event) => {
const customEvent = event as CustomEvent;
console.log('Activated:', customEvent.detail.target.id);
console.log('Nav item:', customEvent.detail.nav);
});
```
---
## ๐งช Demo Integration
The demo in `demo/scripts/utils/toc.ts` builds a nested table of contents from headings, marks each heading with
`data-gumshoe`, and then initializes ScrollSpy against `#tableOfContents`.
```ts
import {
generateTOC,
initScrollspy,
setupMobileToggle,
setupSmoothScroll,
} from './utils/toc';
const content = document.querySelector('#content') as HTMLElement;
generateTOC(content);
setupMobileToggle();
setupSmoothScroll();
initScrollspy();
```
In that demo flow, the generated headings look like this:
```html
Intro
```
`initScrollspy()` configures the instance with `content: '[data-gumshoe]'`, `offset: 120`, `bottomThreshold: 10`,
`reflow: true`, and `events: true`.
---
## โ๏ธ Options
All available options for customizing behavior:
| Option | Type | Default | Description |
|---------------------|-----------------------------------------------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `nav` | `string` | โ | **(Required)** Selector for the navigation container. This is the element ScrollSpy scans for links. |
| `content` | `string` | `[data-gumshoe]` | Default selector used by the demo and dynamic-content workflows. The core lookup still resolves targets from nav fragments and `fragmentAttribute`. |
| `nested` | `boolean` | `false` | Adds a class to parent `
| `nestedClass` | `string` | `'active-parent'` | Class name for parent `
| `offset` | `number \| () => number` | `0` | Scroll offset in pixels or a function returning an offset, useful for fixed headers. |
| `bottomThreshold` | `number` | `100` | Distance in pixels from the bottom of the page where the last section is auto-activated. |
| `reflow` | `boolean` | `false` | If `true`, ScrollSpy also re-detects on window resize. |
| `events` | `boolean` | `true` | Emits `gumshoeactivate` when the active section changes. `gumshoedeactivate` is part of the typings, but the current runtime does not dispatch it. |
| `observe` | `boolean` | `false` | Enables a `MutationObserver` that calls `refresh()` when observed DOM nodes change. |
| `fragmentAttribute` | `string \| (item: Element) => string \| null` | `null` | Attribute or function used to map nav items to content sections instead of relying on `href`. Supports full URLs like `/route#fragment` through the default hash parsing path. |
| `navItemSelector` | `string` | `'a[href*="#"]'` | Selector for nav items (anchors or other elements) that should be considered by ScrollSpy. |
> If you're using `observe: true`, make sure your headings or section wrappers have a consistent structure. The
> `data-gumshoe` attribute is used by the demo and matches the default `content` selector, but section matching still
> starts from the nav fragments themselves.
---
### Advanced Fragment Mapping (SPA/Angular)
If you need to support full URLs in `href` (e.g. `/route#fragment`) or use a custom attribute (e.g.
`data-scrollspy-fragment`), use the `fragmentAttribute` option:
```js
// Use a custom attribute
const spy = new ScrollSpy('#toc', {
fragmentAttribute: 'data-scrollspy-fragment',
});
// Or use a function for advanced mapping
const spy = new ScrollSpy('#toc', {
fragmentAttribute: (item) => item.getAttribute('data-scrollspy-fragment') || null,
});
```
- The library will now match anchors using the custom attribute or function, not just `href`.
- This is useful for Angular/SPA scenarios where you want the user to see the full URL in the browser, but scrollspy to
map by fragment only.
---
## ๐ก Events
These custom events are available on `document` when ScrollSpy updates the active section.
### `gumshoeactivate`
Triggered when a new section becomes active.
```js
document.addEventListener('gumshoeactivate', (e) => {
console.log('Activated:', e.detail.target.id);
console.log('Content:', e.detail.content);
console.log('Nav item:', e.detail.nav);
});
```
### About `gumshoedeactivate`
`gumshoedeactivate` is included in the type definitions, but the current implementation does not dispatch it. If you
need deactivation hooks, listen for `gumshoeactivate` and compare the previous active section yourself.
Event `detail` includes:
- `target`: The content section element
- `content`: Alias of `target`
- `nav`: Corresponding anchor tag from the TOC
### Type-Safe Event Listeners
The library includes full TypeScript type definitions for the custom events that ship with the package. The
`DocumentEventMap` is augmented to include both `gumshoeactivate` and `gumshoedeactivate`, even though only
`gumshoeactivate` is emitted by the current runtime:
```ts
import type {ScrollSpyEvent} from '@fsegurai/scrollspy';
// TypeScript knows about these custom events automatically
document.addEventListener('gumshoeactivate', (event: Event) => {
const customEvent = event as CustomEvent;
// Full intellisense support for event.detail.target, content, nav
console.log(customEvent.detail.target.id);
});
```
- `target`: The content section element
- `content`: Alias of `target`
- `nav`: Corresponding anchor tag from the TOC
---
## ๐ Dynamic Content Support
If you update the TOC or headings dynamically, call:
```js
spy.refresh();
```
Or initialize with `observe: true` to let ScrollSpy refresh itself using a `MutationObserver`.
---
## ๐ API
| Method | Description |
|-----------------|-----------------------------------------------------------------------------|
| `init()` | Performs the initial DOM lookup, content mapping, detection, and listeners. |
| `getContents()` | Rebuilds the internal nav-to-target map from the current DOM. |
| `getNavItem()` | Resolves the nav element associated with a content section. |
| `detect()` | Re-runs detection logic based on current scroll position. |
| `setup()` | Rebuilds contents and runs detection again. |
| `refresh()` | Same rebuild/detect cycle as `setup()`; use this after dynamic updates. |
| `destroy()` | Removes listeners, clears active classes, and disconnects the observer. |
> `setup()` and `refresh()` currently perform the same rebuild/detect pass.
---
## ๐ฏ TypeScript Support
The library is built entirely in **TypeScript** and exports complete type definitions:
```ts
import ScrollSpy, {
type ScrollSpyOptions,
type ScrollSpyEvent,
type ContentPosition
} from '@fsegurai/scrollspy';
// Full type safety for all options
const options: ScrollSpyOptions = {
offset: 80,
nested: true,
};
// Constructor is fully typed
const spy = new ScrollSpy('#toc', options);
// Event detail is typed
document.addEventListener('gumshoeactivate', (event: Event) => {
const e = event as CustomEvent;
const target: Element = e.detail.target;
const nav: Element = e.detail.nav;
});
```
`ScrollSpyOptions` includes the navigation selector, offset controls, nested-navigation classes, fragment mapping, and
the optional MutationObserver toggle. `ScrollSpyEvent` is the shared detail payload for the activation event.
---
## โ Browser Support
| Browser | Support |
|---------|---------|
| Chrome | โ
|
| Firefox | โ
|
| Safari | โ
|
| Edge | โ
|
| IE11 | โ |
โ ๏ธ Requires `CustomEvent` support. You may need polyfills for legacy environments.
---
## ๐งผ License
Licensed under [MIT](https://opensource.org/licenses/MIT).