Componentfeedback1.26 KB gzipZero dependencies

Drawer Nav

Mobile hamburger drawer sliding from the left with staggered links.

Preview

Live Preview

Installation

Tokens guide →

Add to your project via CLI (zero dependencies, code you own):

npx bigbullui add drawer-nav

Or install the full npm package:

npm install bigbullui
import { DrawerNav } from "@/components/ui/drawer-nav";

<DrawerNav />

Usage guidance

Use Drawer Nav when a ticket stub surface needs Mobile hamburger drawer sliding from the left with staggered links. Keep props minimal and prefer composition with sibling stubs.

Avoid Drawer Nav for long form flows or dense data grids where a dedicated layout block fits better.

Source code

drawer-nav.tsx

Zero dependencies (only React + utils). Copy and paste directly into your project:

"use client";

import * as React from "react";
import { cn } from "./lib/utils";

export interface DrawerNavItem {
  id: string;
  label: string;
  href?: string;
}

export interface DrawerNavProps extends React.HTMLAttributes<HTMLDivElement> {
  items: DrawerNavItem[];
  open: boolean;
  onOpenChange: (open: boolean) => void;
  title?: string;
}

/** Drawer navigation: slide-in overlay menu + staggered links. */
export function DrawerNav({ items, open, onOpenChange, title = "Menu", className, ...props }: DrawerNavProps) {
  React.useEffect(() => {
    if (!open) return;
    const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onOpenChange(false); };
    document.addEventListener("keydown", onKey);
    document.body.style.overflow = "hidden";
    return () => {
      document.removeEventListener("keydown", onKey);
      document.body.style.overflow = "";
    };
  }, [open, onOpenChange]);

  if (!open) return null;

  return (
    <div className={cn("fixed inset-0 z-50", className)} {...props}>
      <style>{`@keyframes dnPanel { from { transform: translateX(-100%); } to { transform: translateX(0); } }`}</style>
      <div className="absolute inset-0 bg-black/40" onClick={() => onOpenChange(false)} aria-hidden="true" />
      <div className="absolute inset-y-0 left-0 flex w-64 flex-col border-r-2 border-dashed border-border bg-background shadow-xl" style={{ animation: "dnPanel 0.3s cubic-bezier(0.16,1,0.3,1) both" }} role="dialog" aria-modal="true" aria-label={title}>
        <div className="flex items-center justify-between border-b border-dashed border-border px-4 py-3">
          <span className="font-mono text-xs font-bold uppercase tracking-widest">{title}</span>
          <button
            onClick={() => onOpenChange(false)}
            className="rounded-sm p-1 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring motion-reduce:transition-none"
            aria-label="Close"
          >
            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true"><path d="M18 6L6 18M6 6l12 12" /></svg>
          </button>
        </div>
        <ul className="flex-1 space-y-1 p-3">
          {items.map((item, idx) => (
            <li key={item.id} style={{ animationDelay: `${idx * 50}ms` }}>
              <a
                href={item.href ?? "#"}
                className={cn(
                  "block rounded-md px-3 py-2.5 text-sm font-medium transition-colors motion-reduce:transition-none",
                  "hover:bg-secondary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
                  "animate-[fade-in-up_0.3s_ease-out_both] motion-reduce:animate-none"
                )}
                style={{ animationDelay: `${0.15 + idx * 0.05}s` }}
              >
                {item.label}
              </a>
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
}

Props

PropTypeDescription
itemsDrawerNavItem[]
openboolean
onOpenChange(open: boolean) => void
titlestring