use-scroller 1.1.0 → 2.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.
Files changed (42) hide show
  1. package/README.md +406 -72
  2. package/dist/chunk-BDIJT5M4.js +1188 -0
  3. package/dist/core.d.ts +121 -0
  4. package/dist/core.js +36 -0
  5. package/dist/index.d.ts +51 -0
  6. package/dist/index.js +346 -0
  7. package/package.json +39 -26
  8. package/dist/cjs/hooks/use-iso-morphic-effect.d.ts +0 -2
  9. package/dist/cjs/hooks/use-iso-morphic-effect.js +0 -6
  10. package/dist/cjs/hooks/use-scroll.d.ts +0 -20
  11. package/dist/cjs/hooks/use-scroll.js +0 -90
  12. package/dist/cjs/hooks/use-window-scroll.d.ts +0 -13
  13. package/dist/cjs/hooks/use-window-scroll.js +0 -60
  14. package/dist/cjs/index.d.ts +0 -3
  15. package/dist/cjs/index.js +0 -19
  16. package/dist/cjs/scroller.d.ts +0 -10
  17. package/dist/cjs/scroller.js +0 -97
  18. package/dist/cjs/types.d.ts +0 -17
  19. package/dist/cjs/types.js +0 -2
  20. package/dist/cjs/utils.d.ts +0 -24
  21. package/dist/cjs/utils.js +0 -79
  22. package/dist/esm/hooks/use-iso-morphic-effect.d.ts +0 -2
  23. package/dist/esm/hooks/use-iso-morphic-effect.js +0 -3
  24. package/dist/esm/hooks/use-scroll.d.ts +0 -20
  25. package/dist/esm/hooks/use-scroll.js +0 -86
  26. package/dist/esm/hooks/use-window-scroll.d.ts +0 -13
  27. package/dist/esm/hooks/use-window-scroll.js +0 -56
  28. package/dist/esm/index.d.ts +0 -3
  29. package/dist/esm/index.js +0 -3
  30. package/dist/esm/scroller.d.ts +0 -10
  31. package/dist/esm/scroller.js +0 -93
  32. package/dist/esm/types.d.ts +0 -17
  33. package/dist/esm/types.js +0 -1
  34. package/dist/esm/utils.d.ts +0 -24
  35. package/dist/esm/utils.js +0 -72
  36. package/src/hooks/use-iso-morphic-effect.ts +0 -4
  37. package/src/hooks/use-scroll.ts +0 -95
  38. package/src/hooks/use-window-scroll.ts +0 -60
  39. package/src/index.ts +0 -3
  40. package/src/scroller.ts +0 -110
  41. package/src/types.ts +0 -21
  42. package/src/utils.ts +0 -90
