Skip to content

Commit 9e4d609

Browse files
committed
Add auto-hide navigation with opt-out and simplify ScrollProgress
- Install headroom.js (1.5 kB) for scroll-aware navigation behavior - Desktop nav slides up, mobile nav slides down on scroll with 50px threshold - Egyptian water easing (0.4s) and prefers-reduced-motion support - Opt-out mechanism: homepage and contact keep nav always visible - Simplify ScrollProgress to official Astro astro:page-load pattern - Page-controlled ScrollProgress via showScrollProgress prop - Enable on About, Resume, and case study pages only
1 parent a92b013 commit 9e4d609

File tree

10 files changed

+455
-236
lines changed

10 files changed

+455
-236
lines changed

bun.lock

Lines changed: 356 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"@tailwindcss/typography": "^0.5.19",
77
"@tailwindcss/vite": "^4.1.16",
88
"@types/bun": "latest",
9+
"@types/headroom": "^0.12.4",
910
"astro": "^5.15.1",
1011
"class-variance-authority": "^0.7.1",
1112
"clsx": "^2.1.1",
@@ -24,8 +25,11 @@
2425
},
2526
"type": "module",
2627
"dependencies": {
28+
"@astrojs/mdx": "^4.3.9",
2729
"@lucide/astro": "^0.548.0",
2830
"@northstarthemes/astrobook": "^0.11.1",
31+
"astro-mermaid": "^1.1.0",
32+
"headroom.js": "^0.12.0",
2933
"motion": "^12.23.24",
3034
"shiki": "^3.13.0"
3135
}

src/components/animations/ScrollProgress.astro

Lines changed: 6 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,9 @@
2121
import { scroll, animate } from "motion";
2222
import { prefersReducedMotion } from "../../utils/animations";
2323

24-
function initScrollProgress() {
24+
document.addEventListener('astro:page-load', () => {
2525
const progressBar = document.querySelector('.scroll-progress');
26-
27-
if (!progressBar || !(progressBar instanceof HTMLElement)) {
28-
console.warn('ScrollProgress: Progress bar element not found');
29-
return;
30-
}
31-
32-
// Skip if already initialized
33-
if (progressBar.dataset.scrollInit === 'true') {
34-
return;
35-
}
36-
progressBar.dataset.scrollInit = 'true';
26+
if (!progressBar || !(progressBar instanceof HTMLElement)) return;
3727

3828
// If user prefers reduced motion, show full bar instantly
3929
if (prefersReducedMotion()) {
@@ -42,34 +32,10 @@
4232
}
4333

4434
// Animate scaleX based on scroll position
45-
try {
46-
scroll(
47-
animate(progressBar, { scaleX: [0, 1] }),
48-
{
49-
target: document.documentElement,
50-
offset: ['start start', 'end end']
51-
}
52-
);
53-
} catch (error) {
54-
console.error('ScrollProgress: Failed to initialize scroll animation', error);
55-
}
56-
}
57-
58-
// Run on initial load
59-
if (document.readyState === 'loading') {
60-
document.addEventListener('DOMContentLoaded', initScrollProgress);
61-
} else {
62-
initScrollProgress();
63-
}
64-
65-
// Re-run after Astro View Transitions
66-
document.addEventListener('astro:page-load', () => {
67-
// Reset initialization flag to allow reinit
68-
const progressBar = document.querySelector('.scroll-progress');
69-
if (progressBar instanceof HTMLElement) {
70-
delete progressBar.dataset.scrollInit;
71-
}
72-
initScrollProgress();
35+
scroll(
36+
animate(progressBar, { scaleX: [0, 1] }),
37+
{ target: document.documentElement, offset: ['start start', 'end end'] }
38+
);
7339
});
7440
</script>
7541

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,57 @@
11
---
22
// BottomTabBar - Composable mobile navigation container
33
// Purely structural, accepts TabItem children for flexibility
4+
5+
interface Props {
6+
disableAutoHide?: boolean;
7+
}
8+
9+
const { disableAutoHide = false } = Astro.props;
410
---
511

612
<nav
7-
class="fixed bottom-0 left-0 right-0 z-50 md:hidden
13+
id="mobile-nav"
14+
class="headroom headroom--mobile fixed bottom-0 left-0 right-0 z-50 md:hidden
815
bg-surface/95 backdrop-blur-md border-t border-neutral-lighter
916
h-16 pb-safe"
1017
role="navigation"
1118
aria-label="Mobile navigation"
19+
data-disable-auto-hide={disableAutoHide}
1220
>
1321
<div class="flex items-center justify-around h-full px-4">
1422
<slot />
1523
</div>
1624
</nav>
25+
26+
<script>
27+
import Headroom from 'headroom.js';
28+
29+
let headroomInstance: Headroom | null = null;
30+
31+
document.addEventListener('astro:page-load', () => {
32+
const nav = document.getElementById('mobile-nav');
33+
if (!nav) return;
34+
35+
// Check if page opted out of auto-hide behavior
36+
const disableAutoHide = nav.dataset.disableAutoHide === 'true';
37+
38+
// Destroy previous instance to prevent memory leaks
39+
if (headroomInstance) {
40+
headroomInstance.destroy();
41+
headroomInstance = null;
42+
}
43+
44+
// Skip initialization if page disabled auto-hide
45+
if (disableAutoHide) return;
46+
47+
// Initialize headroom with same settings as desktop
48+
headroomInstance = new Headroom(nav, {
49+
offset: 50, // Hide after scrolling down 50px
50+
tolerance: {
51+
up: 10, // Show after scrolling up 10px
52+
down: 10 // Hide after scrolling down 10px past offset
53+
}
54+
});
55+
headroomInstance.init();
56+
});
57+
</script>

src/layouts/Layout.astro

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,16 @@ interface Props {
1414
title: string;
1515
description?: string;
1616
ogImage?: string;
17+
showScrollProgress?: boolean;
18+
disableAutoHideNav?: boolean;
1719
}
1820
1921
const {
2022
title,
2123
description = 'Building realtime systems that scale through functional architecture and separation of concerns',
22-
ogImage = '/og-image.png'
24+
ogImage = '/og-image.png',
25+
showScrollProgress = false,
26+
disableAutoHideNav = false
2327
} = Astro.props;
2428
2529
const currentPath = Astro.url.pathname;
@@ -92,13 +96,47 @@ const ogImageURL = new URL(ogImage, Astro.site);
9296
});
9397
});
9498
</script>
99+
100+
<!-- Auto-hide desktop navigation on scroll -->
101+
<script>
102+
import Headroom from 'headroom.js';
103+
104+
let headroomInstance: Headroom | null = null;
105+
106+
document.addEventListener('astro:page-load', () => {
107+
const nav = document.getElementById('desktop-nav');
108+
if (!nav) return;
109+
110+
// Check if page opted out of auto-hide behavior
111+
const disableAutoHide = nav.dataset.disableAutoHide === 'true';
112+
113+
// Destroy previous instance to prevent memory leaks
114+
if (headroomInstance) {
115+
headroomInstance.destroy();
116+
headroomInstance = null;
117+
}
118+
119+
// Skip initialization if page disabled auto-hide
120+
if (disableAutoHide) return;
121+
122+
// Initialize headroom with threshold and tolerance settings
123+
headroomInstance = new Headroom(nav, {
124+
offset: 50, // Hide after scrolling down 50px
125+
tolerance: {
126+
up: 10, // Show after scrolling up 10px
127+
down: 10 // Hide after scrolling down 10px past offset
128+
}
129+
});
130+
headroomInstance.init();
131+
});
132+
</script>
95133
</head>
96134
<body class="bg-background text-text antialiased">
97-
<!-- Scroll Progress Indicator (reading pages only, not homepage) -->
98-
{currentPath !== '/' && <ScrollProgress />}
135+
<!-- Scroll Progress Indicator (long-form content pages only) -->
136+
{showScrollProgress && <ScrollProgress />}
99137

