Scroll To Top
Demo
Scroll to see the button at bottom right
Installation
Shadcn CLI
npx shadcn@latest add https://laviecn.vercel.app/r/scroll-to-top.json
pnpm dlx shadcn@latest add https://laviecn.vercel.app/r/scroll-to-top.json
yarn dlx shadcn@latest add https://laviecn.vercel.app/r/scroll-to-top.json
bun x shadcn@latest add https://laviecn.vercel.app/r/scroll-to-top.json
Manual
Copy and paste the following code into your project.
"use client"
import { useEffect, useState } from "react"
import { ChevronUp } from "lucide-react"
import { motion } from "motion/react"
type ScrollToTopProps = { size?: number }
export function ScrollToTop({ size = 44 }: ScrollToTopProps) {
const [isVisible, setIsVisible] = useState(false)
const [offset, setOffset] = useState(0)
const stroke = 4
const center = size / 2
const r = center - stroke / 2
const circumference = 2 * Math.PI * r
const iconSize = size * 0.4
const iconTranslate = size * 0.3
useEffect(() => {
const handleScroll = () => {
const scrollTop = window.scrollY
const docHeight =
document.documentElement.scrollHeight - window.innerHeight
const progress = Math.min(scrollTop / docHeight, 1)
setOffset(progress >= 0.999 ? 0 : circumference * (1 - progress))
setIsVisible(scrollTop > 100)
}
window.addEventListener("scroll", handleScroll, { passive: true })
window.addEventListener("load", handleScroll)
requestAnimationFrame(handleScroll)
return () => {
window.removeEventListener("scroll", handleScroll)
window.removeEventListener("load", handleScroll)
}
}, [circumference])
const scrollToTop = () => window.scrollTo({ top: 0, behavior: "smooth" })
const svgStyle =
"fixed bottom-4 right-4 z-50 cursor-pointer transition-opacity duration-300"
const visibleStyle = isVisible
? "opacity-100 pointer-events-auto"
: "opacity-0 pointer-events-none"
return (
<svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
onClick={scrollToTop}
className={`${svgStyle} ${visibleStyle}`}
aria-label="Scroll to top"
>
<circle
cx={center}
cy={center}
r={r}
transform={`rotate(-90 ${center} ${center})`}
style={{
fill: "var(--scroll-top-bg)",
stroke: "var(--scroll-top-track)",
strokeWidth: 2,
}}
/>
<motion.circle
cx={center}
cy={center}
r={r}
transform={`rotate(-90 ${center} ${center})`}
style={{ fill: "none", stroke: "var(--scroll-top)", strokeWidth: 2 }}
strokeDasharray={circumference}
strokeDashoffset={isNaN(offset) ? 0 : offset}
/>
<g transform={`translate(${iconTranslate}, ${iconTranslate})`}>
<ChevronUp size={iconSize} color="var(--scroll-top)" strokeWidth={3} />
</g>
</svg>
)
}
Add the following styles to your global.css
file.
@theme inline {
--scroll-top: var(--scroll-top);
--scroll-top-bg: var(--scroll-top-bg);
--scroll-top-track: var(--scroll-top-track);
}
:root {
--scroll-top: oklch(0.55 0.01 286);
--scroll-top-bg: oklch(0.55 0.01 286 / 10%);
--scroll-top-track: oklch(0.55 0.01 286 / 20%);
}
.dark {
--scroll-top: oklch(0.92 0 286);
--scroll-top-bg: oklch(0.92 0 286 / 15%);
--scroll-top-track: oklch(0.92 0 286 / 20%);
}
Update the import paths to match your project setup.
Props
Prop | Type | Default |
---|---|---|
size? | number | 44 |