HamburgerButton
CSS-driven mobile menu trigger.
HamburgerButton
Domain: navigation
Package path: packages/ui/src/navigation/hamburger-button.tsx
Import: @ziteox/ui or @ziteox/ui/navigation
Mobile menu trigger. Three SVG bars morph into an X via CSS transitions driven by aria-expanded.
Preview
Usage
import { useState } from "react";
import { HamburgerButton } from "@ziteox/ui";
export function MobileNavTrigger() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<HamburgerButton
variant="circle"
isOpen={isOpen}
onOpenChange={setIsOpen}
className="lg:hidden"
/>
{/* App-owned menu overlay/sheet */}
</>
);
}Props
| Prop | Type | Required | Description |
|---|---|---|---|
isOpen | boolean | Yes | Open state; sets aria-expanded |
onOpenChange | (isOpen: boolean) => void | Yes | Called on click with toggled value |
className | string | No | Appended to root button classes |
variant | "default" | "circle" | No | circle is fully rounded (recommended for mobile nav) |
haptic | boolean | preset name | No | WebHaptics feedback on tap; default selection; false disables |
Install peer: pnpm add web-haptics
Accessibility
| Attribute | Value |
|---|---|
aria-expanded | Mirrors isOpen |
aria-label | "Open menu" / "Close menu" based on state |
| Screen reader | Hidden "Menu" text via ziteox-sr-only |
Styling
Default appearance: frosted glass in both themes — backdrop-filter blur, semi-transparent fill, and layered inset/outset shadows. Light mode uses a bright frosted panel with dark bars; dark mode (.dark / [data-theme="dark"] on an ancestor) uses a light frosted panel with white bars.
Scoped classes:
ziteox-hamburger-buttonziteox-hamburger-button--circle(fully rounded variant)ziteox-hamburger-bar-top|middle|bottomziteox-hamburger-icon
Responsive visibility (e.g. lg:hidden) is not built in. Pass via className.
Animation
| Concern | Implementation |
|---|---|
| Morph | CSS transform + transition on three <rect> elements |
| Easing | cubic-bezier per bar (matches original design) |
| Reduced motion | usePrefersReducedMotion → 150ms vs 300ms duration |
| Hydration | useSkipInitialAnimation disables transitions on first render |
Does not use motion. Uses web-haptics for tap feedback on supported mobile browsers (skipped when prefers-reduced-motion is set).
Expanded state transforms
| Bar | Closed | Open (aria-expanded="true") |
|---|---|---|
| Top | translate(7px, -5px) | rotate(315deg) |
| Middle | — | rotate(45deg) |
| Bottom | translateY(5px) | rotate(135deg) |
State ownership
HamburgerButton does not include menu panel, overlay, scroll lock, or context provider. The app provides:
- Open state (
useState, context, or URL) - Menu sheet component (future
@ziteox/uiMobileMenuTBD) - Body scroll lock on open (app concern today)
Architecture notes
| Decision | Rationale |
|---|---|
| Controlled API | Parent coordinates button + menu; no hidden global state |
CSS not motion | Simple transform morph; smaller bundle; no animation lib on critical path |
| No Tailwind in package | Framework-agnostic; scoped injected CSS |
No lg:hidden default | Visibility is layout concern per app |