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.

API Reference

PropTypeDefault
openbooleanfalse
onClose() => void

Preview

Company Name.

Usage

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)} />

Basic Usage

import { MobileMenuLayered } from "@/extensions/layout";

<MobileMenuLayered open={open} onClose={() => setOpen(false)} />

Source

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>
  );
}