Skip to content

feat: make navbar re-appear on upward scroll#8808

Open
shivxmsharma wants to merge 1 commit intonodejs:mainfrom
shivxmsharma:feat/navbar-scroll-reveal
Open

feat: make navbar re-appear on upward scroll#8808
shivxmsharma wants to merge 1 commit intonodejs:mainfrom
shivxmsharma:feat/navbar-scroll-reveal

Conversation

@shivxmsharma
Copy link
Copy Markdown

Description

This PR implements a "scroll-up-to-reveal" behavior for the navigation bar on the website, giving desktop users more screen real estate while scrolling through content.

When scrolling down, the navbar slides out of view. When scrolling up (or returning to the top of the page), it smoothly slides back into view.

Key implementation details:

  • Added a new useScrollDirection client hook that uses requestAnimationFrame for high-performance, flicker-free scroll tracking
  • Added a SCROLL_THRESHOLD of 10px to avoid direction flips on micro-scrolls
  • Integrated the hook into WithNavBar to apply a hidden CSS class when scrolling down
  • The sticky + hide behavior is scoped to the xl breakpoint (≥1280px) only to avoid side effects on the mobile navigation layout
  • CSS transitions use transform: translateY(-100%) for smooth, hardware-accelerated animations

Validation

  • On a desktop viewport, scrolling down causes the navbar to slide up and out of view
  • Scrolling back up reveals the navbar with a smooth transition
  • At scrollY === 0, the navbar is always visible regardless of previous scroll direction
  • On mobile/tablet viewports, the navbar behaves exactly as before — no regression

Related Issues

Addresses #8699

Check List

  • I have read the Contributing Guidelines and made commit messages that follow the guideline.
  • I have run pnpm format to ensure the code follows the style guide.
  • I have run pnpm test to check if all tests are passing.
  • I have run pnpm build to check if the website builds without errors.
  • I've covered new added functionality with unit tests if necessary.

Copilot AI review requested due to automatic review settings April 7, 2026 13:52
@shivxmsharma shivxmsharma requested a review from a team as a code owner April 7, 2026 13:52
@cursor
Copy link
Copy Markdown

cursor bot commented Apr 7, 2026

PR Summary

Low Risk
Low risk UI behavior change scoped to desktop (xl) via CSS, with minimal logic and unit test coverage for the new scroll-direction hook.

Overview
Implements a desktop-only scroll-up-to-reveal navbar behavior: WithNavBar now conditionally applies a hidden class when useScrollDirection() reports scrolling down.

Adds a new useScrollDirection hook (client + server guard export) with a small threshold and requestAnimationFrame throttling, plus unit tests, and introduces navBarWrapper/hidden styles to animate the navbar off-screen at the xl breakpoint.

Reviewed by Cursor Bugbot for commit b98e45d. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nodejs-org Ready Ready Preview Apr 7, 2026 1:54pm

Request Review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 7, 2026

👋 Codeowner Review Request

The following codeowners have been identified for the changed files:

Team reviewers: @nodejs/nodejs-website

Please review the changes when you have a chance. Thank you! 🙏

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements a “scroll-up-to-reveal” navbar behavior for the site by tracking scroll direction in a new client hook and toggling a CSS transform to hide/show the header area (desktop-only via xl: utilities).

Changes:

  • Added useScrollDirection hook (client + server stub) with RAF-based scroll tracking and a threshold, plus unit tests.
  • Added navBarWrapper / hidden styles to translate the header out of view on downward scroll at xl breakpoint.
  • Integrated the hook into WithNavBar to toggle the new wrapper classes.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/ui-components/src/Containers/NavBar/index.module.css Adds navBarWrapper sticky + transform transition styles and a hidden state.