100138
<!-- Responsive Navigation: Mobile circular badge → Desktop bar -->
101-
<nav class="md:fixed md:mt-0 mt-8 top-2 left-2 right-2 max-w-6xl mx-auto z-50 rounded-full md:bg-surface/60 md:backdrop-blur-lg flex items-center justify-center border border-neutral-lighter">
139+
<nav id="desktop-nav" class="headroom md:fixed md:mt-0 mt-8 top-2 left-2 right-2 max-w-6xl mx-auto z-50 rounded-full md:bg-surface/80 md:backdrop-blur-lg flex items-center justify-center border border-neutral-lighter" data-disable-auto-hide={disableAutoHideNav}>
102140
<div class="flex items-center justify-center md:justify-between w-full px-4">
103141
<a href="/" class="hover:opacity-80 transition-opacity">
104142
<SSLogo />
@@ -115,7 +153,7 @@ const ogImageURL = new URL(ogImage, Astro.site);
115153
</nav>
116154

117155
<!-- Mobile Bottom Tab Bar -->
118-
<BottomTabBar>
156+
<BottomTabBar disableAutoHide={disableAutoHideNav}>
119157
<TabItem href="/" active={currentPath === '/'} label="Home">
120158
<House size={24} />
121159
</TabItem>

src/pages/about.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import Body from '../components/typography/Body.astro';
77
import { FileText } from '@lucide/astro';
88
---
99

10-
<Layout title="About - Saad Shahd">
10+
<Layout title="About - Saad Shahd" showScrollProgress={true}>
1111
<Container width="narrow">
1212
<Heading level={1} as="h1" class="mb-16"><span>About</span></Heading>
1313

src/pages/contact.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import Body from '../components/typography/Body.astro';
55
import Link from '../components/Link.astro';
66
---
77

8-
<Layout title="Contact - Saad Shahd">
8+
<Layout title="Contact - Saad Shahd" disableAutoHideNav={true}>
99
<div class="max-w-4xl mx-auto px-4 md:px-6 py-20">
1010
<Heading level={1} as="h1" class="mb-16"><span>Get in Touch</span></Heading>
1111
<Body size="lg" as="p" class="text-neutral mb-16 leading-normal">

src/pages/index.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import Display from '../components/typography/Display.astro';
66
import Body from '../components/typography/Body.astro';
77
---
88

9-
<Layout title="Saad Shahd - Software Engineer">
9+
<Layout title="Saad Shahd - Software Engineer" disableAutoHideNav={true}>
1010
<!-- Centered content on top of patterns -->
1111
<Container width="default" spacing="hero" class="relative flex flex-col justify-center items-center" style="z-index: 1;">
1212
<section class="w-full flex flex-col items-center text-center">

0 commit comments

Comments
 (0)