Composables

Hamburger Menu

A three-bar toggle button that animates into a different glyph (X, arrow, plus, or minus) when opened. Supports three colors, three sizes, seven animations, and an active state through the recipe system.

Overview

The Hamburger Menu is an interactive toggle button typically used in mobile navigation headers to expand or collapse a drawer or sidebar. It renders as three horizontal bars in its resting state and animates into an alternative glyph — an X, arrow, plus, or minus — when the active state is set to true. The styling is provided by useHamburgerMenuRecipe(), which exposes color, size, animation, and active axes.

The Hamburger Menu recipe integrates directly with the default design tokens preset and generates type-safe utility classes at build time with zero runtime CSS. Visual state (open/closed) is driven by modifier classes, so transitions are pure CSS.

Why use the Hamburger Menu recipe?

The Hamburger Menu recipe helps you:

  • Ship faster with sensible defaults: Get 3 colors, 3 sizes, and 7 open-state animations out of the box with a single composable call.
  • Keep transitions smooth: CSS-driven transforms with staggered timings produce polished open-close animations without JavaScript.
  • Swap animations freely: Pick the glyph that fits your UI — X for close, arrows for directional menus, plus/minus for expand/collapse affordances.
  • Maintain consistency: Compound variants ensure every color adapts correctly across light and dark modes.
  • Customize without forking: Override base styles, default variants, or filter out options you don't need — all through the options API.
  • Stay type-safe: Full TypeScript support means your editor catches invalid color, size, animation, or active values at compile time.

Usage

Register the recipe

Add the Hamburger Menu recipe to a local Styleframe instance. The global styleframe.config.ts provides design tokens and utilities, while the component-level file registers the recipe itself:

src/components/hamburger-menu.styleframe.ts
import { styleframe } from 'virtual:styleframe';
import { useHamburgerMenuRecipe } from '@styleframe/theme';

const s = styleframe();

const hamburgerMenu = useHamburgerMenuRecipe(s);

export default s;

Build the component

Import the hamburgerMenu runtime function from the virtual module, manage active state locally, and render a <button> with a single .hamburger-menu-inner child — the pseudo-elements render the top and bottom bars automatically:

src/components/HamburgerMenu.tsx
import { useState } from "react";
import { hamburgerMenu } from "virtual:styleframe";

interface HamburgerMenuProps {
    color?: "light" | "dark" | "neutral";
    size?: "sm" | "md" | "lg";
    animation?:
        | "close"
        | "arrow-up"
        | "arrow-down"
        | "arrow-left"
        | "arrow-right"
        | "minus"
        | "plus";
    label?: string;
    onToggle?: (active: boolean) => void;
}

export function HamburgerMenu({
    color = "neutral",
    size = "md",
    animation = "close",
    label = "Toggle menu",
    onToggle,
}: HamburgerMenuProps) {
    const [active, setActive] = useState(false);
    const toggle = () => {
        const next = !active;
        setActive(next);
        onToggle?.(next);
    };
    return (
        <button
            type="button"
            className={hamburgerMenu({
                color,
                size,
                animation,
                active: active ? "true" : "false",
            })}
            aria-expanded={active}
            aria-label={label}
            onClick={toggle}
        >
            <span className="hamburger-menu-inner" />
        </button>
    );
}

See it in action

Colors

The Hamburger Menu recipe includes 3 color variants: light, dark, and neutral. The color value drives the bar color via the button's color CSS property (the inner bars use currentColor).

The neutral color adapts automatically: dark bars in light mode, light bars in dark mode, making it the safest default.

Color Reference

ColorTokenUse Case
light@color.gray-900 (fixed)Dark bars on light surfaces, stays light in dark mode
dark@color.white (fixed)Light bars on dark surfaces, stays dark in light mode
neutralAdaptive (light ↔ dark)Default color, adapts to the current color scheme
Pro tip: Use neutral as your default color. It adapts automatically to the user's color scheme without requiring separate light- and dark-mode props.

Sizes

Three size variants control the bar dimensions (width, height, vertical offset) and the button's padding:

SizeBar WidthBar HeightOffsetButton Padding
sm14px2px5px@0.375
md20px2px6px@0.5
lg26px3px8px@0.625

Animations

Seven animation values control how the bars morph when active is true. When active is false the button renders as three static horizontal bars regardless of which animation is selected.

Close

The classic hamburger → X transition. Top and bottom bars pivot in and the middle bar fades out, forming an X.

Arrow Left / Right

Bars scale down and rotate to form a (left) or (right). Useful for drawers that slide horizontally.

Arrow Up / Down

Like the horizontal arrows, but the entire icon rotates ±90° so the arrow points up or down.

Minus

All bars collapse into a single horizontal line — a subtle transition that suggests "collapsed" rather than "closed".

Plus

