scroll-snap-kit 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,181 @@
1
+ # scroll-snap-kit 🎯
2
+
3
+ > Smooth scroll utilities + React hooks for modern web apps.
4
+
5
+ Zero dependencies. Tree-shakeable. Works with or without React.
6
+
7
+ ---
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install scroll-snap-kit
13
+ ```
14
+
15
+ ---
16
+
17
+ ## Vanilla JS Utilities
18
+
19
+ ```js
20
+ import { scrollTo, scrollToTop, scrollToBottom, getScrollPosition, onScroll, isInViewport, lockScroll, unlockScroll } from 'scroll-snap-kit/utils';
21
+ ```
22
+
23
+ ### `scrollTo(target, options?)`
24
+
25
+ Smoothly scroll to a DOM element or a Y pixel value.
26
+
27
+ ```js
28
+ scrollTo(document.querySelector('#section'));
29
+ scrollTo(500); // scroll to y=500px
30
+ scrollTo(document.querySelector('#hero'), { offset: -80 }); // with offset (e.g. sticky header)
31
+ ```
32
+
33
+ | Option | Type | Default | Description |
34
+ |--------|------|---------|-------------|
35
+ | `behavior` | `'smooth' \| 'instant'` | `'smooth'` | Scroll behavior |
36
+ | `block` | `ScrollLogicalPosition` | `'start'` | Vertical alignment |
37
+ | `offset` | `number` | `0` | Pixel offset adjustment |
38
+
39
+ ---
40
+
41
+ ### `scrollToTop(options?)` / `scrollToBottom(options?)`
42
+
43
+ ```js
44
+ scrollToTop();
45
+ scrollToBottom({ behavior: 'instant' });
46
+ ```
47
+
48
+ ---
49
+
50
+ ### `getScrollPosition(container?)`
51
+
52
+ ```js
53
+ const { x, y, percentX, percentY } = getScrollPosition();
54
+ // percentY = how far down the page (0–100)
55
+ ```
56
+
57
+ ---
58
+
59
+ ### `onScroll(callback, options?)`
60
+
61
+ Throttled scroll listener. Returns a cleanup function.
62
+
63
+ ```js
64
+ const stop = onScroll(({ y, percentY }) => {
65
+ console.log(`Scrolled ${percentY}% down`);
66
+ }, { throttle: 150 });
67
+
68
+ // Later:
69
+ stop(); // removes the listener
70
+ ```
71
+
72
+ ---
73
+
74
+ ### `isInViewport(element, options?)`
75
+
76
+ ```js
77
+ if (isInViewport(document.querySelector('.card'), { threshold: 0.5 })) {
78
+ // At least 50% of the card is visible
79
+ }
80
+ ```
81
+
82
+ ---
83
+
84
+ ### `lockScroll()` / `unlockScroll()`
85
+
86
+ Lock page scroll (e.g. when a modal is open) and restore position on unlock.
87
+
88
+ ```js
89
+ lockScroll(); // body stops scrolling
90
+ unlockScroll(); // restored to previous position
91
+ ```
92
+
93
+ ---
94
+
95
+ ## React Hooks
96
+
97
+ ```js
98
+ import { useScrollPosition, useInViewport, useScrollTo, useScrolledPast, useScrollDirection } from 'scroll-snap-kit/hooks';
99
+ ```
100
+
101
+ ### `useScrollPosition(options?)`
102
+
103
+ ```jsx
104
+ function ProgressBar() {
105
+ const { percentY } = useScrollPosition({ throttle: 50 });
106
+ return <div style={{ width: `${percentY}%` }} className="progress" />;
107
+ }
108
+ ```
109
+
110
+ ---
111
+
112
+ ### `useInViewport(options?)`
113
+
114
+ ```jsx
115
+ function FadeIn() {
116
+ const [ref, inView] = useInViewport({ threshold: 0.2, once: true });
117
+ return (
118
+ <div ref={ref} style={{ opacity: inView ? 1 : 0, transition: 'opacity 0.5s' }}>
119
+ I fade in when visible!
120
+ </div>
121
+ );
122
+ }
123
+ ```
124
+
125
+ | Option | Type | Default | Description |
126
+ |--------|------|---------|-------------|
127
+ | `threshold` | `number` | `0` | 0–1 portion of element visible to trigger |
128
+ | `once` | `boolean` | `false` | Only trigger the first time |
129
+
130
+ ---
131
+
132
+ ### `useScrollTo()`
133
+
134
+ ```jsx
135
+ function Page() {
136
+ const [containerRef, scrollToTarget] = useScrollTo();
137
+ const sectionRef = useRef(null);
138
+
139
+ return (
140
+ <div ref={containerRef} style={{ overflowY: 'scroll', height: '400px' }}>
141
+ <button onClick={() => scrollToTarget(sectionRef.current)}>Jump to section</button>
142
+ <div style={{ height: 300 }} />
143
+ <div ref={sectionRef}>Target section</div>
144
+ </div>
145
+ );
146
+ }
147
+ ```
148
+
149
+ ---
150
+
151
+ ### `useScrolledPast(threshold?, options?)`
152
+
153
+ ```jsx
154
+ function BackToTopButton() {
155
+ const scrolledPast = useScrolledPast(300);
156
+ return scrolledPast ? <button onClick={scrollToTop}>↑ Top</button> : null;
157
+ }
158
+ ```
159
+
160
+ ---
161
+
162
+ ### `useScrollDirection()`
163
+
164
+ ```jsx
165
+ function Navbar() {
166
+ const direction = useScrollDirection();
167
+ return (
168
+ <nav style={{ transform: direction === 'down' ? 'translateY(-100%)' : 'translateY(0)' }}>
169
+ My Navbar
170
+ </nav>
171
+ );
172
+ }
173
+ ```
174
+
175
+ Returns `'up'`, `'down'`, or `null` (on initial render).
176
+
177
+ ---
178
+
179
+ ## License
180
+
181
+ MIT
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "scroll-snap-kit",
3
+ "version": "1.0.0",
4
+ "description": "Smooth scroll utilities and React hooks for modern web apps",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "module": "./src/index.js",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./src/index.js"
11
+ },
12
+ "./utils": {
13
+ "import": "./src/utils.js"
14
+ },
15
+ "./hooks": {
16
+ "import": "./src/hooks.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "src"
21
+ ],
22
+ "scripts": {
23
+ "test": "echo \"No tests yet\""
24
+ },
25
+ "keywords": [
26
+ "scroll",
27
+ "smooth-scroll",
28
+ "scroll-hooks",
29
+ "react-hooks",
30
+ "viewport",
31
+ "scroll-position",
32
+ "frontend",
33
+ "ui"
34
+ ],
35
+ "author": "Fabian Faraz Farid",
36
+ "license": "MIT",
37
+ "peerDependencies": {
38
+ "react": ">=16.8.0"
39
+ },
40
+ "peerDependenciesMeta": {
41
+ "react": {
42
+ "optional": true
43
+ }
44
+ },
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/farazfarid/scroll-snap-kit"
48
+ }
49
+ }
package/src/hooks.js ADDED
@@ -0,0 +1,136 @@
1
+ /**
2
+ * scroll-snap-kit — React Hooks
3
+ * Requires React 16.8+
4
+ */
5
+ import { useState, useEffect, useRef, useCallback } from 'react';
6
+ import { getScrollPosition, onScroll, isInViewport } from './utils.js';
7
+
8
+ /**
9
+ * Returns the current scroll position, updated on scroll.
10
+ * @param {{ throttle?: number, container?: Element }} options
11
+ * @returns {{ x: number, y: number, percentX: number, percentY: number }}
12
+ */
13
+ export function useScrollPosition(options = {}) {
14
+ const { throttle = 100, container } = options;
15
+ const [position, setPosition] = useState(() =>
16
+ typeof window !== 'undefined' ? getScrollPosition(container) : { x: 0, y: 0, percentX: 0, percentY: 0 }
17
+ );
18
+
19
+ useEffect(() => {
20
+ const cleanup = onScroll((pos) => setPosition(pos), { throttle, container });
21
+ return cleanup;
22
+ }, [throttle, container]);
23
+
24
+ return position;
25
+ }
26
+
27
+ /**
28
+ * Returns whether a referenced element is currently in the viewport.
29
+ * @param {{ threshold?: number, once?: boolean }} options
30
+ * @returns {[React.RefObject, boolean]}
31
+ */
32
+ export function useInViewport(options = {}) {
33
+ const { threshold = 0, once = false } = options;
34
+ const ref = useRef(null);
35
+ const [inView, setInView] = useState(false);
36
+ const hasTriggered = useRef(false);
37
+
38
+ useEffect(() => {
39
+ if (!ref.current) return;
40
+
41
+ const observer = new IntersectionObserver(
42
+ ([entry]) => {
43
+ const visible = entry.isIntersecting;
44
+ if (once) {
45
+ if (visible && !hasTriggered.current) {
46
+ hasTriggered.current = true;
47
+ setInView(true);
48
+ observer.disconnect();
49
+ }
50
+ } else {
51
+ setInView(visible);
52
+ }
53
+ },
54
+ { threshold }
55
+ );
56
+
57
+ observer.observe(ref.current);
58
+ return () => observer.disconnect();
59
+ }, [threshold, once]);
60
+
61
+ return [ref, inView];
62
+ }
63
+
64
+ /**
65
+ * Returns a scrollTo function scoped to an element ref or the window.
66
+ * @returns {[React.RefObject, (target: Element|number, options?: object) => void]}
67
+ */
68
+ export function useScrollTo() {
69
+ const containerRef = useRef(null);
70
+
71
+ const scrollToTarget = useCallback((target, options = {}) => {
72
+ const { behavior = 'smooth', offset = 0 } = options;
73
+ const container = containerRef.current;
74
+
75
+ if (!container) {
76
+ // fallback to window scroll
77
+ if (typeof target === 'number') {
78
+ window.scrollTo({ top: target + offset, behavior });
79
+ } else if (target instanceof Element) {
80
+ target.scrollIntoView({ behavior });
81
+ }
82
+ return;
83
+ }
84
+
85
+ if (typeof target === 'number') {
86
+ container.scrollTo({ top: target + offset, behavior });
87
+ } else if (target instanceof Element) {
88
+ const containerTop = container.getBoundingClientRect().top;
89
+ const targetTop = target.getBoundingClientRect().top;
90
+ container.scrollBy({ top: targetTop - containerTop + offset, behavior });
91
+ }
92
+ }, []);
93
+
94
+ return [containerRef, scrollToTarget];
95
+ }
96
+
97
+ /**
98
+ * Tracks whether the user has scrolled past a given pixel threshold.
99
+ * @param {number} threshold - Y pixel value to check against (default: 100)
100
+ * @param {{ container?: Element }} options
101
+ * @returns {boolean}
102
+ */
103
+ export function useScrolledPast(threshold = 100, options = {}) {
104
+ const { container } = options;
105
+ const [scrolledPast, setScrolledPast] = useState(false);
106
+
107
+ useEffect(() => {
108
+ const cleanup = onScroll(({ y }) => {
109
+ setScrolledPast(y > threshold);
110
+ }, { container });
111
+ return cleanup;
112
+ }, [threshold, container]);
113
+
114
+ return scrolledPast;
115
+ }
116
+
117
+ /**
118
+ * Returns scroll direction: 'up' | 'down' | null
119
+ * @param {{ throttle?: number }} options
120
+ * @returns {'up'|'down'|null}
121
+ */
122
+ export function useScrollDirection(options = {}) {
123
+ const { throttle = 100 } = options;
124
+ const [direction, setDirection] = useState(null);
125
+ const lastY = useRef(typeof window !== 'undefined' ? window.scrollY : 0);
126
+
127
+ useEffect(() => {
128
+ const cleanup = onScroll(({ y }) => {
129
+ setDirection(y > lastY.current ? 'down' : 'up');
130
+ lastY.current = y;
131
+ }, { throttle });
132
+ return cleanup;
133
+ }, [throttle]);
134
+
135
+ return direction;
136
+ }
package/src/index.js ADDED
@@ -0,0 +1,22 @@
1
+ // scroll-snap-kit
2
+ // Smooth scroll utilities + React hooks
3
+ // https://github.com/farazfarid/scroll-snap-kit
4
+
5
+ export {
6
+ scrollTo,
7
+ scrollToTop,
8
+ scrollToBottom,
9
+ getScrollPosition,
10
+ onScroll,
11
+ isInViewport,
12
+ lockScroll,
13
+ unlockScroll,
14
+ } from './utils.js';
15
+
16
+ export {
17
+ useScrollPosition,
18
+ useInViewport,
19
+ useScrollTo,
20
+ useScrolledPast,
21
+ useScrollDirection,
22
+ } from './hooks.js';
package/src/utils.js ADDED
@@ -0,0 +1,154 @@
1
+ /**
2
+ * scroll-snap-kit — Core Utilities
3
+ */
4
+
5
+ /**
6
+ * Smoothly scrolls to a target element or position.
7
+ * @param {Element|number} target - A DOM element or Y pixel value
8
+ * @param {{ behavior?: ScrollBehavior, block?: ScrollLogicalPosition, offset?: number }} options
9
+ */
10
+ export function scrollTo(target, options = {}) {
11
+ const { behavior = 'smooth', block = 'start', offset = 0 } = options;
12
+
13
+ if (typeof target === 'number') {
14
+ window.scrollTo({ top: target + offset, behavior });
15
+ return;
16
+ }
17
+
18
+ if (!(target instanceof Element)) {
19
+ console.warn('[scroll-snap-kit] scrollTo: target must be an Element or a number');
20
+ return;
21
+ }
22
+
23
+ if (offset !== 0) {
24
+ const y = target.getBoundingClientRect().top + window.scrollY + offset;
25
+ window.scrollTo({ top: y, behavior });
26
+ } else {
27
+ target.scrollIntoView({ behavior, block });
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Smoothly scrolls to the top of the page.
33
+ * @param {{ behavior?: ScrollBehavior }} options
34
+ */
35
+ export function scrollToTop(options = {}) {
36
+ const { behavior = 'smooth' } = options;
37
+ window.scrollTo({ top: 0, behavior });
38
+ }
39
+
40
+ /**
41
+ * Smoothly scrolls to the bottom of the page.
42
+ * @param {{ behavior?: ScrollBehavior }} options
43
+ */
44
+ export function scrollToBottom(options = {}) {
45
+ const { behavior = 'smooth' } = options;
46
+ window.scrollTo({ top: document.body.scrollHeight, behavior });
47
+ }
48
+
49
+ /**
50
+ * Returns the current scroll position and scroll percentage.
51
+ * @param {Element} [container=window] - Optional scrollable container
52
+ * @returns {{ x: number, y: number, percentX: number, percentY: number }}
53
+ */
54
+ export function getScrollPosition(container) {
55
+ if (container && container instanceof Element) {
56
+ const { scrollLeft, scrollTop, scrollWidth, scrollHeight, clientWidth, clientHeight } = container;
57
+ return {
58
+ x: scrollLeft,
59
+ y: scrollTop,
60
+ percentX: scrollWidth > clientWidth ? Math.round((scrollLeft / (scrollWidth - clientWidth)) * 100) : 0,
61
+ percentY: scrollHeight > clientHeight ? Math.round((scrollTop / (scrollHeight - clientHeight)) * 100) : 0,
62
+ };
63
+ }
64
+
65
+ const x = window.scrollX;
66
+ const y = window.scrollY;
67
+ const maxX = document.body.scrollWidth - window.innerWidth;
68
+ const maxY = document.body.scrollHeight - window.innerHeight;
69
+
70
+ return {
71
+ x,
72
+ y,
73
+ percentX: maxX > 0 ? Math.round((x / maxX) * 100) : 0,
74
+ percentY: maxY > 0 ? Math.round((y / maxY) * 100) : 0,
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Attaches a throttled scroll event listener.
80
+ * @param {(position: ReturnType<typeof getScrollPosition>) => void} callback
81
+ * @param {{ throttle?: number, container?: Element }} options
82
+ * @returns {() => void} Cleanup function to remove the listener
83
+ */
84
+ export function onScroll(callback, options = {}) {
85
+ const { throttle: throttleMs = 100, container } = options;
86
+ const target = container || window;
87
+
88
+ let ticking = false;
89
+ let lastTime = 0;
90
+
91
+ const handler = () => {
92
+ const now = Date.now();
93
+ if (now - lastTime < throttleMs) {
94
+ if (!ticking) {
95
+ ticking = true;
96
+ requestAnimationFrame(() => {
97
+ callback(getScrollPosition(container));
98
+ ticking = false;
99
+ lastTime = Date.now();
100
+ });
101
+ }
102
+ return;
103
+ }
104
+ lastTime = now;
105
+ callback(getScrollPosition(container));
106
+ };
107
+
108
+ target.addEventListener('scroll', handler, { passive: true });
109
+ return () => target.removeEventListener('scroll', handler);
110
+ }
111
+
112
+ /**
113
+ * Checks whether an element is currently visible in the viewport.
114
+ * @param {Element} element
115
+ * @param {{ threshold?: number }} options - threshold: 0–1, portion of element that must be visible
116
+ * @returns {boolean}
117
+ */
118
+ export function isInViewport(element, options = {}) {
119
+ if (!(element instanceof Element)) {
120
+ console.warn('[scroll-snap-kit] isInViewport: argument must be an Element');
121
+ return false;
122
+ }
123
+
124
+ const { threshold = 0 } = options;
125
+ const rect = element.getBoundingClientRect();
126
+ const windowHeight = window.innerHeight || document.documentElement.clientHeight;
127
+ const windowWidth = window.innerWidth || document.documentElement.clientWidth;
128
+
129
+ const verticalVisible = rect.top + rect.height * threshold < windowHeight && rect.bottom - rect.height * threshold > 0;
130
+ const horizontalVisible = rect.left + rect.width * threshold < windowWidth && rect.right - rect.width * threshold > 0;
131
+
132
+ return verticalVisible && horizontalVisible;
133
+ }
134
+
135
+ /**
136
+ * Locks the page scroll (e.g. when a modal is open).
137
+ */
138
+ export function lockScroll() {
139
+ const scrollY = window.scrollY;
140
+ document.body.style.position = 'fixed';
141
+ document.body.style.top = `-${scrollY}px`;
142
+ document.body.style.width = '100%';
143
+ }
144
+
145
+ /**
146
+ * Unlocks the page scroll and restores position.
147
+ */
148
+ export function unlockScroll() {
149
+ const scrollY = document.body.style.top;
150
+ document.body.style.position = '';
151
+ document.body.style.top = '';
152
+ document.body.style.width = '';
153
+ window.scrollTo(0, parseInt(scrollY || '0') * -1);
154
+ }