Skip to content

Commit 49578eb

Browse files
committed
fix(website): add mobile hamburger menu for header navigation
Nav links were hidden below 768px with no alternative; expose them in an accessible slide-down panel.
1 parent a848a0d commit 49578eb

1 file changed

Lines changed: 181 additions & 5 deletions

File tree

website/src/components/Header.astro

Lines changed: 181 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,29 @@ const basePath = import.meta.env.BASE_URL.endsWith("/")
2323
<a href="#frameworks" data-section="frameworks">Frameworks</a>
2424
<a href={`${basePath}docs/`} data-section="docs">Docs</a>
2525
</nav>
26-
<a href="https://github.com/devalade/shipnode" target="_blank" rel="noopener" class="github-link" id="github-link" aria-label="View ShipNode on GitHub">
27-
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
28-
<span>GitHub</span>
29-
</a>
26+
<div class="header-actions">
27+
<button
28+
type="button"
29+
class="menu-toggle"
30+
id="menu-toggle"
31+
aria-expanded="false"
32+
aria-controls="main-nav"
33+
aria-label="Open menu"
34+
>
35+
<svg class="menu-icon menu-icon-open" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
36+
<path d="M4 7h16M4 12h16M4 17h16"/>
37+
</svg>
38+
<svg class="menu-icon menu-icon-close" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
39+
<path d="M6 6l12 12M18 6L6 18"/>
40+
</svg>
41+
</button>
42+
<a href="https://github.com/devalade/shipnode" target="_blank" rel="noopener" class="github-link" id="github-link" aria-label="View ShipNode on GitHub">
43+
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
44+
<span>GitHub</span>
45+
</a>
46+
</div>
3047
</div>
48+
<div class="nav-backdrop" id="nav-backdrop" hidden></div>
3149
</header>
3250

3351
<style>
@@ -52,6 +70,7 @@ const basePath = import.meta.env.BASE_URL.endsWith("/")
5270
display: flex;
5371
align-items: center;
5472
justify-content: space-between;
73+
gap: var(--space-md);
5574
}
5675

5776
.logo {
@@ -63,10 +82,54 @@ const basePath = import.meta.env.BASE_URL.endsWith("/")
6382
color: var(--text-primary);
6483
transition: opacity var(--transition-fast), transform var(--transition-fast);
6584
letter-spacing: -0.01em;
85+
flex-shrink: 0;
6686
}
6787
.logo:hover { opacity: 0.8; }
6888
.logo:active { transform: scale(0.97); }
6989

90+
.header-actions {
91+
display: flex;
92+
align-items: center;
93+
gap: var(--space-sm);
94+
flex-shrink: 0;
95+
}
96+
97+
.menu-toggle {
98+
display: none;
99+
align-items: center;
100+
justify-content: center;
101+
width: 2.5rem;
102+
height: 2.5rem;
103+
padding: 0;
104+
border: 1px solid var(--border-strong);
105+
border-radius: var(--radius-md);
106+
background: transparent;
107+
color: var(--text-primary);
108+
cursor: pointer;
109+
transition: border-color var(--transition-fast), background var(--transition-fast), transform var(--transition-fast);
110+
}
111+
.menu-toggle:hover {
112+
border-color: var(--accent-primary);
113+
background: var(--accent-muted);
114+
}
115+
.menu-toggle:active {
116+
transform: scale(0.97);
117+
}
118+
.menu-toggle:focus-visible {
119+
outline: 2px solid var(--accent-primary);
120+
outline-offset: 2px;
121+
}
122+
123+
.menu-icon-close {
124+
display: none;
125+
}
126+
.header.is-menu-open .menu-icon-open {
127+
display: none;
128+
}
129+
.header.is-menu-open .menu-icon-close {
130+
display: block;
131+
}
132+
70133
.nav {
71134
display: flex;
72135
gap: 0.35rem;
@@ -85,11 +148,19 @@ const basePath = import.meta.env.BASE_URL.endsWith("/")
85148
border-radius: var(--radius-full);
86149
}
87150
.nav a:hover { color: var(--text-primary); background: rgba(237, 237, 223, 0.05); }
151+
.nav a:focus-visible {
152+
outline: 2px solid var(--accent-primary);
153+
outline-offset: 2px;
154+
}
88155
.nav a.active {
89156
color: var(--text-primary);
90157
background: rgba(237, 237, 223, 0.08);
91158
}
92159

