react-use-section-observer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # react-use-section-observer
2
+
3
+ A lightweight React hook to track element visibility using the Intersection Observer API and automatically apply an active class to matching links.
4
+
5
+ ---
6
+
7
+ ## 🎥 Demo
8
+
9
+ ![Preview](./docs/preview.gif)
10
+
11
+ ---
12
+
13
+ ## ✨ Features
14
+
15
+ - Tracks visibility of multiple sections
16
+ - Automatically updates matching anchor links
17
+ - Supports custom root containers
18
+ - Supports multiple class names (e.g. Tailwind)
19
+ - No DOM mutation via `data-*`
20
+ - No external dependencies
21
+ - Fully typed (TypeScript)
22
+ - Adjustable to your DOM structure
23
+
24
+ ---
25
+
26
+ ## ⚠️ Limitations
27
+
28
+ - Only vertical scrolling (Y-axis) is supported.
29
+ - Links must use the `href="#id"` format.
30
+ - Requires browser support for `IntersectionObserver`.
31
+
32
+ ---
33
+
34
+ ## 📦 Installation
35
+
36
+ ```bash
37
+ npm install react-use-section-observer
38
+ ```
@@ -0,0 +1,45 @@
1
+ import { type DependencyList } from "react";
2
+ export interface SectionObserverOptions {
3
+ /**
4
+ * Array of section IDs to observe.
5
+ * Each ID should correspond to an element in the DOM that you want to track.
6
+ */
7
+ ids: string[];
8
+ /**
9
+ * Enable or disable the observer.
10
+ * If your sections are dynamically added or removed, you can set this to false
11
+ * until the sections are ready, then set it to true to start observing.
12
+ */
13
+ enabled?: boolean;
14
+ /**
15
+ * The root element for the Intersection Observer.
16
+ * Defaults to the viewport if not specified.
17
+ */
18
+ viewportRoot?: Element | null;
19
+ /** Class name(s) to apply to the active link */
20
+ activeClassName?: string;
21
+ /**
22
+ * Callback function invoked when the active element changes.
23
+ * Receives an object containing the active link and the corresponding active section element.
24
+ */
25
+ onActiveChange?: (params: {
26
+ link: Element;
27
+ activeElement: Element | null;
28
+ }) => void;
29
+ /**
30
+ * Optional accessors to customize how links are selected and how active state is determined.
31
+ * This allows you to adapt the hook to different DOM structures and link formats.
32
+ */
33
+ linkAccessors?: {
34
+ getLinkSelector: (id: string) => string;
35
+ getIsLinkActive: (params: {
36
+ link: Element;
37
+ activeElement: Element;
38
+ }) => boolean;
39
+ };
40
+ }
41
+ /**
42
+ * React hook to observe sections and update corresponding links
43
+ * based on their visibility in the viewport.
44
+ */
45
+ export declare function useSectionObserver({ ids, enabled, viewportRoot, activeClassName, onActiveChange, linkAccessors: linkAccessor, }: SectionObserverOptions, deps?: DependencyList): void;
@@ -0,0 +1,65 @@
1
+ import { useRef as b, useEffect as p } from "react";
2
+ function L({
3
+ ids: n,
4
+ enabled: r = !0,
5
+ viewportRoot: t = null,
6
+ activeClassName: o = "active",
7
+ onActiveChange: l,
8
+ linkAccessors: g
9
+ }, d = []) {
10
+ const {
11
+ getLinkSelector: v = (c) => `a[href$="#${c}"]`,
12
+ getIsLinkActive: E = ({
13
+ link: c,
14
+ activeElement: i
15
+ }) => c.getAttribute("href")?.endsWith(`#${i?.id}`)
16
+ } = g || {}, u = b(/* @__PURE__ */ new Map());
17
+ p(() => {
18
+ if (!r || !n.length) return;
19
+ const c = n.map((e) => document.getElementById(e)).filter((e) => !!e), i = n.map((e) => document.querySelector(v(e))).filter((e) => !!e), a = new IntersectionObserver(
20
+ (e) => {
21
+ e.forEach((s) => {
22
+ u.current.set(s.target, s);
23
+ });
24
+ const f = A(u.current);
25
+ i.forEach((s) => {
26
+ const h = f && E({ link: s, activeElement: f });
27
+ m(s, o, !!h), h && l && l({ link: s, activeElement: f });
28
+ });
29
+ },
30
+ {
31
+ root: t,
32
+ threshold: B()
33
+ }
34
+ );
35
+ return c.forEach((e) => a.observe(e)), () => {
36
+ a.disconnect(), u.current.clear(), i.forEach((e) => m(e, o, !1));
37
+ };
38
+ }, [
39
+ n.join(","),
40
+ r,
41
+ t,
42
+ o,
43
+ l,
44
+ ...d
45
+ ]);
46
+ }
47
+ function A(n) {
48
+ const r = Array.from(n.values()).filter(
49
+ (t) => t.isIntersecting
50
+ );
51
+ return r.length ? (r.sort((t, o) => o.intersectionRatio - t.intersectionRatio), r[0].target) : null;
52
+ }
53
+ function m(n, r, t) {
54
+ const o = r.trim().split(/\s+/);
55
+ n.classList[t ? "add" : "remove"](...o);
56
+ }
57
+ function B(n = 0.1) {
58
+ const r = [];
59
+ for (let t = 0; t <= 1; t += n)
60
+ r.push(Number(t.toFixed(2)));
61
+ return r;
62
+ }
63
+ export {
64
+ L as useSectionObserver
65
+ };
@@ -0,0 +1 @@
1
+ (function(o,i){typeof exports=="object"&&typeof module<"u"?i(exports,require("react")):typeof define=="function"&&define.amd?define(["exports","react"],i):(o=typeof globalThis<"u"?globalThis:o||self,i(o.useSectionObserver={},o.React))})(this,(function(o,i){"use strict";function g({ids:n,enabled:r=!0,viewportRoot:t=null,activeClassName:s="active",onActiveChange:l,linkAccessors:E},S=[]){const{getLinkSelector:y=u=>`a[href$="#${u}"]`,getIsLinkActive:O=({link:u,activeElement:f})=>u.getAttribute("href")?.endsWith(`#${f?.id}`)}=E||{},d=i.useRef(new Map);i.useEffect(()=>{if(!r||!n.length)return;const u=n.map(e=>document.getElementById(e)).filter(e=>!!e),f=n.map(e=>document.querySelector(y(e))).filter(e=>!!e),m=new IntersectionObserver(e=>{e.forEach(c=>{d.current.set(c.target,c)});const a=p(d.current);f.forEach(c=>{const b=a&&O({link:c,activeElement:a});h(c,s,!!b),b&&l&&l({link:c,activeElement:a})})},{root:t,threshold:v()});return u.forEach(e=>m.observe(e)),()=>{m.disconnect(),d.current.clear(),f.forEach(e=>h(e,s,!1))}},[n.join(","),r,t,s,l,...S])}function p(n){const r=Array.from(n.values()).filter(t=>t.isIntersecting);return r.length?(r.sort((t,s)=>s.intersectionRatio-t.intersectionRatio),r[0].target):null}function h(n,r,t){const s=r.trim().split(/\s+/);n.classList[t?"add":"remove"](...s)}function v(n=.1){const r=[];for(let t=0;t<=1;t+=n)r.push(Number(t.toFixed(2)));return r}o.useSectionObserver=g,Object.defineProperty(o,Symbol.toStringTag,{value:"Module"})}));
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "react-use-section-observer",
3
+ "version": "1.0.0",
4
+ "description": "A react hook to track the visibility of elements in the viewport using Intersection Observer API",
5
+ "homepage": "https://github.com/theo-js/react-use-section-observer#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/theo-js/react-use-section-observer/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/theo-js/react-use-section-observer.git"
12
+ },
13
+ "license": "ISC",
14
+ "author": {
15
+ "email": "dev.js.theo@gmail.com",
16
+ "name": "theo-js",
17
+ "url": "https://github.com/theo-js"
18
+ },
19
+ "main": "dist/index.umd.js",
20
+ "module": "dist/index.es.js",
21
+ "types": "dist/index.d.ts",
22
+ "files": [
23
+ "dist",
24
+ "README.md"
25
+ ],
26
+ "scripts": {
27
+ "build:types": "tsc",
28
+ "build:js": "vite build",
29
+ "copy:types": "xcopy types\\* dist /E /I /Y",
30
+ "build": "npm run build:types && npm run build:js && npm run copy:types"
31
+ },
32
+ "keywords": [
33
+ "react",
34
+ "hook",
35
+ "intersection-observer",
36
+ "visibility-tracker"
37
+ ],
38
+ "dependencies": {
39
+ "react": "^19.2.4",
40
+ "react-dom": "^19.2.4"
41
+ },
42
+ "devDependencies": {
43
+ "@types/react": "^19.2.14",
44
+ "@types/react-dom": "^19.2.3",
45
+ "tsup": "^8.5.1",
46
+ "typescript": "^5.9.3",
47
+ "vite": "^7.3.1",
48
+ "@vitejs/plugin-react": "^5.1.4"
49
+ },
50
+ "peerDependencies": {
51
+ "react": "^19.0.0",
52
+ "react-dom": "^19.0.0"
53
+ },
54
+ "workspaces": [
55
+ "playground"
56
+ ]
57
+ }