Skip to content

Commit b4608e1

Browse files
committed
feat: implement floating scroll-to-top and scroll-to-bottom navigation shortcuts
1 parent 4ae0ef6 commit b4608e1

2 files changed

Lines changed: 76 additions & 0 deletions

File tree

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useLocation } from "react-router-dom";
22
import Navbar from "./components/Navbar";
33
import Footer from "./components/Footer";
44
import ScrollProgressBar from "./components/ScrollProgressBar";
5+
import ScrollNavigator from "./components/ScrollNavigator";
56
import { Toaster } from "react-hot-toast";
67
import Router from "./Routes/Router";
78

@@ -16,6 +17,7 @@ function App() {
1617
{!isFullscreen && <ScrollProgressBar />}
1718

1819
{!isFullscreen && <Navbar />}
20+
<ScrollNavigator />
1921

2022
<main className={`flex justify-center items-center ${isFullscreen ? "flex-1" : "flex-grow bg-gray-50 dark:bg-gray-800"}`}>
2123
<Router />

src/components/ScrollNavigator.tsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { useEffect, useState } from "react";
2+
import { ChevronDown, ChevronUp } from "lucide-react";
3+
4+
const BOTTOM_THRESHOLD = 24;
5+
6+
const ScrollNavigator = () => {
7+
const [showUpButton, setShowUpButton] = useState(false);
8+
const [showDownButton, setShowDownButton] = useState(false);
9+
10+
const updateVisibility = () => {
11+
const { documentElement } = document;
12+
const scrollTop = window.scrollY;
13+
const scrollableHeight =
14+
documentElement.scrollHeight - documentElement.clientHeight;
15+
const nearBottom = scrollTop >= scrollableHeight - BOTTOM_THRESHOLD;
16+
17+
setShowUpButton(scrollTop > 300);
18+
setShowDownButton(scrollableHeight > 0 && !nearBottom);
19+
};
20+
21+
const scrollToTop = () => {
22+
window.scrollTo({ top: 0, behavior: "smooth" });
23+
};
24+
25+
const scrollToBottom = () => {
26+
window.scrollTo({
27+
top: document.documentElement.scrollHeight,
28+
behavior: "smooth",
29+
});
30+
};
31+
32+
useEffect(() => {
33+
window.addEventListener("scroll", updateVisibility);
34+
window.addEventListener("resize", updateVisibility);
35+
updateVisibility();
36+
37+
return () => {
38+
window.removeEventListener("scroll", updateVisibility);
39+
window.removeEventListener("resize", updateVisibility);
40+
};
41+
}, []);
42+
43+
if (!showUpButton && !showDownButton) {
44+
return null;
45+
}
46+
47+
return (
48+
<div className="fixed bottom-5 right-5 z-50 flex flex-col items-center gap-3">
49+
{showUpButton && (
50+
<button
51+
type="button"
52+
onClick={scrollToTop}
53+
aria-label="Scroll to top"
54+
className="flex h-12 w-12 items-center justify-center rounded-full bg-blue-600 text-white shadow-lg shadow-blue-600/30 transition-all duration-200 hover:-translate-y-1 hover:bg-blue-700 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 dark:focus:ring-offset-slate-900"
55+
>
56+
<ChevronUp className="h-6 w-6" />
57+
</button>
58+
)}
59+
60+
{showDownButton && (
61+
<button
62+
type="button"
63+
onClick={scrollToBottom}
64+
aria-label="Scroll to bottom"
65+
className="flex h-12 w-12 items-center justify-center rounded-full bg-slate-900 text-white shadow-lg shadow-slate-900/30 transition-all duration-200 hover:translate-y-1 hover:bg-slate-700 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-white dark:focus:ring-offset-slate-900"
66+
>
67+
<ChevronDown className="h-6 w-6" />
68+
</button>
69+
)}
70+
</div>
71+
);
72+
};
73+
74+
export default ScrollNavigator;

0 commit comments

Comments
 (0)