Skip to content

Add auto-cycling marquee effect to footer solution and comparison links#3647

Open
plainspace wants to merge 10 commits intofastrepl:mainfrom
plainspace:auto-cycle-web-footer-links
Open

Add auto-cycling marquee effect to footer solution and comparison links#3647
plainspace wants to merge 10 commits intofastrepl:mainfrom
plainspace:auto-cycle-web-footer-links

Conversation

@plainspace
Copy link

@plainspace plainspace commented Feb 4, 2026

What changed
Replaced the hover-based blur effect on the footer's "for [use case]" and "vs. [competitor]" links with an auto-cycling marquee that smoothly fades between different options.

Why we did it
Improved discoverability: The previous implementation required users to hover over the links to see different options, which was not discoverable and required interaction. Many users likely never knew multiple use cases and comparisons existed.

Better engagement: The auto-cycling effect highlights the variety of solutions and comparisons offered, without requiring any user action. It subtly showcases our different market segments (Sales, Recruiting, Consulting, etc.) and competitive positioning.

More elegant UX: The blur effect felt jarring and unclear. The new smooth fade transitions (500ms duration) feel more polished and elegant, aligning with the site's overall design aesthetic.

Technical details

  • Both links now cycle through their respective lists every 2 seconds
  • Fade transitions use opacity with 500ms duration for smooth in/out effects
  • The two links are staggered by 1 second so they don't change simultaneously, creating a more natural, less distracting pattern
  • Random initial selection and non-repeating random rotation ensure variety
  • Proper cleanup of intervals and timeouts to prevent memory leaks
  • Used cn utility for conditional className management
  • Added fixed-height containers to prevent layout shift during transitions

footer

Extras

  • Removed envelope icon in favor of ExternalLinkIcon based on link behaviour
  • Add text-balance to the home page hero heading and footer brand section for better wrapping across viewport widths

PS I couldn't access contributing guidlines so I'm YOLOing this.

@netlify
Copy link

netlify bot commented Feb 4, 2026

👷 Deploy request for hyprnote pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit 6f8fff3

@netlify
Copy link

netlify bot commented Feb 4, 2026

Deploy Preview for hyprnote-storybook canceled.

Name Link
🔨 Latest commit 6f8fff3
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote-storybook/deploys/6984a9874e562600076e80b1

Comment on lines 189 to 214
const useCaseInterval = setInterval(() => {
setIsUseCaseFading(true);
setTimeout(() => {
setUseCaseIndex((prev) =>
getNextRandomIndex(useCasesList.length, prev),
);
setIsUseCaseFading(false);
}, 500);
}, 2000);

let vsInterval: ReturnType<typeof setInterval>;
const vsDelay = setTimeout(() => {
vsInterval = setInterval(() => {
setIsVsFading(true);
setTimeout(() => {
setVsIndex((prev) => getNextRandomIndex(vsList.length, prev));
setIsVsFading(false);
}, 500);
}, 2000);
}, 1000);

return () => {
clearInterval(useCaseInterval);
clearTimeout(vsDelay);
if (vsInterval) clearInterval(vsInterval);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Critical Bug: Memory leak and state updates on unmounted component

The nested setTimeout calls inside each interval are not tracked or cleaned up. If the component unmounts while a setTimeout is pending (e.g., during the 500ms fade transition), those callbacks will still execute and attempt to call setUseCaseIndex, setIsUseCaseFading, setVsIndex, and setIsVsFading on an unmounted component.

What breaks: React will log warnings about setting state on unmounted components, and the timeouts will continue running after the component is destroyed, causing memory leaks.

Fix: Track the timeout IDs and clear them in the cleanup function:

const timeoutIds = useRef<Set<ReturnType<typeof setTimeout>>>(new Set());

useEffect(() => {
  // ... existing initialization code ...

  const useCaseInterval = setInterval(() => {
    setIsUseCaseFading(true);
    const tid = setTimeout(() => {
      setUseCaseIndex((prev) => getNextRandomIndex(useCasesList.length, prev));
      setIsUseCaseFading(false);
      timeoutIds.current.delete(tid);
    }, 500);
    timeoutIds.current.add(tid);
  }, 2000);

  // ... similar for vsInterval ...

  return () => {
    clearInterval(useCaseInterval);
    clearTimeout(vsDelay);
    if (vsInterval) clearInterval(vsInterval);
    timeoutIds.current.forEach(clearTimeout);
    timeoutIds.current.clear();
  };
}, []);

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

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.

1 participant