Hamburger Menu
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:
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:
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>
);
}
<script setup lang="ts">
import { computed, ref } from "vue";
import { hamburgerMenu } from "virtual:styleframe";
const {
color = "neutral",
size = "md",
animation = "close",
label = "Toggle menu",
} = defineProps<{
color?: "light" | "dark" | "neutral";
size?: "sm" | "md" | "lg";
animation?:
| "close"
| "arrow-up"
| "arrow-down"
| "arrow-left"
| "arrow-right"
| "minus"
| "plus";
label?: string;
}>();
const active = ref(false);
const classes = computed(() =>
hamburgerMenu({
color,
size,
animation,
active: active.value ? "true" : "false",
}),
);
</script>
<template>
<button
type="button"
:class="classes"
:aria-expanded="active"
:aria-label="label"
@click="active = !active"
>
<span class="hamburger-menu-inner" />
</button>
</template>
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
| Color | Token | Use 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 |
neutral | Adaptive (light ↔ dark) | Default color, adapts to the current color scheme |
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:
| Size | Bar Width | Bar Height | Offset | Button Padding |
|---|---|---|---|---|
sm | 14px | 2px | 5px | @0.375 |
md | 20px | 2px | 6px | @0.5 |
lg | 26px | 3px | 8px | @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.
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>
| Part | Role |
|---|---|
.hamburger-menu | Outer button; owns hover, focus ring, disabled state, padding, and color |
.hamburger-menu-inner | Middle bar; also hosts the transform when active is true |
.hamburger-menu-inner::before | Top bar (pseudo-element) |
.hamburger-menu-inner::after | Bottom 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-visibleusing@color.primary. Do not remove it. - Hit target size. Default
mdpadding (@0.5) combined with20pxbar 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
:disabledstyling; 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:
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:
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;
API Reference
useHamburgerMenuRecipe(s, options?)
Creates the hamburger menu recipe with button, bar, and animation styling.
Parameters:
| Parameter | Type | Description |
|---|---|---|
s | Styleframe | The Styleframe instance |
options | DeepPartial<RecipeConfig> | Optional overrides for the recipe configuration |
options.base | VariantDeclarationsBlock | Custom base styles for the button |
options.variants | Variants | Custom variant definitions for the recipe |
options.defaultVariants | Record<keyof Variants, string> | Default variant values for the recipe |
options.compoundVariants | CompoundVariant[] | Custom compound variant definitions for the recipe |
options.filter | Record<string, string[]> | Limit which variant values are generated |
Variants:
| Variant | Options | Default |
|---|---|---|
color | light, dark, neutral | neutral |
size | sm, md, lg | md |
animation | close, arrow-up, arrow-down, arrow-left, arrow-right, minus, plus | close |
active | true, false | false |
Best Practices
- Track
activestate in the consumer. The recipe provides the CSS; the open/closed state lives in your app (ReactuseState, Vueref, etc.). - Pair
aria-expandedwithactive. Mirror theactiveprop ontoaria-expandedso screen readers announce state changes. - Pick the animation that matches the affordance. Use
closefor a classic menu toggle,arrow-*for directional drawers,plus/minusfor expand/collapse interactions. - Prefer
neutralcolor. It adapts to light and dark modes automatically; only reach forlightordarkwhen the button sits on a surface that conflicts with the user's theme. - Keep the HTML minimal. The recipe expects exactly one
.hamburger-menu-innerchild — don't wrap it or add siblings, or the::before/::afterpseudo-elements won't line up.
Dropdown
A menu-style floating panel for presenting actions or navigation options. Composed of five coordinated recipes (panel, item, separator, label, arrow) with three colors, three visual styles, and three sizes through the recipe system.
Overview
Explore Styleframe's comprehensive design token system. Create consistent, scalable design systems with composable functions for colors, typography, spacing, and more.