A crisp hamburger icon that morphs into a clear “X” is one of those tiny touches that makes an interface feel polished. In this walkthrough you will build an animated hamburger menu icon toggle that smoothly transitions between the two states with CSS transitions, clean HTML, and a small dose of accessible JavaScript to keep screen readers in sync. By the end you will fully understand how to structure the markup, style the three bars, and pivot them into a balanced close icon.
Why Animating a Hamburger Menu Icon Toggle Matters
A static icon communicates state poorly. When an icon toggles between open and close with a subtle animation, it tells users what just happened and what will happen next if they click again. You also avoid swapping images or icon fonts, which reduces requests and keeps styling in your control. Pure CSS lines render crisply at any scale, they theme with variables, and they adapt to dark mode or brand colors instantly. If you want a refresher on the shape itself, see this standalone guide to the CSS hamburger menu icon. Since the end state becomes a close button, you can also reference the companion X icon shape for visual benchmarks.
Prerequisites
You do not need a framework or build tool for this. A single HTML file will do, with styles in a stylesheet or a style block. Knowledge of CSS variables and pseudo-elements will make the code readable on first pass.
- Basic HTML
- CSS custom properties
- CSS pseudo-elements (::before / ::after)
Step 1: The HTML Structure
The icon is a real button for keyboard and screen reader support. A child span becomes the middle line, and its ::before and ::after pseudo-elements create the top and bottom lines. A small script toggles aria-expanded and the label so the animation and accessibility stay aligned. This is the complete, final HTML for the demo, including a placeholder nav to show a common structure.
<!-- HTML -->
<button class="burger" aria-label="Open menu" aria-controls="site-nav" aria-expanded="false">
<span class="burger__lines" aria-hidden="true"></span>
<span class="sr-only">Menu</span>
</button>
<nav id="site-nav" hidden>
<!-- Your navigation lives here -->
</nav>
<script>
// JS
const burger = document.querySelector('.burger');
if (burger) {
burger.addEventListener('click', () => {
const isOpen = burger.getAttribute('aria-expanded') === 'true';
burger.setAttribute('aria-expanded', String(!isOpen));
burger.setAttribute('aria-label', isOpen ? 'Open menu' : 'Close menu');
});
}
</script>
This structure keeps the icon accessible and straightforward. The button hosts the visual lines and exposes the correct control semantics with aria-controls and aria-expanded. The JavaScript flips the state and label without touching classes, which lets CSS use attribute selectors to drive the animation.
Step 2: The Basic CSS & Styling
Start by defining a few custom properties for size, color, and timing. The button uses a circular tap target, which helps thumb usability and keeps the layout tidy. If you want a quick refresher on building a rounded hit area, this tutorial on making a circle with CSS shows the border-radius trick in isolation.
/* CSS */
:root {
--burger-size: 48px;
--line-length: 28px;
--line-thickness: 2px;
--line-gap: 8px;
--line-radius: 2px;
--line-color: #222;
--bg-hover: #f2f2f2;
--focus: 2px solid #4e8cff;
--ease: cubic-bezier(.2,.7,.3,1);
--speed: 240ms;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html, body {
height: 100%;
}
body {
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
color: #1a1a1a;
display: grid;
place-items: center;
background: white;
}
.burger {
inline-size: var(--burger-size);
block-size: var(--burger-size);
border: 0;
background: transparent;
color: var(--line-color);
display: inline-grid;
place-items: center;
border-radius: 50%;
cursor: pointer;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
transition: background-color var(--speed) var(--ease);
}
.burger:hover {
background-color: var(--bg-hover);
}
.burger:focus-visible {
outline: var(--focus);
outline-offset: 2px;
}
.sr-only {
position: absolute;
inline-size: 1px;
block-size: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
Advanced Tip: All visuals key off CSS variables. This makes theming trivial. You can pull the colors from a design token system or a dark mode media query and the icon will adapt without code changes.
Step 3: Building the Icon Lines
The three bars are a single element for the middle line and two pseudo-elements for the top and bottom. Using one DOM node for all bars keeps markup lean and guarantees perfect alignment when you animate toward the center.
/* CSS */
.burger__lines {
position: relative;
inline-size: var(--line-length);
block-size: var(--line-thickness);
background-color: var(--line-color);
border-radius: var(--line-radius);
transition:
transform var(--speed) var(--ease),
opacity var(--speed) var(--ease),
background-color var(--speed) var(--ease);
}
.burger__lines::before,
.burger__lines::after {
content: "";
position: absolute;
inset: 0;
background-color: var(--line-color);
border-radius: var(--line-radius);
transition: transform var(--speed) var(--ease);
transform-origin: 50% 50%;
}
/* Offset the top and bottom bars vertically */
.burger__lines::before {
transform: translateY(calc(-1 * var(--line-gap)));
}
.burger__lines::after {
transform: translateY(var(--line-gap));
}
How This Works (Code Breakdown)
The button centers a single .burger__lines element. That element renders the middle bar. Both ::before and ::after inherit the same size and color using inset: 0, which overlays them exactly on the middle line. Each pseudo-element then moves up or down using translateY by the gap value to form the three-bar stack.
Using transform for the offset is deliberate. When you animate back to the center, you animate transform again to remove the translation and add rotation. This keeps the work on the compositor thread for smooth frames. The transform-origin stays centered so each bar rotates around its midpoint rather than a corner.
The line radius rounds the ends a bit, which gives a friendlier look and hides tiny antialiasing when lines overlap during the animation.
If you want to compare static icon recipes, the CSS hamburger menu icon guide shows another approach that uses absolute positioning rather than translation. Either way works; translation tends to yield cleaner motion when animating to the “X” state.
Step 4: Toggling to the X State
When the button is pressed, aria-expanded flips to true. CSS watches that attribute and transitions the bars toward the center, rotating them to form the close “X”. The middle line fades out to reduce visual clutter.
/* CSS */
.burger[aria-expanded="true"] .burger__lines {
background-color: transparent; /* hide middle bar */
}
.burger[aria-expanded="true"] .burger__lines::before {
transform: translateY(0) rotate(45deg);
}
.burger[aria-expanded="true"] .burger__lines::after {
transform: translateY(0) rotate(-45deg);
}
/* Optional: a bit more presence while open */
.burger[aria-expanded="true"] {
background-color: var(--bg-hover);
}
How This Works (Code Breakdown)
The attribute selector .burger[aria-expanded=”true”] is the toggle. It pairs cleanly with the minimal JavaScript that only updates aria-expanded on click. No classes, no data attributes. In the open state, the pseudo-elements shed their vertical translation so they meet at the center. Each receives an opposite rotation: +45 degrees on the top bar and -45 degrees on the bottom bar. The middle line turns transparent so the “X” looks crisp and balanced.
These changes animate with the same transition timing you set on the base lines. Because all movement and rotation lives in transform, the browser can animate on the GPU without layout thrash. If the timing feels snappy or sluggish, adjust –speed or the cubic-bezier curve to taste.
Visually compare the end state to the dedicated X icon recipe. The angles should match, and the intersection should sit at the center of the button’s hit area. If the bars look too thick when crossed, reduce –line-thickness slightly for a more refined look.
Advanced Techniques: Adding Animations & Hover Effects
You can make the motion feel more tactile with minor tweaks: scale the icon during hover, stagger the lines by a few milliseconds, and add a “press” feedback scale. Keep everything subtle so it does not distract.
/* CSS */
.burger {
transition:
background-color var(--speed) var(--ease),
transform 120ms ease-out;
}
.burger:active {
transform: scale(0.96);
}
/* Slight stagger for the cross lines */
.burger__lines::before,
.burger__lines::after {
transition:
transform var(--speed) var(--ease);
}
.burger[aria-expanded="true"] .burger__lines::before {
transition-delay: 20ms;
}
.burger[aria-expanded="true"] .burger__lines::after {
transition-delay: 40ms;
}
/* Theme swap example */
@media (prefers-color-scheme: dark) {
:root {
--line-color: #f5f5f5;
--bg-hover: rgba(255,255,255,0.08);
--focus: 2px solid #a7c2ff;
}
}
/* Motion safety */
@media (prefers-reduced-motion: reduce) {
.burger,
.burger__lines,
.burger__lines::before,
.burger__lines::after {
transition: none;
}
}
The staggered transition-delay creates a tiny cascade as the bars settle into the “X”, which gives the motion a bit of personality without feeling busy. The pressed scale helps confirm the click on touch devices. The prefers-color-scheme rule flips colors automatically for dark mode, and prefers-reduced-motion removes animation for people who request reduced movement.
Accessibility & Performance
This tiny component is visible and tactile, but it also carries input semantics and state for assistive tech. The markup remains minimal while still communicating the right control name and state with ARIA. The animation uses transforms that perform well across devices, and the code avoids layout triggers that would cause dropped frames.
Accessibility
The icon is a real button, which means keyboard users can Tab to it and activate with Enter or Space. The aria-controls relationship points to the affected region. aria-expanded flips between true and false so screen readers announce the current state. The script also updates aria-label to “Open menu” or “Close menu,” which clarifies intent.
The .sr-only technique hides the “Menu” text visually while keeping it in the accessibility tree. If you prefer visible text, remove the sr-only styles and place descriptive text next to the icon. The prefers-reduced-motion query removes transitions for people who prefer less animation, which is polite and prevents discomfort.
Performance
The icon animates transform and opacity only, which keeps the work on the compositor. There are no box-shadow or filter transitions here, which helps maintain smoothness. The footprint is a single button with one child element and two pseudo-elements, so the DOM cost is tiny. Attribute selectors are inexpensive at this scale, and the JavaScript only flips two attributes on click.
From Burger to Close, Smoothly
You built a hamburger menu icon that morphs cleanly into a close “X,” driven by CSS variables, pseudo-elements, and a tiny ARIA-aware click handler. You now have a reusable, theme-ready toggle that fits any layout and respects user motion preferences. Expand it into a design system token, or tune the timing curve to match your brand’s motion language.