Layered Mobile Menu
Full-screen mobile menu with grouped navigation sections and nested child links. Best for docs sites or apps with deep navigation trees.
| Prop | Type | Default |
|---|---|---|
| open | boolean | false |
| onClose | () => void | — |
Company Name.
import { useState } from "react";
import { MobileMenuLayered } from "@/extensions/layout";
import { IconButton } from "@/ui";
import { MenuIcon } from "@/ui/icons";
const [open, setOpen] = useState(false);
<IconButton
variant="ghost"
color="root"
onClick={() => setOpen(true)}
aria-label="Open sidebar"
>
<MenuIcon />
</IconButton>
<MobileMenuLayered open={open} onClose={() => setOpen(false)} />import { MobileMenuLayered } from "@/extensions/layout";
<MobileMenuLayered open={open} onClose={() => setOpen(false)} />src/extensions/layout/MobileMenuLayered.tsx
"use client";
import { useEffect } from "react";
import { usePathname } from "next/navigation";
import { Link, IconButton } from "@/ui";
import { CloseIcon } from "@/ui/icons";
import { BRAND } from "@/constants";
import { DOCS_NAV } from "@/constants";
import { useTheme } from "next-themes";
import { cn } from "@/utils";
interface MobileMenuLayeredProps {
open: boolean;
onClose: () => void;
}
export function MobileMenuLayered({ open, onClose }: MobileMenuLayeredProps) {
const pathname = usePathname();
const { resolvedTheme } = useTheme();
useEffect(() => {
if (!open) return;
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = "";
};
}, [open]);
if (!open) return null;
return (
<div
className={cn(
"fixed inset-0 z-50 flex flex-col bg-root",
resolvedTheme === "dark" && "dark",
)}
>
<div className="flex items-center justify-between border-b border-divider px-6 py-3 lg:px-8">
<Link
href="/"
className="text-sm font-semibold tracking-tight text-root-contrast"
onClick={onClose}
>
{BRAND.name}.
</Link>
<IconButton
variant="ghost"
color="root"
onClick={onClose}
aria-label="Close menu"
>
<CloseIcon />
</IconButton>
</div>
<nav className="flex-1 overflow-y-auto px-6 py-6">
<div className="space-y-6">
{DOCS_NAV.map((group) => (
<div key={group.label}>
<p className="mb-2 text-xs font-semibold uppercase tracking-widest text-root-contrast/40">
{group.label}
</p>
<ul className="space-y-0.5">
{group.items.map((item) => {
const isActive = pathname === item.href;
const isParentActive =
item.children?.some((child) => pathname === child.href) ??
false;
return (
<li key={item.href}>
<Link
href={item.href}
onClick={onClose}
className={cn(
"block rounded-md px-3 py-2 text-sm transition-colors",
isActive || isParentActive
? "bg-divider font-medium text-root-contrast"
: "text-root-contrast/60 hover:text-root-contrast",
)}
>
{item.label}
</Link>
{item.children && (
<ul className="ml-3 mt-0.5 space-y-0.5 border-l border-divider pl-3">
{item.children.map((child) => {
const childActive = pathname === child.href;
return (
<li key={child.href}>
<Link
href={child.href}
onClick={onClose}
className={cn(
"block rounded-md px-2 py-1.5 text-xs transition-colors",
childActive
? "font-medium text-root-contrast"
: "text-root-contrast/40 hover:text-root-contrast",
)}
>
{child.label}
</Link>
</li>
);
})}
</ul>
)}
</li>
);
})}
</ul>
</div>
))}
</div>
</nav>
</div>
);
}