160+
.nav-backdrop {
161+
display: none;
162+
}
163+
93164
.github-link {
94165
display: flex;
95166
align-items: center;
@@ -110,19 +181,124 @@ const basePath = import.meta.env.BASE_URL.endsWith("/")
110181
.github-link:active {
111182
transform: scale(0.97) translateY(0);
112183
}
184+
.github-link:focus-visible {
185+
outline: 2px solid var(--accent-primary);
186+
outline-offset: 2px;
187+
}
188+
189+
:global(body.menu-open) {
190+
overflow: hidden;
191+
}
113192

114193
@media (max-width: 768px) {
115-
.nav { display: none; }
194+
.menu-toggle {
195+
display: flex;
196+
}
197+
198+
.nav {
199+
display: none;
200+
position: fixed;
201+
top: 64px;
202+
left: 0;
203+
right: 0;
204+
z-index: var(--z-modal);
205+
flex-direction: column;
206+
gap: 0;
207+
padding: var(--space-sm) var(--space-md) var(--space-md);
208+
border: none;
209+
border-bottom: 1px solid var(--border-primary);
210+
border-radius: 0;
211+
background: rgba(16, 17, 15, 0.96);
212+
backdrop-filter: blur(18px) saturate(130%);
213+
-webkit-backdrop-filter: blur(18px) saturate(130%);
214+
box-shadow: var(--shadow-md);
215+
}
216+
217+
.header.is-menu-open .nav {
218+
display: flex;
219+
}
220+
221+
.nav a {
222+
font-size: 0.9375rem;
223+
padding: 0.75rem 1rem;
224+
border-radius: var(--radius-md);
225+
}
226+
227+
.nav-backdrop {
228+
display: none;
229+
position: fixed;
230+
inset: 64px 0 0;
231+
z-index: calc(var(--z-modal) - 1);
232+
background: rgba(5, 6, 4, 0.45);
233+
border: none;
234+
padding: 0;
235+
margin: 0;
236+
cursor: pointer;
237+
}
238+
239+
.header.is-menu-open .nav-backdrop {
240+
display: block;
241+
}
242+
116243
.github-link span { display: none; }
117244
.github-link { padding: 0.5rem; border-radius: var(--radius-md); }
118245
}
119246

120247
@media (max-width: 420px) {
121248
.header-inner { padding: 0 var(--space-md); }
122249
}
250+
251+
@media (prefers-reduced-motion: reduce) {
252+
.nav,
253+
.menu-toggle,
254+
.github-link {
255+
transition: none;
256+
}
257+
}
123258
</style>
124259

125260
<script>
261+
const header = document.getElementById("header");
262+
const menuToggle = document.getElementById("menu-toggle");
263+
const mainNav = document.getElementById("main-nav");
264+
const navBackdrop = document.getElementById("nav-backdrop");
265+
266+
function setMenuOpen(open: boolean) {
267+
if (!header || !menuToggle || !mainNav) return;
268+
header.classList.toggle("is-menu-open", open);
269+
menuToggle.setAttribute("aria-expanded", String(open));
270+
menuToggle.setAttribute("aria-label", open ? "Close menu" : "Open menu");
271+
document.body.classList.toggle("menu-open", open);
272+
if (navBackdrop) {
273+
navBackdrop.hidden = !open;
274+
}
275+
}
276+
277+
function closeMenu(returnFocusToToggle = false) {
278+
const wasOpen = header?.classList.contains("is-menu-open") ?? false;
279+
setMenuOpen(false);
280+
if (wasOpen && returnFocusToToggle) {
281+
menuToggle?.focus();
282+
}
283+
}
284+
285+
menuToggle?.addEventListener("click", () => {
286+
const isOpen = header?.classList.contains("is-menu-open") ?? false;
287+
setMenuOpen(!isOpen);
288+
});
289+
290+
navBackdrop?.addEventListener("click", () => closeMenu(true));
291+
292+
mainNav?.querySelectorAll("a").forEach((link) => {
293+
link.addEventListener("click", () => closeMenu(false));
294+
});
295+
296+
document.addEventListener("keydown", (e) => {
297+
if (e.key === "Escape" && header?.classList.contains("is-menu-open")) {
298+
closeMenu(true);
299+
}
300+
});
301+
126302
// Active section highlighting on scroll
127303
const sections = ["how-it-works", "features", "commands", "frameworks"];
128304
const navLinks = document.querySelectorAll<HTMLAnchorElement>("#main-nav a[data-section]");

0 commit comments

Comments
 (0)