The middle bar stays horizontal while the top bar rotates 90° to form a vertical bar, producing a + glyph. The bottom bar fades out. Pairs well with expand/collapse interactions.

Active State

The active axis is a boolean ("true" / "false") that drives the open-state transform. Consumers are responsible for tracking the open state and toggling it on click.

Good to know: Pass active as a string ("true" or "false") when calling the runtime function — recipes stringify boolean variant values.

Anatomy

The Hamburger Menu renders as a single <button> element with one child <span> that represents the middle bar; the span's ::before and ::after pseudo-elements render the top and bottom bars:

<button class="hamburger-menu(...)" aria-expanded="false" aria-label="Toggle menu">
    <span class="hamburger-menu-inner"></span>
</button>
PartRole
.hamburger-menuOuter button; owns hover, focus ring, disabled state, padding, and color
.hamburger-menu-innerMiddle bar; also hosts the transform when active is true
.hamburger-menu-inner::beforeTop bar (pseudo-element)
.hamburger-menu-inner::afterBottom bar (pseudo-element)

Accessibility

  • Expose the toggle state. Always set aria-expanded="true|false" on the button so assistive technologies know whether the menu is open.
  • Provide an accessible name. Set aria-label="Toggle menu" (or localized equivalent) since the button has no visible text.
  • Focus visibility. The recipe applies a keyboard-only focus ring via &:focus-visible using @color.primary. Do not remove it.
  • Hit target size. Default md padding (@0.5) combined with 20px bar width yields a minimum 24px × 24px hit target; pair with additional padding on the parent container to reach the WCAG 2.1 44px target on mobile.
  • Disable carefully. The recipe supports :disabled styling; when disabled, pointer events are ignored and opacity drops to 0.5.
<!-- Correct: accessible hamburger menu -->
<button
    class="hamburger-menu(...)"
    type="button"
    aria-expanded="true"
    aria-controls="main-nav"
    aria-label="Close menu"
>
    <span class="hamburger-menu-inner"></span>
</button>

Customization

Overriding Defaults

The useHamburgerMenuRecipe composable accepts an optional second argument to override any part of the recipe configuration. Overrides are deep-merged with the defaults:

src/components/hamburger-menu.styleframe.ts
import { styleframe } from 'virtual:styleframe';
import { useHamburgerMenuRecipe } from '@styleframe/theme';

const s = styleframe();

const hamburgerMenu = useHamburgerMenuRecipe(s, {
    defaultVariants: {
        color: 'dark',
        size: 'lg',
        animation: 'arrow-left',
        active: 'false',
    },
});

export default s;

Filtering Variants

If you only need a subset of the available animations, use the filter option to limit which values are generated. This reduces the output CSS:

src/components/hamburger-menu.styleframe.ts
import { styleframe } from 'virtual:styleframe';
import { useHamburgerMenuRecipe } from '@styleframe/theme';

const s = styleframe();

// Only generate the close animation in all three colors
const hamburgerMenu = useHamburgerMenuRecipe(s, {
    filter: {
        animation: ['close'],
    },
});

export default s;
Good to know: Filtering also removes compound variants and adjusts default variants that reference filtered-out values, so your recipe stays consistent.

API Reference

useHamburgerMenuRecipe(s, options?)

Creates the hamburger menu recipe with button, bar, and animation styling.

Parameters:

ParameterTypeDescription
sStyleframeThe Styleframe instance
optionsDeepPartial<RecipeConfig>Optional overrides for the recipe configuration
options.baseVariantDeclarationsBlockCustom base styles for the button
options.variantsVariantsCustom variant definitions for the recipe
options.defaultVariantsRecord<keyof Variants, string>Default variant values for the recipe
options.compoundVariantsCompoundVariant[]Custom compound variant definitions for the recipe
options.filterRecord<string, string[]>Limit which variant values are generated

Variants:

VariantOptionsDefault
colorlight, dark, neutralneutral
sizesm, md, lgmd
animationclose, arrow-up, arrow-down, arrow-left, arrow-right, minus, plusclose
activetrue, falsefalse

Learn more about recipes →

Best Practices

  • Track active state in the consumer. The recipe provides the CSS; the open/closed state lives in your app (React useState, Vue ref, etc.).
  • Pair aria-expanded with active. Mirror the active prop onto aria-expanded so screen readers announce state changes.
  • Pick the animation that matches the affordance. Use close for a classic menu toggle, arrow-* for directional drawers, plus/minus for expand/collapse interactions.
  • Prefer neutral color. It adapts to light and dark modes automatically; only reach for light or dark when the button sits on a surface that conflicts with the user's theme.
  • Keep the HTML minimal. The recipe expects exactly one .hamburger-menu-inner child — don't wrap it or add siblings, or the ::before/::after pseudo-elements won't line up.