The ScrollUp component (also known as ScrollToTop) provides a convenient button that appears when users scroll down, allowing them to quickly return to the top of the page with a smooth animation.
Features
Automatic visibility based on scroll position
Smooth scroll animation
Fixed positioning in bottom-right corner
Hover effects with shadow
Responsive design
High z-index to stay above other content
Client-side only component
Basic Usage
import ScrollToTop from "@/components/ScrollToTop" ;
export default function Layout ({ children }) {
return (
<>
{ children }
< ScrollToTop />
</>
);
}
This component must be used in a client component or wrapped with "use client" since it uses React hooks and browser APIs.
How It Works
The component uses React hooks to:
Track scroll position : Listens to window scroll events
Toggle visibility : Shows button after scrolling 300px
Smooth scroll : Uses window.scrollTo() with smooth behavior
const [ isVisible , setIsVisible ] = useState ( false );
useEffect (() => {
const toggleVisibility = () => {
if ( window . pageYOffset > 300 ) {
setIsVisible ( true );
} else {
setIsVisible ( false );
}
};
window . addEventListener ( "scroll" , toggleVisibility );
return () => window . removeEventListener ( "scroll" , toggleVisibility );
}, []);
Configuration
Adjusting Visibility Threshold
Change when the button appears by modifying the scroll threshold:
src/components/ScrollToTop/index.tsx
const toggleVisibility = () => {
if ( window . pageYOffset > 500 ) { // Show after 500px instead of 300px
setIsVisible ( true );
} else {
setIsVisible ( false );
}
};
Scroll distance in pixels before the button becomes visible
Changing Position
The button is fixed to the bottom-right corner:
< div className = "fixed bottom-8 right-8 z-[99]" >
Move to bottom-left:
< div className = "fixed bottom-8 left-8 z-[99]" >
Move to top-right:
< div className = "fixed top-8 right-8 z-[99]" >
Adjusting Size and Style
The button has these default dimensions:
< div
onClick = { scrollToTop }
aria-label = "scroll to top"
className = "flex h-10 w-10 cursor-pointer items-center justify-center rounded-md bg-primary text-white shadow-md transition duration-300 ease-in-out hover:bg-opacity-80 hover:shadow-signUp"
>
Make it larger:
className = "flex h-14 w-14 cursor-pointer items-center justify-center rounded-md bg-primary text-white shadow-md transition duration-300 ease-in-out hover:bg-opacity-80 hover:shadow-signUp"
Make it circular:
className = "flex h-10 w-10 cursor-pointer items-center justify-center rounded-full bg-primary text-white shadow-md transition duration-300 ease-in-out hover:bg-opacity-80 hover:shadow-signUp"
Change colors:
className = "flex h-10 w-10 cursor-pointer items-center justify-center rounded-md bg-blue-600 text-white shadow-md transition duration-300 ease-in-out hover:bg-blue-700 hover:shadow-lg"
Custom Icon
The default icon is a rotated border creating an arrow:
< span className = "mt-[6px] h-3 w-3 rotate-45 border-l border-t border-white" ></ span >
Replace with an SVG icon:
< svg
width = "16"
height = "16"
viewBox = "0 0 16 16"
fill = "currentColor"
xmlns = "http://www.w3.org/2000/svg"
>
< path d = "M8 1L8 15M8 1L4 5M8 1L12 5" stroke = "currentColor" strokeWidth = "2" strokeLinecap = "round" strokeLinejoin = "round" />
</ svg >
Or use an icon library like Lucide:
import { ArrowUp } from "lucide-react" ;
< ArrowUp className = "h-4 w-4" />
Advanced Customization
Add Animation on Appear
Add a fade-in animation when the button appears:
import { useEffect , useState } from "react" ;
export default function ScrollToTop () {
const [ isVisible , setIsVisible ] = useState ( false );
const scrollToTop = () => {
window . scrollTo ({
top: 0 ,
behavior: "smooth" ,
});
};
useEffect (() => {
const toggleVisibility = () => {
if ( window . pageYOffset > 300 ) {
setIsVisible ( true );
} else {
setIsVisible ( false );
}
};
window . addEventListener ( "scroll" , toggleVisibility );
return () => window . removeEventListener ( "scroll" , toggleVisibility );
}, []);
return (
< div className = "fixed bottom-8 right-8 z-[99]" >
< div
className = { `transition-all duration-300 ${
isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-10 pointer-events-none"
} ` }
>
< div
onClick = { scrollToTop }
aria-label = "scroll to top"
className = "flex h-10 w-10 cursor-pointer items-center justify-center rounded-md bg-primary text-white shadow-md transition duration-300 ease-in-out hover:bg-opacity-80 hover:shadow-signUp"
>
< span className = "mt-[6px] h-3 w-3 rotate-45 border-l border-t border-white" ></ span >
</ div >
</ div >
</ div >
);
}
Display how far down the page the user has scrolled:
import { useEffect , useState } from "react" ;
export default function ScrollToTopWithProgress () {
const [ isVisible , setIsVisible ] = useState ( false );
const [ scrollProgress , setScrollProgress ] = useState ( 0 );
const scrollToTop = () => {
window . scrollTo ({
top: 0 ,
behavior: "smooth" ,
});
};
useEffect (() => {
const handleScroll = () => {
const totalHeight = document . documentElement . scrollHeight - window . innerHeight ;
const progress = ( window . pageYOffset / totalHeight ) * 100 ;
setScrollProgress ( progress );
setIsVisible ( window . pageYOffset > 300 );
};
window . addEventListener ( "scroll" , handleScroll );
return () => window . removeEventListener ( "scroll" , handleScroll );
}, []);
return (
< div className = "fixed bottom-8 right-8 z-[99]" >
{ isVisible && (
< div className = "relative" >
{ /* Progress ring */ }
< svg className = "absolute inset-0 h-12 w-12" viewBox = "0 0 48 48" >
< circle
cx = "24"
cy = "24"
r = "20"
fill = "none"
stroke = "currentColor"
strokeWidth = "4"
className = "text-gray-200 dark:text-gray-700"
/>
< circle
cx = "24"
cy = "24"
r = "20"
fill = "none"
stroke = "currentColor"
strokeWidth = "4"
strokeDasharray = { ` ${ scrollProgress * 1.257 } ${ 125.7 - scrollProgress * 1.257 } ` }
className = "text-primary"
transform = "rotate(-90 24 24)"
/>
</ svg >
{ /* Button */ }
< div
onClick = { scrollToTop }
aria-label = "scroll to top"
className = "flex h-12 w-12 cursor-pointer items-center justify-center rounded-full bg-primary text-white shadow-md transition duration-300 ease-in-out hover:bg-opacity-80 hover:shadow-signUp"
>
< span className = "mt-[6px] h-3 w-3 rotate-45 border-l border-t border-white" ></ span >
</ div >
</ div >
) }
</ div >
);
}
Accessibility
ARIA Label
The button includes aria-label="scroll to top" for screen readers
Keyboard Support
The button is keyboard accessible (clickable with Enter/Space)
Focus Visible
Add focus styles for keyboard navigation: className = "... focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
For better performance with scroll events, add throttling:
import { useEffect , useState , useCallback } from "react" ;
function throttle ( func : Function , delay : number ) {
let timeoutId : NodeJS . Timeout | null = null ;
return ( ... args : any []) => {
if ( ! timeoutId ) {
timeoutId = setTimeout (() => {
func ( ... args );
timeoutId = null ;
}, delay );
}
};
}
export default function ScrollToTop () {
const [ isVisible , setIsVisible ] = useState ( false );
const scrollToTop = () => {
window . scrollTo ({ top: 0 , behavior: "smooth" });
};
const toggleVisibility = useCallback (
throttle (() => {
if ( window . pageYOffset > 300 ) {
setIsVisible ( true );
} else {
setIsVisible ( false );
}
}, 100 ),
[]
);
useEffect (() => {
window . addEventListener ( "scroll" , toggleVisibility );
return () => window . removeEventListener ( "scroll" , toggleVisibility );
}, [ toggleVisibility ]);
return (
< div className = "fixed bottom-8 right-8 z-[99]" >
{ isVisible && (
< div
onClick = { scrollToTop }
aria-label = "scroll to top"
className = "flex h-10 w-10 cursor-pointer items-center justify-center rounded-md bg-primary text-white shadow-md transition duration-300 ease-in-out hover:bg-opacity-80 hover:shadow-signUp"
>
< span className = "mt-[6px] h-3 w-3 rotate-45 border-l border-t border-white" ></ span >
</ div >
) }
</ div >
);
}
Z-Index Considerations
The button uses z-[99] to stay above most content but below modals. Adjust if needed:
z-50 - Below most modals
z-[99] - Default, above content
z-[9999] - Above everything (use cautiously)
Common Use Cases
Blog Posts
Documentation
E-commerce
Landing Pages
Add to long blog posts for easy navigation back to top
Help users navigate lengthy documentation pages
Allow shoppers to quickly return to product filters/navigation
Enhance UX on long-form landing pages with multiple sections
Browser Support
The smooth scroll behavior is supported in all modern browsers. For older browsers, add a fallback:
const scrollToTop = () => {
// Modern browsers
if ( 'scrollBehavior' in document . documentElement . style ) {
window . scrollTo ({ top: 0 , behavior: "smooth" });
} else {
// Fallback for older browsers
window . scrollTo ( 0 , 0 );
}
};
Header Main navigation with sticky behavior
Footer Site footer with navigation links