Repository navigation
feat(site): Lenis smooth scrolling + house motion tokens #523
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
0584904
feat(scroll): add Aperture Reveal
hari a320171
feat(demo): add Aperture Reveal dawn climb demo
hari af199d6
docs(scroll): document Aperture Reveal and add October 2026 changelog
hari 3334ed0
fix(scroll): address Aperture Reveal review feedback
hari 5711c5d
fix(demo): align route details with the headline's left edge
hari d7e6a0e
refactor(scroll): style Aperture Reveal with Tailwind
hari 42d376b
feat(site): smooth wheel scrolling with Lenis
hari ca3164a
feat(site): house motion tokens and fix ignored easing classes
hari cb92484
docs: smooth scrolling guidance and October changelog for motion work
hari a98cebd
fix(site): smooth scroll review feedback
hari 5d736d1
Merge main into feat/smooth-scroll
hari File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| "use client"; | ||
|
|
||
| import Lenis from "lenis"; | ||
| import { cancelFrame, frame } from "motion/react"; | ||
| import { useEffect } from "react"; | ||
|
|
||
| import "lenis/dist/lenis.css"; | ||
|
|
||
| /** | ||
| * Site-wide smooth wheel scrolling. | ||
| * | ||
| * Wheel input is interpolated so scroll-linked animations move continuously instead of in | ||
| * ~100px wheel steps. Touch and keyboard keep native scrolling. Lenis advances on Motion's | ||
| * frame loop, so scroll position and Motion animations share one clock per frame. | ||
| */ | ||
| export function SmoothScroll() { | ||
| useEffect(() => { | ||
| const lenis = new Lenis({ | ||
| // Fraction of the remaining distance covered each frame. Higher feels tighter. | ||
| lerp: 0.14, | ||
| wheelMultiplier: 1.1, | ||
| // Sidebars, code blocks, menus, and contained previews keep their own scrolling. | ||
| allowNestedScroll: true, | ||
| prevent: (node) => node.closest("[data-lenis-prevent], [role='dialog']") !== null, | ||
| // Same-page hash links scroll through Lenis, so an in-flight wheel glide can't pull | ||
| // the page back from the heading. Targets honor scroll-margin like a native jump. | ||
| anchors: true, | ||
| stopInertiaOnNavigate: true, | ||
| }); | ||
|
|
||
| if (process.env.NODE_ENV !== "production") { | ||
| // Handy for inspecting scroll state from the console during development. | ||
| (window as unknown as { __lenis?: Lenis }).__lenis = lenis; | ||
| } | ||
|
|
||
| const update = ({ timestamp }: { timestamp: number }) => lenis.raf(timestamp); | ||
| frame.update(update, true); | ||
|
|
||
| // Lenis must not keep scrolling the page under a scroll lock. Radix dialogs and menus | ||
| // set `data-scroll-locked`; useLockBody (SplitReveal) sets inline overflow and position. | ||
| const html = document.documentElement; | ||
| const body = document.body; | ||
| const isLocked = () => | ||
| body.hasAttribute("data-scroll-locked") || | ||
| body.style.position === "fixed" || | ||
| body.style.overflow === "hidden" || | ||
| html.style.overflow === "hidden"; | ||
| const syncLock = () => { | ||
| const locked = isLocked(); | ||
| if (locked && !lenis.isStopped) lenis.stop(); | ||
| else if (!locked && lenis.isStopped) lenis.start(); | ||
| }; | ||
| const observer = new MutationObserver(syncLock); | ||
| observer.observe(body, { attributes: true, attributeFilter: ["data-scroll-locked", "style"] }); | ||
| observer.observe(html, { attributes: true, attributeFilter: ["style"] }); | ||
| syncLock(); | ||
|
|
||
| return () => { | ||
| observer.disconnect(); | ||
| cancelFrame(update); | ||
| lenis.destroy(); | ||
| }; | ||
| }, []); | ||
|
|
||
| return null; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| /** | ||
| * Motion tokens for JavaScript animation (Motion, WAAPI). Mirrors the --ease-* tokens in | ||
| * styles/globals.css; keep both in sync. | ||
| */ | ||
|
|
||
| export type CubicBezier = readonly [number, number, number, number]; | ||
|
|
||
| export const ease = { | ||
| /** UI responses: hover, press, menus, toasts. */ | ||
| out: [0.22, 1, 0.36, 1], | ||
| /** Exits only, kept short. */ | ||
| in: [0.64, 0, 0.78, 0], | ||
| /** On-screen moves between two states. */ | ||
| inOut: [0.4, 0, 0, 1], | ||
| /** Large, expressive moves: page transitions, hero reveals. */ | ||
| glide: [0.65, 0, 0.05, 1], | ||
| /** Micro feedback that should feel instant. */ | ||
| snap: [0.16, 1, 0.3, 1], | ||
| /** Playful arrivals with a small overshoot. */ | ||
| overshoot: [0.34, 1.56, 0.64, 1], | ||
| } as const satisfies Record<string, CubicBezier>; | ||
|
|
||
| /** Seconds, for Motion. Multiply by 1000 for CSS or WAAPI. */ | ||
| export const duration = { | ||
| instant: 0.12, | ||
| fast: 0.2, | ||
| base: 0.32, | ||
| slow: 0.6, | ||
| epic: 1.2, | ||
| } as const; | ||
|
|
||
| /** Delay between items in a list or a split headline. */ | ||
| export const stagger = { | ||
| tight: 0.03, | ||
| base: 0.05, | ||
| loose: 0.08, | ||
| } as const; | ||
|
|
||
| export function toCssBezier([x1, y1, x2, y2]: CubicBezier) { | ||
| return `cubic-bezier(${x1}, ${y1}, ${x2}, ${y2})`; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
stopInertiaOnNavigateonly resets Lenis for links whose pathname changes, whileanchorsremains disabled. If a user clicks the plain hash links incomponents/toc.tsxor the skip link while wheel interpolation is still running, the browser performs the native anchor jump but Lenis remains in its smooth-scrolling state and the next frame writes the previous animated position, pulling the page away from the requested heading. Enable Lenis anchor handling or explicitly reset it for same-page hash navigation.Useful? React with 👍 / 👎.