apps/site/hooks/client/useScrollDirection.ts New client hook to detect scroll direction with RAF and a 10px threshold.
apps/site/hooks/client/tests/useScrollDirection.test.mjs Unit tests for the new hook behavior.
apps/site/hooks/client/index.ts Re-exports the new client hook.
apps/site/hooks/server/useScrollDirection.ts Server stub to prevent calling the hook from RSC.
apps/site/hooks/server/index.ts Re-exports the new server stub.
apps/site/components/withNavBar.tsx Applies wrapper classes based on scroll direction.
Comments suppressed due to low confidence (1)

apps/site/components/withNavBar.tsx:65

  • This wrapper now contains SkipToContentButton, WithBanner, and NavBar, but the navBarWrapper styling applies xl:sticky and the hidden transform to the entire wrapper. That means the banner (and skip link) will also become sticky and slide out of view on downward scroll, which doesn't match the PR description of hiding only the navbar and may reduce desktop screen real estate. Consider applying the wrapper class only around the NavBar element (or splitting into separate wrappers) so banner behavior remains unchanged.
  return (
    <div
      className={classNames(styles.navBarWrapper, {
        [styles.hidden]: scrollDirection === 'down',
      })}
    >
      <SkipToContentButton>
        {t('components.common.skipToContent')}
      </SkipToContentButton>

      <WithBanner section="index" />

      <NavBar
        navItems={navigationItems.map(([, { label, link, target }]) => ({

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +10 to +20
const [scrollDirection, setScrollDirection] = useState<ScrollDirection>(null);
const lastScrollY = useRef(0);
const ticking = useRef(false);

useEffect(() => {
const updateScrollDirection = () => {
const currentScrollY = window.scrollY;

if (currentScrollY <= 0) {
setScrollDirection(null);
lastScrollY.current = currentScrollY;
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lastScrollY is initialized to 0 and never set to the current window.scrollY on mount. If the page loads/restores at a non-zero scroll position (e.g., back/forward cache, anchor links), the first scroll event can mis-detect direction (e.g., scrolling up can still be reported as down). Initialize lastScrollY.current from window.scrollY when the effect runs (and consider keeping scrollDirection unchanged until you have a baseline).

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +47
const onScroll = () => {
if (!ticking.current) {
ticking.current = true;
window.requestAnimationFrame(updateScrollDirection);
}
};

window.addEventListener('scroll', onScroll, { passive: true });

return () => window.removeEventListener('scroll', onScroll);
}, []);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The requestAnimationFrame callback can still run after the component unmounts, which can call setScrollDirection on an unmounted component. Track the RAF id and cancel it in the cleanup (or guard updates with an isMounted ref) to avoid potential React warnings in fast route transitions.

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +33
beforeEach(() => {
scrollY = 0;

Object.defineProperty(window, 'scrollY', {
get: () => scrollY,
configurable: true,
});

originalRAF = window.requestAnimationFrame;
Object.defineProperty(window, 'requestAnimationFrame', {
value: cb => {
cb();
return 1;
},
writable: true,
configurable: true,
});
});

afterEach(() => {
window.requestAnimationFrame = originalRAF;
});
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test overrides window.scrollY via Object.defineProperty but doesn't restore the original property descriptor in afterEach. That can leak into later tests and create order-dependent failures. Capture the original descriptor/value in beforeEach and restore it in afterEach (similar to how requestAnimationFrame is restored).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b98e45d. Configure here.


const useScrollDirection = (): ScrollDirection => {
const [scrollDirection, setScrollDirection] = useState<ScrollDirection>(null);
const lastScrollY = useRef(0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect direction on restored scroll position

Medium Severity

lastScrollY is initialized to 0 and never synced with the actual window.scrollY when the effect mounts. If the browser restores a non-zero scroll position (page refresh, back-navigation), the first scroll event compares against 0 instead of the real position. This causes scrolling up from, say, scrollY=500 to 490 to be reported as 'down' (since 490 > 0), incorrectly hiding the navbar. Initializing lastScrollY.current = window.scrollY at the start of the useEffect callback would fix this.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b98e45d. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants