Skip to content

Commit 3c8ae2a

Browse files
committed
initial version
1 parent 79c6248 commit 3c8ae2a

29 files changed

+1218
-0
lines changed

.gitignore

+175
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
2+
3+
# Logs
4+
5+
logs
6+
_.log
7+
npm-debug.log_
8+
yarn-debug.log*
9+
yarn-error.log*
10+
lerna-debug.log*
11+
.pnpm-debug.log*
12+
13+
# Caches
14+
15+
.cache
16+
17+
# Diagnostic reports (https://nodejs.org/api/report.html)
18+
19+
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
20+
21+
# Runtime data
22+
23+
pids
24+
_.pid
25+
_.seed
26+
*.pid.lock
27+
28+
# Directory for instrumented libs generated by jscoverage/JSCover
29+
30+
lib-cov
31+
32+
# Coverage directory used by tools like istanbul
33+
34+
coverage
35+
*.lcov
36+
37+
# nyc test coverage
38+
39+
.nyc_output
40+
41+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
42+
43+
.grunt
44+
45+
# Bower dependency directory (https://bower.io/)
46+
47+
bower_components
48+
49+
# node-waf configuration
50+
51+
.lock-wscript
52+
53+
# Compiled binary addons (https://nodejs.org/api/addons.html)
54+
55+
build/Release
56+
57+
# Dependency directories
58+
59+
node_modules/
60+
jspm_packages/
61+
62+
# Snowpack dependency directory (https://snowpack.dev/)
63+
64+
web_modules/
65+
66+
# TypeScript cache
67+
68+
*.tsbuildinfo
69+
70+
# Optional npm cache directory
71+
72+
.npm
73+
74+
# Optional eslint cache
75+
76+
.eslintcache
77+
78+
# Optional stylelint cache
79+
80+
.stylelintcache
81+
82+
# Microbundle cache
83+
84+
.rpt2_cache/
85+
.rts2_cache_cjs/
86+
.rts2_cache_es/
87+
.rts2_cache_umd/
88+
89+
# Optional REPL history
90+
91+
.node_repl_history
92+
93+
# Output of 'npm pack'
94+
95+
*.tgz
96+
97+
# Yarn Integrity file
98+
99+
.yarn-integrity
100+
101+
# dotenv environment variable files
102+
103+
.env
104+
.env.development.local
105+
.env.test.local
106+
.env.production.local
107+
.env.local
108+
109+
# parcel-bundler cache (https://parceljs.org/)
110+
111+
.parcel-cache
112+
113+
# Next.js build output
114+
115+
.next
116+
out
117+
118+
# Nuxt.js build / generate output
119+
120+
.nuxt
121+
dist
122+
123+
# Gatsby files
124+
125+
# Comment in the public line in if your project uses Gatsby and not Next.js
126+
127+
# https://nextjs.org/blog/next-9-1#public-directory-support
128+
129+
# public
130+
131+
# vuepress build output
132+
133+
.vuepress/dist
134+
135+
# vuepress v2.x temp and cache directory
136+
137+
.temp
138+
139+
# Docusaurus cache and generated files
140+
141+
.docusaurus
142+
143+
# Serverless directories
144+
145+
.serverless/
146+
147+
# FuseBox cache
148+
149+
.fusebox/
150+
151+
# DynamoDB Local files
152+
153+
.dynamodb/
154+
155+
# TernJS port file
156+
157+
.tern-port
158+
159+
# Stores VSCode versions used for testing VSCode extensions
160+
161+
.vscode-test
162+
163+
# yarn v2
164+
165+
.yarn/cache
166+
.yarn/unplugged
167+
.yarn/build-state.yml
168+
.yarn/install-state.gz
169+
.pnp.*
170+
171+
# IntelliJ based IDEs
172+
.idea
173+
174+
# Finder (MacOS) folder config
175+
.DS_Store

.prettierignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
generated/

.prettierrc

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"semi": false,
3+
"singleQuote": true
4+
}