package/README.md CHANGED
@@ -1,108 +1,442 @@
1
- ![npm](https://img.shields.io/npm/v/scroll-js)
2
- ![node](https://img.shields.io/node/v/scroll-js)
3
- ![license](https://img.shields.io/npm/l/scroll-js)
1
+ # use-scroller
4
2
 
5
- ## use-scroller
3
+ Scroll utilities for the web, with React hooks on top. Native scrolling stays in charge; the
4
+ library steps in only where the platform falls short.
6
5
 
7
- A light-weight library that will allow you to scroll any html element using simple react hooks provided by the package.
8
- In addition to providing extra scrolling features, this library also return a number of callback that can triger scroll operation depending on the user needs.
6
+ ## What it fixes
9
7
 
10
- <p align="middle">
11
- <img src="https://s9.gifyu.com/images/ezgif.com-gif-maker3092e8916a41884d.gif" width="400"/>
12
- <img src="https://s3.gifyu.com/images/ezgif.com-gif-maker-1c4ee7c66dcd4dd01.gif" width="400"/>
13
- </p>
8
+ - **Swipe tracks that drift.** On a phone, a slightly diagonal swipe on a horizontal carousel can
9
+ move the page, the carousel, or both. `dragScroll` decides once per gesture and sticks to one axis.
10
+ - **Modal scroll bleed on iOS.** `overflow: hidden` on the page doesn't stop iOS Safari from
11
+ scrolling it behind a modal. `lockScroll` adds a touch guard designed for iOS Safari (verify on a
12
+ device) and keeps the modal's own list scrollable.
13
+ - **Animations that fight the user.** `animateScroll` stops the moment the user touches, wheels or
14
+ presses a key, pauses CSS scroll snapping while it runs, and respects reduced motion.
15
+ - Also: scroll state (edges, progress, direction, velocity), scroll spy, in-view detection, scroll
16
+ restoration, and sticking to the bottom of a chat or log.
14
17
 
15
- ## Installation
18
+ ## Install
16
19
 
17
- ### React
20
+ ```sh
21
+ npm install use-scroller
22
+ ```
18
23
 
19
- ```javascript
20
- #YARN
21
- yarn add use-scroller
24
+ | Import | Contains | Needs React |
25
+ | ------------------- | ------------------------------------- | ----------- |
26
+ | `use-scroller` | React hooks plus everything from core | 18 or 19 |
27
+ | `use-scroller/core` | Framework-free functions | No |
22
28
 
23
- #NPM
24
- npm install use-scroller
29
+ React is an optional peer dependency, so the core entry works in any framework or none. Both
30
+ entries are ESM-only with types, and are safe to import during server rendering.
25
31
 
26
- #PNPM
27
- pnpm install use-scroller
32
+ A scroll target is an `HTMLElement` or `window` (`ScrollTarget`). Every function that sets
33
+ something up returns a cleanup function.
28
34
 
29
- ```
35
+ ## React hooks
30
36
 
31
- ## Usage
37
+ ### `useScroll`
32
38
 
33
- ### Carousel
39
+ `useScroll(target, options?)` gives animated scrolling for a ref or `'window'`. Hook options are
40
+ defaults; each call can override them.
34
41
 
35
- ```javascript
42
+ ```tsx
43
+ import { useRef } from 'react'
36
44
  import { useScroll } from 'use-scroller'
37
45
 
38
- export default function Carousel() {
39
- const { ref, scrollLeft, scrollRight } = useScroll<HTMLDivElement>()
46
+ function Feed() {
47
+ const ref = useRef<HTMLDivElement>(null)
48
+ const { scrollToEdge, scrollBy } = useScroll(ref, { animation: { type: 'spring' } })
49
+
50
+ async function toBottom() {
51
+ const result = await scrollToEdge('bottom').finished // 'completed' | 'interrupted' | 'cancelled'
52
+ if (result === 'interrupted') showToast('Stopped where you grabbed it')
53
+ }
54
+
40
55
  return (
41
56
  <>
42
- <button onClick={() => scrollLeft()} >
43
- Top
44
- </button>
45
- <button onClick={() => scrollRight()}>
46
- Next
47
- </button>
48
-
49
- <Box ref={ref} />
57
+ <button onClick={toBottom}>Latest</button>
58
+ <button onClick={() => scrollBy({ y: 300 })}>More</button>
59
+ <div ref={ref} style={{ overflow: 'auto', height: 300 }}>
60
+ …
61
+ </div>
50
62
  </>
51
63
  )
52
64
  }
53
65
  ```
54
66
 
55
- ### Box
67
+ The other actions are `scrollTo({ x, y })`, `scrollToElement(elementOrRef, { align, offset })` and
68
+ `cancel()`.
56
69
 
57
- ```javascript
58
- import { useScroll } from 'use-scroller'
70
+ ```ts
71
+ const page = useScroll('window')
72
+ page.scrollToElement(sectionRef, { align: 'start', offset: 64 }) // 64px sticky header
73
+ ```
74
+
75
+ Actions have stable identities, return a `ScrollHandle` (`{ finished, cancel() }`) and throw if the
76
+ ref isn't attached; `cancel()` returns nothing and does nothing then. A new animation on the same target cancels the previous one; unmounting cancels
77
+ a running one.
78
+
79
+ ### `useScrollState`
80
+
81
+ Live scroll state for a ref or `'window'`. Pass a selector to re-render only when that value changes.
82
+
83
+ ```tsx
84
+ const state = useScrollState(ref)
85
+ const atEnd = useScrollState(ref, (s) => s.atRight)
86
+ const progress = useScrollState('window', (s) => s.progressY)
87
+ ```
88
+
89
+ `ScrollState` fields: `x`, `y`, `minX`, `maxX`, `maxY`, `progressX`, `progressY` (0–1), `atTop`,
90
+ `atBottom`, `atLeft`, `atRight`, `canScrollX`, `canScrollY`, `directionX`, `directionY` (−1, 0, 1),
91
+ `velocityX`, `velocityY` (px/s) and `isScrolling`. Edges and progress are correct in RTL. Before
92
+ mount and on the server every position is 0, every edge is reached and nothing can scroll.
93
+
94
+ ### `useDragScroll`
95
+
96
+ Axis-locked swiping for a horizontal (or vertical) track.
97
+
98
+ ```tsx
99
+ const track = useRef<HTMLUListElement>(null)
100
+ useDragScroll(track, { axis: 'x', mouse: true })
101
+ ```
102
+
103
+ It sets `touch-action: pan-y pinch-zoom` on the track (`pan-x pinch-zoom` for `axis: 'y'`), so
104
+ cross-axis movement scrolls the page natively and pinch-zoom keeps working. A gesture
105
+ is judged after 8px: mostly sideways and the track follows the finger, otherwise it is left to the
106
+ browser. Release continues with momentum and lands on a CSS scroll-snap position if the track has
107
+ any. A drag doesn't fire a click on whatever is under the finger. Wheel, trackpad, keyboard and the scrollbar stay
108
+ native. `mouse: true` also lets a mouse drag the track.
109
+
110
+ ### `useScrollLock`
111
+
112
+ Locks page scrolling while `active` is true. Elements in `allow` stay scrollable and never chain to
113
+ the page.
114
+
115
+ ```tsx
116
+ const [open, setOpen] = useState(false)
117
+ const list = useRef<HTMLUListElement>(null)
118
+ useScrollLock(open, { allow: [list] })
119
+ ```
120
+
121
+ Locks are reference-counted, so nested modals work. The scrollbar's width is added as body padding
122
+ so the layout doesn't shift, and the page's scroll position is kept.
123
+
124
+ ### `useScrollSpy`
125
+
126
+ Returns the index of the section at the reading line, or −1 before the first one. Sections are refs
127
+ or element ids.
128
+
129
+ ```tsx
130
+ const active = useScrollSpy(['intro', 'install', apiRef], { offset: 64 })
131
+ ```
132
+
133
+ `offset` moves the reading line down from the root's top edge, usually by a sticky header's height.
134
+ `root` is a ref to a scroll container (default: the window). A missing id throws.
135
+
136
+ ### `useInView`
137
+
138
+ ```tsx
139
+ const ref = useRef<HTMLDivElement>(null)
140
+ const { inView, entry } = useInView(ref, { threshold: 0.3, once: true })
141
+ ```
142
+
143
+ Also takes `root` and `rootMargin`, like `IntersectionObserver`. With `once`, `inView` stays true
144
+ after the first intersection.
145
+
146
+ ### `useStickToBottom`
147
+
148
+ Keeps a chat, log or streaming reply pinned to its newest line.
149
+
150
+ ```tsx
151
+ function Chat({ messages }: { messages: Message[] }) {
152
+ const list = useRef<HTMLOListElement>(null)
153
+ const { isAtBottom, scrollToBottom } = useStickToBottom(list)
154
+
155
+ return (
156
+ <div style={{ position: 'relative' }}>
157
+ <ol ref={list} style={{ overflow: 'auto', height: 400 }}>
158
+ {messages.map((message) => (
159
+ <li key={message.id}>{message.text}</li>
160
+ ))}
161
+ </ol>
162
+ {!isAtBottom && <button onClick={() => scrollToBottom()}>↓ New messages</button>}
163
+ </div>
164
+ )
165
+ }
166
+ ```
167
+
168
+ It jumps to the bottom on mount. While at the bottom, it jumps to the new bottom whenever the
169
+ content grows or the element resizes: a new child, a child getting taller as a reply streams into it,
170
+ or text changing directly inside the element. The jump is instant so streamed text never lags
171
+ behind. Scrolling up more than 8px from the bottom stops the following; coming back within 8px
172
+ resumes it. `scrollToBottom(options?)` animates with the `animateScroll` options, follows again
173
+ right away, and returns a `ScrollHandle`. It throws if the ref isn't attached. The target must be
174
+ an element, not the window.
175
+
176
+ ### `useScrollRestoration`
177
+
178
+ Saves a scroll position under a key and puts it back on the next mount: after a reload, or when
179
+ the user comes back to a route.
180
+
181
+ ```tsx
182
+ const list = useRef<HTMLUListElement>(null)
183
+ useScrollRestoration(list, 'inbox')
184
+ useScrollRestoration('window', `page:${pathname}`) // pathname from your router, not `location`, so it renders on the server
185
+ ```
186
+
187
+ Options are `storage` (default `sessionStorage`) and `timeout` (default 3000 ms); `restoreScroll`
188
+ below has the details. A new key or element starts over with that key's saved position.
189
+
190
+ ## Core functions
191
+
192
+ Everything below is exported from both entries.
193
+
194
+ ### `animateScroll`
195
+
196
+ ```ts
197
+ import { animateScroll, easings, cubicBezier } from 'use-scroller/core'
198
+
199
+ const handle = animateScroll(
200
+ window,
201
+ { y: 800 },
202
+ {
203
+ animation: { type: 'tween', duration: 300, easing: easings.easeInOutCubic },
204
+ }
205
+ )
206
+ await handle.finished
207
+ ```
208
+
209
+ | Option | Default | Notes |
210
+ | ---------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
211
+ | `animation` | `{ type: 'tween' }` | `tween` (`duration` 450, `easing` easeOutCubic), `spring` (`stiffness` 170, `damping` 26, `mass` 1, `velocity`), or `instant` |
212
+ | `interruptible` | `true` | wheel, touchstart, pointerdown or keydown on the target stops it with `'interrupted'` |
213
+ | `respectReducedMotion` | `true` | jumps instantly when `prefers-reduced-motion: reduce` matches |
214
+
215
+ A missing axis doesn't move. Targets are clamped to the scroll range every frame. `easings` has
216
+ `linear`, `easeInCubic`, `easeOutCubic` and `easeInOutCubic`; `cubicBezier(0.25, 0.1, 0.25, 1)` builds
217
+ any CSS curve.
218
+
219
+ ### `scrollToEdge`, `scrollToElement`, `scrollBy`, `cancelScroll`
220
+
221
+ ```ts
222
+ scrollToEdge(list, 'bottom') // 'top' | 'bottom' | 'left' | 'right'; left and right respect RTL
223
+ scrollToElement(window, heading, { align: 'center', offset: 64 }) // 'start' | 'center' | 'end' | 'nearest'
224
+ scrollBy(list, { y: 200 }) // repeated calls add up while an animation is running
225
+ cancelScroll(list)
226
+ ```
227
+
228
+ All take the same options as `animateScroll` and return a `ScrollHandle`.
229
+
230
+ ### `observeScroll`
231
+
232
+ ```ts
233
+ const stop = observeScroll(list, (state) => console.log(state.progressY), { idleDelay: 120 })
234
+ ```
235
+
236
+ Emits a `ScrollState` once on subscribe, then at most once per frame when something changed,
237
+ including when the content or container resizes. `isScrolling` turns false after `idleDelay` ms
238
+ without scroll events.
239
+
240
+ ### `restoreScroll`
241
+
242
+ ```ts
243
+ const stop = restoreScroll(list, 'inbox', { storage: localStorage, timeout: 5000 })
244
+ ```
245
+
246
+ - The position is stored as JSON under `use-scroller:<key>`. With the default `sessionStorage` it
247
+ survives reloads and back/forward navigation in the same tab, but not a new tab.
248
+ - A saved position the content can already reach is written instantly. If the content isn't tall
249
+ enough yet (data still loading), it waits for the content to grow and restores as soon as the
250
+ position fits.
251
+ - Wheel, touch, pointer down or a key press on the target (the window, for a window target) during
252
+ the wait cancels the restore. The user's own scrolling wins.
253
+ - After `timeout` ms it stops waiting and scrolls as close to the saved position as the content
254
+ allows. A timeout outside 0–2147483647 ms throws a `RangeError`.
255
+ - Saving starts only once restoring has finished or been cancelled, so the initial position of 0
256
+ never overwrites the saved one. From then on it saves on scroll (at most once per frame), on
257
+ `pagehide`, and on cleanup.
258
+ - For `window`, it sets `history.scrollRestoration = 'manual'` while active, so the browser's own
259
+ restore can't move the page after ours, and puts the previous value back on cleanup.
260
+ - If the storage is blocked or throws on read (privacy settings, sandboxed iframes), it does
261
+ nothing: no restore, no saving, no error. A value under the key that isn't `{ x, y }` with finite
262
+ numbers is ignored. A failed write (storage full) is skipped and the next scroll tries again.
263
+
264
+ ### `stickToBottom`
265
+
266
+ ```ts
267
+ const sticky = stickToBottom(log, (isAtBottom) => {
268
+ newMessagesButton.hidden = isAtBottom
269
+ })
270
+ sticky.scrollToBottom({ animation: { type: 'spring' } })
271
+ sticky.stop()
272
+ ```
273
+
274
+ `onChange` is called once on attach, then only when the state changes. `scrollToBottom` throws after
275
+ `stop()`. Behavior is the same as `useStickToBottom` above.
276
+
277
+ ### `dragScroll`, `lockScroll`
278
+
279
+ ```ts
280
+ const stopDrag = dragScroll(track, { axis: 'x', mouse: false })
281
+ const unlock = lockScroll({ allow: [modalList] })
282
+ ```
283
+
284
+ The hooks above are thin wrappers around these.
285
+
286
+ ### `observeActiveSection`
287
+
288
+ ```ts
289
+ const stop = observeActiveSection(sections, (index) => highlight(index), {
290
+ root: window,
291
+ offset: 64,
292
+ })
293
+ ```
294
+
295
+ ### Low-level helpers
296
+
297
+ ```ts
298
+ readScroll(target) // { x, y, minX, maxX, maxY, viewportWidth, viewportHeight }
299
+ isScrollable(element, 'y')
300
+ findScrollParent(element, 'y') // nearest scrollable ancestor, or window
301
+ ```
302
+
303
+ ## Recipes
304
+
305
+ ### Infinite scroll
306
+
307
+ Load the next page when a sentinel after the last item comes within 800px of the viewport.
308
+
309
+ ```tsx
310
+ function Feed() {
311
+ const { items, hasMore, loading, loadMore } = useFeed()
312
+ const sentinel = useRef<HTMLDivElement>(null)
313
+ const { inView } = useInView(sentinel, { rootMargin: '0px 0px 800px 0px' })
314
+
315
+ useEffect(() => {
316
+ if (inView && hasMore && !loading) loadMore()
317
+ }, [inView, hasMore, loading, loadMore])
59
318
 
60
- export default function ScrollBox() {
61
- const { ref, scrollLeft, scrollRight, scrollTop, scrollBotom, scrollCenter } = useScroll<HTMLDivElement>()
62
319
  return (
63
320
  <>
64
- <button onClick={() => scrollCenter()} >
65
- Center
66
- </button>
67
- <button onClick={() => scrollLeft()} >
68
- Left
69
- </button>
70
- <button onClick={() => scrollRight()}>
71
- Right
72
- </button>
73
- <button onClick={() => scrollTop()}>
74
- Top
75
- </button>
76
- <button onClick={() => scrollBotom()}>
77
- Bottom
78
- </button>
79
- # Box component
80
- <Box ref={ref} />
321
+ {items.map((item) => (
322
+ <Card key={item.id} item={item} />
323
+ ))}
324
+ {hasMore && <div ref={sentinel} />}
81
325
  </>
82
326
  )
83
327
  }
84
328
  ```
85
329
 
86
- ## API Documentation
330
+ `loading` is a dependency, so the effect runs again after each page. If a short page leaves the
331
+ sentinel in view, the next one loads without waiting for another scroll. Inside a scroll container,
332
+ also pass `root: listRef`.
333
+
334
+ ### Hide the header while scrolling down
335
+
336
+ ```tsx
337
+ function Header() {
338
+ const hidden = useScrollState('window', (s) => s.directionY === 1 && s.y > 64)
339
+ return <header className={hidden ? 'header header--hidden' : 'header'}>…</header>
340
+ }
341
+ ```
342
+
343
+ `directionY` keeps its last value when scrolling stops, so the header stays hidden until the user
344
+ scrolls up. The selector returns a boolean, so the header re-renders only when it flips. For a
345
+ scroll container, pass its ref instead of `'window'`.
346
+
347
+ ### Load older messages without a jump
348
+
349
+ No JavaScript is needed to keep the view still when older messages are inserted above it. Scroll
350
+ anchoring (`overflow-anchor: auto`, the default) keeps the visible messages in place. Anchoring does
351
+ nothing at scroll position 0, so start loading before the user reaches the top:
352
+
353
+ ```tsx
354
+ const list = useRef<HTMLOListElement>(null)
355
+ const firstMessage = useRef<HTMLLIElement>(null)
356
+ const { inView } = useInView(firstMessage, { root: list, rootMargin: '600px 0px 0px 0px' })
357
+ ```
358
+
359
+ Scroll anchoring is on by default in Chromium and Firefox. Safari went without it for a long time,
360
+ so check the versions you support. Where it's missing, read `scrollHeight - scrollTop` before the
361
+ insert and set `scrollTop` back from it in a layout effect after.
362
+
363
+ ## CSS first
364
+
365
+ Several scroll problems need no JavaScript. Reach for these before the library.
366
+
367
+ | Problem | CSS | When you still need use-scroller |
368
+ | ----------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
369
+ | A nested list scrolls the page when it hits its end | `overscroll-behavior: contain` on the list | The page must not move at all behind a modal on iOS, including when touching the backdrop: `lockScroll` |
370
+ | Diagonal swipes on a carousel move the page | `touch-action: pan-x pinch-zoom` on the track | You also want vertical swipes on the track to scroll the page: `dragScroll` |
371
+ | Carousel should stop on cards | `scroll-snap-type: x mandatory` + `scroll-snap-align: start` | Only when you also need axis locking or mouse dragging; `dragScroll` lands on the same snap points |
372
+ | Layout jumps when content starts or stops overflowing | `scrollbar-gutter: stable` on the scroll container | Never for this |
373
+ | Anchor links hide under a sticky header | `scroll-margin-top` on the targets | You need to control duration or easing, or stop when the user scrolls: `scrollToElement` with `offset` |
374
+
375
+ ## Known limitations
376
+
377
+ - Scroll spy highlights the last section once the root is scrolled to the end, even if a short last
378
+ section never reached the reading line.
379
+ - Hooks that take a ref pick up its element on the commit that attaches it. An element attached in a
380
+ later commit that doesn't re-render the hook's component goes unnoticed until that component renders.
381
+ - `dragScroll` assumes `writing-mode: horizontal-tb`.
382
+ - Release momentum after a drag ignores `prefers-reduced-motion`, as native momentum does.
383
+ Programmatic scrolls (`animateScroll` and everything built on it) respect it.
384
+ - `stickToBottom` watches the element and its direct children. Growth that doesn't resize either
385
+ (for example an absolutely positioned grandchild) isn't followed.
386
+ - `restoreScroll` saves one position per key, not per history entry. Two history entries that
387
+ share a key share a position, so put the route or item id in the key.
388
+ - `restoreScroll` on the window and a router's own scroll restoration both move the page. Use one.
389
+
390
+ ## Browser support
391
+
392
+ Automated tests run in headless Chromium. These still need verification on real iOS Safari and
393
+ Android devices:
394
+
395
+ - the gesture features: `dragScroll` and the iOS part of `lockScroll`;
396
+ - `restoreScroll` saving on `pagehide` in iOS Safari, and pages coming back from the back/forward
397
+ cache (the page returns as it was, so nothing should need restoring);
398
+ - `stickToBottom` when content grows during iOS momentum scrolling or the rubber-band bounce at the
399
+ bottom.
400
+
401
+ ## Migrating from v1
87
402
 
88
- #### Available hooks
403
+ v2 is a rewrite. Animation options are objects, scroll state moved to its own hook, every action
404
+ names its axis, and the package is ESM-only.
89
405
 
90
- | Hook | Description |
91
- | ----------------- | ----------------------------- |
92
- | `useScroll` | Handles element scroll events |
93
- | `useWindowScroll` | Handles window scroll events |
406
+ | v1 | v2 |
407
+ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- |
408
+ | `const { ref } = useScroll()` | `const ref = useRef(null)`, then `useScroll(ref)` |
409
+ | `useScroll({ animation: 'easing', easing: 'ease-out', duration: 300 })` | `useScroll(ref, { animation: { type: 'tween', duration: 300, easing: easings.easeOutCubic } })` |
410
+ | `easing: 'linear' \| 'ease-in' \| 'ease-out' \| 'ease-in-out'` | `easings.linear`, `easeInCubic`, `easeOutCubic`, `easeInOutCubic` (cubic, not quadratic), or `cubicBezier(…)` |
411
+ | `animation: 'momentum'`, `friction` | `animation: { type: 'spring', stiffness, damping, mass }` |
412
+ | `animation: 'native'`, `behavior` | Removed. Use `element.scrollTo({ behavior: 'smooth' })`, or `{ type: 'instant' }` |
413
+ | `direction: 'horizontal' \| 'vertical'` | Removed. Each call says which axis: `scrollTo({ x })`, `scrollTo({ y })` |
414
+ | `respectMotionPreference` | `respectReducedMotion` (default `true`) |
415
+ | `const { state } = useScroll()` | `useScrollState(ref)` |
416
+ | `state.left`, `state.top` | `x`, `y` |
417
+ | `isScrolledLeft`, `isScrolledRight`, `isScrolledTop`, `isScrolledBottom` | `atLeft`, `atRight`, `atTop`, `atBottom` |
418
+ | `maxScrollLeft`, `maxScrollTop` | `maxX` (and `minX` in RTL), `maxY` |
419
+ | `scrollPercentageX`, `scrollPercentageY` (0–100) | `progressX`, `progressY` (0–1) |
420
+ | `isScrollable` | `canScrollX`, `canScrollY` |
421
+ | `scrollLeft()`, `scrollRight()`, `scrollTop()`, `scrollBottom()` | `scrollToEdge('left' \| 'right' \| 'top' \| 'bottom')` |
422
+ | `scrollLeft(n)`, `scrollTop(n)` (scroll to position `n`) | `scrollTo({ x: n })`, `scrollTo({ y: n })` |
423
+ | `scrollTo(n)` | `scrollTo({ x: n })` or `scrollTo({ y: n })` |
424
+ | `scrollCenter()` | `scrollTo({ y: readScroll(ref.current).maxY / 2 })` |
425
+ | `cancelScroll()` | `cancel()` |
426
+ | `useWindowScroll()` | `useScroll('window')` |
427
+ | `useWindowScroll().state` | `useScrollState('window')` |
428
+ | `useWindowScroll().scrollToTarget(el)` | `useScroll('window').scrollToElement(el, { offset })` |
429
+ | `require('use-scroller')` | `import` (ESM-only) |
430
+ | Actions do nothing while the ref is unattached | Actions throw (`cancel()` does nothing) |
431
+ | `createEasingAnimation`, `createMomentumAnimation` | Removed. Use `animateScroll` |
432
+ | Types `ScrollOptions`, `EasingOptions`, `UseScrollReturn` | `AnimateScrollOptions`, `ScrollAnimation`, `Easing`, `ScrollActions`, `ScrollState` |
94
433
 
95
- #### useScroll(options)
434
+ ## Example app
96
435
 
97
- | Option | Type | Description |
98
- | -------------- | ------------------------ | ------------------------------------ |
99
- | `direction` | `horizontal or vertical` | Set the desired scroll direction |
100
- | `duration` | `number` | Set the animation duration to scroll |
101
- | `easingOption` | `EasingOptions` | Set the type of animation to scroll |
436
+ `example/` has a demo of every feature, with native and enhanced versions side by side for the touch
437
+ ones. Run `npm install && npm run dev` inside it; the dev server listens on your LAN so you can open
438
+ it on a phone.
102
439
 
103
- #### useWindowScroll(options)
440
+ ## License
104
441
 
105
- | Option | Type | Description |
106
- | -------------- | --------------- | ------------------------------------ |
107
- | `duration` | `number` | Set the animation duration to scroll |
108
- | `easingOption` | `EasingOptions` | Set the type of animation to scroll |
442
+ MIT