Mobile Menu

Full-screen mobile menu with large numbered links, arrow icons, contact footer, and CTA button.

API Reference

PropTypeDefault
openbooleanfalse
onClose() => void

Preview

Company Name.

1-800-000-0000

hello@example.com

Usage

import { useState } from "react";
import { MobileMenu } 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 menu"
>
  <MenuIcon />
</IconButton>

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

Basic Usage

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

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

Source

src/extensions/layout/MobileMenu.tsx
"use client";

import { useEffect } from "react";
import { Link, Button, IconButton, Logo } from "@/ui";
import { ArrowForwardIcon, CloseIcon } from "@/ui/icons";
import { NAV_LINKS, NAV_CTA, CONTACT } from "@/constants";
import { useTheme } from "next-themes";
import { cn } from "@/utils";

interface MobileMenuProps {
  open: boolean;
  onClose: () => void;
}

export function MobileMenu({ open, onClose }: MobileMenuProps) {
  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="/" onClick={onClose}>
          <Logo />
        </Link>
        <IconButton
          variant="ghost"
          color="root"
          onClick={onClose}
          aria-label="Close menu"
        >
          <CloseIcon />
        </IconButton>
      </div>

      <nav className="flex flex-1 flex-col justify-center px-8">
        <ul className="space-y-1">
          {NAV_LINKS.map((link, i) => (
            <li key={link.href}>
              <Link
                href={link.href}
                onClick={onClose}
                className="group flex items-center justify-between border-b border-divider py-5"
              >
                <div className="flex items-baseline gap-4">
                  <span className="text-xs tabular-nums text-root-contrast/30">
                    {String(i + 1).padStart(2, "0")}
                  </span>
                  <span className="text-4xl font-light text-root-contrast transition-colors group-hover:text-primary">
                    {link.label}
                  </span>
                </div>
                <ArrowForwardIcon
                  size={5}
                  className="text-root-contrast/30 transition-all group-hover:translate-x-1 group-hover:text-primary"
                />
              </Link>
            </li>
          ))}
        </ul>
      </nav>

      <div className="flex items-end justify-between gap-6 border-t border-divider px-8 py-8">
        <div className="space-y-1 text-sm text-root-contrast/40">
          <p>
            <Link
              href={CONTACT.phone.href}
              className="transition-colors hover:text-root-contrast"
            >
              {CONTACT.phone.number}
            </Link>
          </p>
          <p>
            <Link
              href={CONTACT.email.href}
              className="transition-colors hover:text-root-contrast"
            >
              {CONTACT.email.address}
            </Link>
          </p>
        </div>
        <Button
          href={NAV_CTA.href}
          variant="solid"
          color="root"
          size="sm"
          onClick={onClose}
        >
          {NAV_CTA.label}
        </Button>
      </div>
    </div>
  );
}