README.md

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# Next Scroll Restoration
2+
3+
A robust scroll restoration library for Next.js App Router that supports custom
4+
scrollable elements.
5+
6+
# Purpose
7+
8+
Next Scroll Restoration solves the problem of preserving scroll positions for custom scrollable elements in Next.js applications, which is not natively supported by Next.js's built-in scroll restoration feature.
9+
10+
## Features
11+
12+
- 🔄 Preserves scroll position for custom scrollable elements
13+
- 🚀 Works seamlessly with Next.js App Router and its built-in scroll restoration
14+
- 🖱️ Supports multiple scrollable areas within a single page
15+
- 🔧 Fine-grained control over scroll behavior via URL parameters
16+
- 🏎️ Performance-optimized with batched updates to sessionStorage
17+
- 📦 Minimal setup required
18+
- 🧠 Handles edge cases like same-page navigation, initial page load, hard reload, and more
19+
20+
## Installation
21+
22+
```bash
23+
npm install next-scroll-restoration
24+
```
25+
26+
## Quick Start
27+
28+
```tsx
29+
import React from 'react'
30+
import { ScrollRestoration } from 'next-scroll-restoration'
31+
32+
export default function RootLayout({
33+
children,
34+
}: {
35+
children: React.ReactNode
36+
}) {
37+
return (
38+
<html>
39+
<body>
40+
{children}
41+
<ScrollRestoration />
42+
</body>
43+
</html>
44+
)
45+
}
46+
```
47+
48+
## Usage
49+
50+
1. Add the `ScrollRestoration` component to your root layout.
51+
2. Mark scrollable elements with the `data-scroll-restoration-id` attribute:
52+
53+
```tsx
54+
<div
55+
data-scroll-restoration-id="container"
56+
style={{
57+
height: '100dvh',
58+
overflowY: 'scroll',
59+
}}
60+
>
61+
Your scrollable content
62+
</div>
63+
```
64+
65+
3. Use URL parameters to control scroll behavior:
66+
- `?scroll=false` or `?scroll=0`: Disable scroll restoration
67+
- `?scroll=true` or `?scroll=1`: Force scroll to top (useful for same-page navigation)
68+
69+
## How It Works
70+
71+
- Designed to work alongside Next.js's built-in scroll restoration feature
72+
- Handles scroll restoration for elements explicitly marked with `data-scroll-restoration-id` attribute
73+
- Uses `sessionStorage` to persist scroll positions across page reloads
74+
- Utilizes React hooks and Next.js routing for efficient state management
75+
- Handles various navigation scenarios including history push/forward/reload
76+
- Handles Next.js App Router features like async Components, nested Suspense boundaries, and SSG correctly
77+
- Implements a pre-hydration script to eliminate flickering (experimental: optional)
78+
79+
## Important Considerations
80+
81+
1. **Same-page navigation**: To ensure scroll-to-top behavior, use:
82+
83+
```tsx
84+
<Link href="/same-page?scroll=true">Link</Link>
85+
```
86+
87+
2. **Page navigation with scroll: false**: When using Next.js scroll options, also specify in the URL:
88+
89+
```tsx
90+
<Link href="/another-page?scroll=false" scroll={false}>
91+
Link
92+
</Link>
93+
```
94+
95+
```tsx
96+
router.push('/another-page?scroll=false', { scroll: false })
97+
```
98+
99+
3. **Search query cleanup**: The `scroll` search query is automatically removed after scroll restoration.
100+
101+
## Advanced Usage: Scroll Restoration Before Hydration
102+
103+
> Experimental (React 19+ Only)
104+
105+
For cases where you notice flickering on initial page load or first navigation,
106+
you can optionally use the `ScrollRestorationBeforeHydration` component:
107+
108+
```tsx
109+
import React from 'react'
110+
import {
111+
ScrollRestoration,
112+
experimental_ScrollRestorationBeforeHydration as ScrollRestorationBeforeHydration,
113+
} from 'next-scroll-restoration'
114+
115+
export default function RootLayout({
116+
children,
117+
}: {
118+
children: React.ReactNode
119+
}) {
120+
return (
121+
<html>
122+
<body>
123+
{children}
124+
<ScrollRestoration>
125+
<ScrollRestorationBeforeHydration />
126+
</ScrollRestoration>
127+
</body>
128+
</html>
129+
)
130+
}
131+
```
132+
133+
This component injects a script that runs before React hydration, restoring scroll position immediately after content becomes readable.
134+
135+
## Browser Support
136+
137+
Compatible with all modern browsers that support `sessionStorage` and
138+
`ResizeObserver`.

__tests__/build-output.test.ts

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { test, expect } from 'vitest'
2+
import { execSync } from 'child_process'
3+
import fs from 'fs'
4+
import path from 'path'
5+
6+
test('build script produces correct output', async () => {
7+
// Clean up before test
8+
execSync('rm -rf dist generated')
9+
10+
// Run build script
11+
execSync('bun run build')
12+
13+
// Check if dist directory exists
14+
expect(fs.existsSync('dist')).toBe(true)
15+
16+
// Check if all expected files are present
17+
const expectedFiles = [
18+
'index.cjs',
19+
'index.cjs.map',
20+
'index.d.cts',
21+
'index.d.ts',
22+
'index.js',
23+
'index.js.map',
24+
]
25+
expectedFiles.forEach((file) => {
26+
expect(fs.existsSync(path.join('dist', file))).toBe(true)
27+
})
28+
29+
// Read the content of index.cjs and index.js
30+
const cjsContent = fs.readFileSync('dist/index.cjs', 'utf-8')
31+
const esContent = fs.readFileSync('dist/index.js', 'utf-8')
32+
33+
// Check that $$INLINE_SCRIPT does not appear in the output
34+
expect(cjsContent).not.toContain('$$INLINE_SCRIPT')
35+
expect(esContent).not.toContain('$$INLINE_SCRIPT')
36+
37+
// Check that ResizeObserver is included in the inlined script
38+
expect(cjsContent).toContain('ResizeObserver')
39+
expect(esContent).toContain('ResizeObserver')
40+
41+
// Clean up after test
42+
execSync('rm -rf dist generated')
43+
})

bun.lockb

115 KB
Binary file not shown.

examples/nextjs/.eslintrc.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "next/core-web-vitals"
3+
}

0 commit comments

Comments
 (0)