navbars /
Mobile Sheet Navbar
Mobile-first nav: logo + hamburger; opens a right-side slide-in sheet with stacked links.
Preview
Source
tsx
"use client";
import { useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { Sparkles, Menu, X } from "lucide-react";
const links = [
{ label: "Product", href: "#" },
{ label: "Solutions", href: "#" },
{ label: "Pricing", href: "#" },
{ label: "Docs", href: "#" },
{ label: "Company", href: "#" },
];
export default function NavbarMobileSheet() {
const [open, setOpen] = useState(false);
const triggerRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (!open) return;
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") {
setOpen(false);
triggerRef.current?.focus();
}
};
document.addEventListener("keydown", onKey);
return () => {
document.body.style.overflow = prevOverflow;
document.removeEventListener("keydown", onKey);
};
}, [open]);
return (
<header className="w-full border-b border-border bg-background">
<div className="container mx-auto flex items-center justify-between gap-4 px-6 py-4">
<a href="#" className="flex items-center gap-2 text-foreground font-bold">
<Sparkles className="w-5 h-5 text-primary" />
<span>Acme</span>
</a>
<button
ref={triggerRef}
type="button"
aria-label="Open menu"
aria-expanded={open}
onClick={() => setOpen(true)}
className="inline-flex items-center justify-center w-10 h-10 rounded-md text-foreground hover:bg-accent transition-colors"
>
<Menu className="w-5 h-5" />
</button>
</div>
{open && (
<>
<div
className="fixed inset-0 z-40 bg-background/60 backdrop-blur-sm"
onClick={() => setOpen(false)}
aria-hidden
/>
<aside
className="fixed top-0 right-0 bottom-0 z-50 w-80 max-w-[90vw] bg-background border-l border-border shadow-2xl flex flex-col"
role="dialog"
aria-label="Site menu"
>
<div className="flex items-center justify-between px-5 py-4 border-b border-border">
<span className="text-sm font-semibold text-foreground">Menu</span>
<button
type="button"
aria-label="Close menu"
onClick={() => {
setOpen(false);
triggerRef.current?.focus();
}}
className="inline-flex items-center justify-center w-9 h-9 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
<nav className="flex-1 overflow-y-auto px-3 py-4">
<ul className="space-y-1">
{links.map((l) => (
<li key={l.label}>
<a
href={l.href}
className="block px-3 py-2.5 rounded-md text-sm text-foreground hover:bg-accent transition-colors"
>
{l.label}
</a>
</li>
))}
</ul>
</nav>
<div className="px-5 py-4 border-t border-border">
<Button className="w-full">Get started</Button>
</div>
</aside>
</>
)}
</header>
);
} Claude Code Instructions
CLI Install
npx innovations add mobile-sheetWhere to use it
Full-width mobile header that works at every breakpoint. ESC closes the menu, and body scroll is locked while it's open.
In Astro (src/layouts/Layout.astro):
import NavbarMobileSheet from '../components/innovations/navbars/mobile-sheet';
Customize: edit the links array. To add desktop-inline links, add a hidden-on-mobile nav between the logo and the hamburger (e.g., <nav className="hidden md:flex gap-6">...</nav>) and hide the hamburger on md+ via className="md:hidden" on the button.