Hamburger
Menu button whose three lines cross into an X.
From the Nitip template — see it in contextStudio
"use client";
import { useState } from "react";
import { Hamburger } from "@/components/ui/hamburger";
export default function HamburgerDemo() {
const [open, setOpen] = useState(false);
return (
<div className="w-full max-w-sm rounded-lg border bg-card">
<div className="flex items-center justify-between px-4 py-3">
<span className="text-sm font-semibold">Studio</span>
<Hamburger open={open} onOpenChange={setOpen} controls="hamburger-demo-menu" />
</div>
<div id="hamburger-demo-menu" hidden={!open} className="border-t px-4 py-3 text-sm text-muted-foreground">
Work · Studio · Journal · Contact
</div>
</div>
);
}Installation
$ npx shadcn@latest add https://framebrick.co/r/hamburger.json1. Install the dependencies.
npm install motion clsx tailwind-merge2. Add
cn()tolib/utils.ts(skip if shadcn already created it).import { clsx, type ClassValue } from "clsx"; import { extendTailwindMerge } from "tailwind-merge"; /* tailwind-merge must know the site's type scale (globals.css) is a font size, not a text color — otherwise cn("text-h6 text-muted-foreground") drops text-h6. */ const twMerge = extendTailwindMerge({ extend: { classGroups: { "font-size": [{ text: ["display", "h1", "h2", "h3", "h4", "h5", "h6"] }] } }, }); export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }3. Copy the component into
components/ui/hamburger.tsx."use client"; import * as React from "react"; import { motion, type Transition } from "motion/react"; import { cn } from "@/lib/utils"; /* * Hamburger — menu button whose three lines cross into an X (from the Nitip header). * The lines move with Framer's soft spring (no bounce, 0.4s); the label switches with the state. * Controlled (`open` + `onOpenChange`) or on its own; point `controls` at the id of the menu it opens. */ const soft: Transition = { type: "spring", bounce: 0, duration: 0.4 }; export type HamburgerProps = { open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; /** id of the element this button shows and hides. */ controls?: string; /** Labels beside the lines; `false` hides them. */ labels?: { closed: string; open: string } | false; className?: string; }; export function Hamburger({ open, defaultOpen = false, onOpenChange, controls, labels = { closed: "Menu", open: "Close" }, className, }: HamburgerProps) { const [internal, setInternal] = React.useState(defaultOpen); const isOpen = open ?? internal; function toggle() { const next = !isOpen; if (open === undefined) setInternal(next); onOpenChange?.(next); } const line = "absolute left-0 h-px w-full bg-current"; return ( <button type="button" aria-expanded={isOpen} aria-controls={controls} aria-label={labels ? undefined : isOpen ? "Close menu" : "Open menu"} onClick={toggle} className={cn( "inline-flex cursor-pointer items-center gap-2 rounded-md text-sm font-medium text-foreground", "outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50", className, )} > {labels && (isOpen ? labels.open : labels.closed)} <span aria-hidden className="relative h-3 w-4"> <motion.span className={cn(line, "top-0")} initial={false} animate={isOpen ? { top: 5.5, rotate: 45 } : { top: 0, rotate: 0 }} transition={soft} /> <motion.span className={cn(line, "top-[5.5px]")} initial={false} animate={{ opacity: isOpen ? 0 : 1 }} transition={soft} /> <motion.span className={cn(line, "top-[11px]")} initial={false} animate={isOpen ? { top: 5.5, rotate: -45 } : { top: 11, rotate: 0 }} transition={soft} /> </span> </button> ); }
Usage
import { Hamburger } from "@/components/ui/hamburger";
const [open, setOpen] = useState(false);
<Hamburger open={open} onOpenChange={setOpen} controls="main-menu" />
<nav id="main-menu" hidden={!open}>…</nav>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| open | boolean | — | Open state (controlled). |
| defaultOpen | boolean | false | Initial state (uncontrolled). |
| onOpenChange | (open: boolean) => void | — | Called when the button is pressed. |
| controls | string | — | id of the menu this button shows and hides (aria-controls). |
| labels | { closed, open } | false | { closed: "Menu", open: "Close" } | Text beside the lines; false hides it and sets an aria-label instead. |
AI prompt
Paste into v0, Lovable, Bolt or Claude to rebuild this component in your own project.
Create a React + Tailwind CSS + Motion (motion/react) component called Hamburger.
- A button with an optional label ("Menu" / "Close", text-sm font-medium) and three 1px lines in a 16x12 box.
- Open: the top line moves to the middle and rotates 45°, the bottom line moves to the middle and rotates -45°,
the middle line fades out. Spring with no bounce, duration 0.4s.
- Controlled (open + onOpenChange) or uncontrolled (defaultOpen).
- Accessibility: aria-expanded, aria-controls pointing at the menu, an aria-label when the text label is hidden,
visible focus ring (ring-[3px] ring-ring/50).










