Innovations

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-sheet

Where 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.