implicit-sections-observer
v1.0.0-rc.0
Published
A tiny tool to observe heading-inferred sections and track their viewport intersection on page scroll and resize.
Maintainers
Readme
Implicit Sections Observer
implicit-sections-observer infers sections from your headings and continuously reports how those sections intersect with the viewport on scroll change or page resize. I made it for precisely highlighting table of contents. But with its observation data you are free to do anything you want, not just highlighting a table of contents.
It has two superpowers:
- Implicit sections: Implicit sections are sections that do not explicitly exist but are understood by its algorithm, just as you understand what a section of this page is without looking for some kind of wrapper element around each section. You are no longer forced to wrap those in
<section>elements. - Seamless intersection data: You receive continuous intersection information not only when an implicit section enters or leaves the viewport, but on every scroll or page resize while it is partially visible.
Installation
npm i implicit-sections-observer
# or
pnpm add implicit-sections-observer
# or
yarn add implicit-sections-observerQuick start
Suppose you have content living directly in <body>:
<body>
<h1>H1 Heading</h1>
<p>…</p>
<h2>H2 Heading</h2>
<p>…</p>
<!-- and so on -->
</body>import { implicitSectionsObserver } from 'implicit-sections-observer';
const observer = implicitSectionsObserver(
{
// grab the headings to infer the implicit sections
headings: document.querySelectorAll(':is(h1,h2,h3,h4,h5,h6)'),
},
(data) => {
// You get the observation data here
console.log(data);
},
);
// To start observing call the `start` method
observer.start();
// To stop observing call the `stop` method
// observer.stop();If your content lives inside an <article> (or any other container):
const observer = implicitSectionsObserver(
{
// this time grab the headings within the `<article>` to infer the implicit sections
headings: document.querySelectorAll('article :is(h1, h2, h3, h4, h5, h6)'),
contentContainer: document.querySelector('article'), // needed for correct last-section height
},
(data) => {
console.log(data);
},
);contentContainer defaults to document.body. Passing the actual content container ensures the final implicit section gets the correct height.
Limitation: Nested scroll containers are not supported. The library is designed for the common case of page-level scrolling.
API
Inputs
The first parameter of implicitSectionObserver function expects an object argument of following shape:
interface Inputs {
// Heading elements that define the start of each implicit section
headings: NodeListOf<HTMLHeadingElement> | HTMLHeadingElement[];
// Element that contains all the content (used to calculate the height of the
// last implicit section). Defaults to `document.body`.
contentContainer?: HTMLElement | null;
// Extra space at the top of the viewport. Default: 0
marginTop?: number;
// Extra space at the bottom of the viewport. Default: 0
marginBottom?: number;
}Callback data
Every time the scroll position changes (or on the initial observation) the callback receives an object with this shape:
interface ObservationData {
// Entries in document order. Currently intersecting sections come first.
// Sections that just left the viewport are listed at the end.
entries: Array<{
// The heading that starts this implicit section
target: HTMLHeadingElement;
// How much of the section is currently visible
intersectionRatio: number;
// Whether the section intersects the (possibly margin-adjusted) viewport
isIntersecting: boolean;
// Only present on the first intersecting entry.
// Fraction of the top of that section that is currently above the viewport.
// e.g. `0.5` means the top half is scrolled out of view.
topNotIntersectingRatio?: number;
}>;
// Handy reference to the topmost intersecting entry
topIntersectingEntry?: {
target: HTMLHeadingElement;
intersectionRatio: number;
isIntersecting: boolean;
topNotIntersectingRatio: number;
};
// Handy reference to the bottommost intersecting entry.
// Absent when only one section is intersecting.
bottomIntersectingEntry?: {
target: HTMLHeadingElement;
intersectionRatio: number;
isIntersecting: boolean;
};
// Current `scrollY`
scrollTop: number;
// Scroll delta since the previous observation
scrollDiff: number;
// `performance.now()` at the moment of this observation
timestamp: number;
// Milliseconds since the previous observation
timeDiff: number;
}See the actual type definition file included with this library for stricter type definitions.
Methods
// Begin observing
observer.start();
// Stop observing
observer.stop();
// Useful for replacing stale observation data with fresh data (e.g., after an accordion opens inside an intersecting implicit section).
observer.refresh();