SolutionsCase StudiesAI ToolsResourcesAboutContact
Get Your Free AI Automation Audit Book a Free 30-Minute Strategy Call
Playground~/frontend Open editor

~/css/hamburger-menus

8 CSS Hamburger Menus

1 Pure CSS7 with JSPublished 2026-10-04Free · no sign-up

Eight hamburger icons and mobile menus: animated three-line icons that become a cross or an arrow, a button with a visible Menu label, a slide-out panel, a full-screen overlay, kebab and meatball icons, a two-stage squeeze animation and a simple dropdown list.

01. Hamburger to Cross

MenuAnimation

The classic three-line icon that morphs into an X when the menu opens. The middle bar fades out while the outer bars rotate into a cross, the button toggles aria-expanded, and its accessible name stays Menu in both states.

View code
<button class="hb" aria-expanded="false" aria-label="Menu"><span></span><span></span><span></span></button>
.hb { display: grid; gap: 6px; width: 52px; height: 52px; padding: 14px 13px; border: 1px solid #2f3749; border-radius: 14px; background: #151a26; cursor: pointer; }
.hb span { display: block; height: 3px; border-radius: 2px; background: #fff; transition: transform .25s, opacity .2s; }
.hb[aria-expanded=true] span:nth-child(1) { transform: translateY(9px) rotate(45deg); }
.hb[aria-expanded=true] span:nth-child(2) { opacity: 0; }
.hb[aria-expanded=true] span:nth-child(3) { transform: translateY(-9px) rotate(-45deg); }
.hb:focus-visible { outline: 2px solid #9db8ff; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .hb span { transition: none; } }
const hb = document.querySelector('.hb');
hb.addEventListener('click', () => hb.setAttribute('aria-expanded', String(hb.getAttribute('aria-expanded') !== 'true')));

02. Hamburger to Arrow

MenuBack

A variation where the three lines become a left-pointing arrow, useful when opening a menu means navigating back. The top and bottom bars shorten and rotate towards the left end of the middle bar, all with transforms.

View code
<button class="ha" aria-expanded="false" aria-label="Menu"><span></span><span></span><span></span></button>
.ha { display: grid; gap: 6px; width: 52px; height: 52px; padding: 15px 13px; border: 0; border-radius: 50%; background: #101828; cursor: pointer; }
.ha span { display: block; height: 3px; border-radius: 2px; background: #fff; transform-origin: left center; transition: transform .25s, width .25s; }
.ha[aria-expanded=true] span:nth-child(1) { width: 60%; transform: translateY(3px) rotate(-40deg); }
.ha[aria-expanded=true] span:nth-child(3) { width: 60%; transform: translateY(-3px) rotate(40deg); }
.ha:focus-visible { outline: 2px solid #2563eb; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .ha span { transition: none; } }
const ha = document.querySelector('.ha');
ha.addEventListener('click', () => ha.setAttribute('aria-expanded', String(ha.getAttribute('aria-expanded') !== 'true')));

03. Menu Button with Text Label

MenuUsability

Pairing the icon with the word Menu makes it clearer for people who do not recognise the three-line symbol. The visible text doubles as the accessible name, and the label changes to Close when the menu is open.

View code
<button class="hl" aria-expanded="false"><span class="ic" aria-hidden="true"><i></i><i></i><i></i></span><span class="tx">Menu</span></button>
.hl { display: inline-flex; align-items: center; gap: 10px; padding: 10px 16px; border: 1px solid #3a4256; border-radius: 999px; background: none; color: #fff; font: 700 14px Inter, system-ui, sans-serif; letter-spacing: .04em; text-transform: uppercase; cursor: pointer; }
.ic { display: grid; gap: 4px; width: 18px; }
.ic i { height: 2px; border-radius: 1px; background: currentColor; transition: transform .2s, opacity .2s; }
.hl[aria-expanded=true] .ic i:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.hl[aria-expanded=true] .ic i:nth-child(2) { opacity: 0; }
.hl[aria-expanded=true] .ic i:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }
.hl:focus-visible { outline: 2px solid #fbbf24; outline-offset: 3px; }
const hl = document.querySelector('.hl');
hl.addEventListener('click', () => {
  const open = hl.getAttribute('aria-expanded') !== 'true';
  hl.setAttribute('aria-expanded', String(open));
  hl.querySelector('.tx').textContent = open ? 'Close' : 'Menu';
});

04. Hamburger with Slide-Out Menu

MenuMobile

A complete mobile header: the hamburger opens a panel that slides in from the left. The button controls the panel with aria-controls and aria-expanded, Escape closes the panel and returns focus to the button, and the links are a normal list.

View code
<div class="ph-wrap">
  <header class="mh">
    <b>Fernhill</b>
    <button class="mb" aria-expanded="false" aria-controls="mnav" aria-label="Menu"><span></span><span></span><span></span></button>
  </header>
  <nav id="mnav" class="panel" aria-label="Main">
    <ul><li><a href="#">Shop</a></li><li><a href="#">Workshops</a></li><li><a href="#">About</a></li><li><a href="#">Contact</a></li></ul>
  </nav>
</div>
.ph-wrap { position: relative; width: 300px; height: 320px; overflow: hidden; border-radius: 22px; background: #f9fafb; box-shadow: 0 0 0 8px #101828; font-family: Inter, system-ui, sans-serif; }
.mh { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; background: #fff; border-bottom: 1px solid #eef0f4; }
.mh b { color: #101828; }
.mb { display: grid; gap: 5px; width: 40px; height: 40px; padding: 11px 10px; border: 0; border-radius: 10px; background: #f2f4f7; cursor: pointer; }
.mb span { height: 2px; background: #101828; }
.mb:focus-visible { outline: 2px solid #2563eb; }
.panel { position: absolute; top: 69px; bottom: 0; left: 0; width: 78%; background: #101828; transform: translateX(-100%); transition: transform .3s; visibility: hidden; }
.panel.open { transform: none; visibility: visible; }
.panel ul { margin: 0; padding: 12px 0; list-style: none; }
.panel a { display: block; padding: 14px 22px; color: #fff; font-weight: 600; text-decoration: none; }
.panel a:hover, .panel a:focus-visible { background: #1d2939; outline: none; }
@media (prefers-reduced-motion: reduce) { .panel { transition: none; } }
const mb = document.querySelector('.mb'), panel = document.getElementById('mnav');
function set(open) {
  mb.setAttribute('aria-expanded', String(open));
  panel.classList.toggle('open', open);
  if (open) panel.querySelector('a').focus();
}
mb.addEventListener('click', () => set(mb.getAttribute('aria-expanded') !== 'true'));
document.addEventListener('keydown', e => { if (e.key === 'Escape' && panel.classList.contains('open')) { set(false); mb.focus(); } });

05. Full-Screen Overlay Menu

MenuOverlay

A bold full-screen menu for portfolio and agency sites: large links fade in one after another over a dark overlay. The stagger uses a custom property per item for its delay, and the overlay is hidden from the keyboard until it opens.

View code
<div class="fs-wrap">
  <button class="fsb" aria-expanded="false" aria-controls="fsm">Menu</button>
  <nav class="fsm" id="fsm" aria-label="Main">
    <a href="#" style="--i:0">Work</a><a href="#" style="--i:1">Studio</a><a href="#" style="--i:2">Journal</a><a href="#" style="--i:3">Say hello</a>
  </nav>
</div>
.fs-wrap { position: relative; width: min(460px, 94vw); height: 330px; overflow: hidden; border-radius: 18px; background: #0b0d14; font-family: Inter, system-ui, sans-serif; }
.fsb { position: absolute; top: 16px; right: 16px; z-index: 2; padding: 8px 16px; border: 1px solid #fff; border-radius: 999px; background: none; color: #fff; font: 700 13px Inter, sans-serif; cursor: pointer; }
.fsb:focus-visible { outline: 2px solid #f472b6; outline-offset: 3px; }
.fsm { position: absolute; inset: 0; display: grid; align-content: center; gap: 4px; padding: 0 34px; background: linear-gradient(135deg, #1e1b4b, #0b0d14); visibility: hidden; opacity: 0; transition: opacity .3s, visibility .3s; }
.fsm.open { visibility: visible; opacity: 1; }
.fsm a { color: #fff; font-size: 38px; font-weight: 800; letter-spacing: -.02em; text-decoration: none; opacity: 0; transform: translateY(14px); transition: opacity .4s, transform .4s, color .2s; transition-delay: calc(var(--i) * 70ms); }
.fsm.open a { opacity: 1; transform: none; }
.fsm a:hover, .fsm a:focus-visible { color: #f472b6; outline: none; }
@media (prefers-reduced-motion: reduce) { .fsm, .fsm a { transition: none; } }
const fb = document.querySelector('.fsb'), fm = document.getElementById('fsm');
fb.addEventListener('click', () => {
  const open = !fm.classList.contains('open');
  fm.classList.toggle('open', open);
  fb.setAttribute('aria-expanded', String(open));
  fb.textContent = open ? 'Close' : 'Menu';
});

06. Kebab and Meatball Icons

IconOverflow

Overflow menu triggers drawn entirely in CSS: three vertical dots (kebab) and three horizontal dots (meatball), made from one dot plus two box-shadow copies. Each is a real button with an accessible name such as More options.

View code
<div class="dots-row">
  <button class="kb" aria-label="More options" aria-haspopup="menu"><span></span></button>
  <button class="mt" aria-label="More actions" aria-haspopup="menu"><span></span></button>
</div>
.dots-row { display: flex; gap: 20px; }
.dots-row button { display: grid; place-items: center; width: 44px; height: 44px; border: 1.5px solid #d0d5dd; border-radius: 12px; background: #fff; cursor: pointer; }
.dots-row button:hover { background: #f2f4f7; }
.dots-row button:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
.dots-row span { width: 4px; height: 4px; border-radius: 50%; background: #101828; }
.kb span { box-shadow: 0 -7px 0 #101828, 0 7px 0 #101828; }
.mt span { box-shadow: -7px 0 0 #101828, 7px 0 0 #101828; }

07. Squeeze Hamburger

MenuAnimation

The bars first squeeze together into a single line, then rotate into an X, a two-stage motion made with transition delays that run in reverse when closing. It feels more mechanical and deliberate than a straight rotation.

View code
<button class="sq" aria-expanded="false" aria-label="Menu"><span></span><span></span><span></span></button>
.sq { display: grid; place-items: center; width: 54px; height: 54px; border: 0; border-radius: 12px; background: #f97316; cursor: pointer; }
.sq span { grid-area: 1 / 1; display: block; width: 26px; height: 3px; border-radius: 2px; background: #1a0d03; transition: transform .2s, opacity .2s; transition-delay: .2s, .2s; }
.sq span:nth-child(1) { transform: translateY(-8px); }
.sq span:nth-child(3) { transform: translateY(8px); }
.sq[aria-expanded=true] span { transition-delay: 0s, 0s; }
.sq[aria-expanded=true] span:nth-child(1) { transform: rotate(45deg); transition: transform .2s .2s; }
.sq[aria-expanded=true] span:nth-child(2) { opacity: 0; }
.sq[aria-expanded=true] span:nth-child(3) { transform: rotate(-45deg); transition: transform .2s .2s; }
.sq:focus-visible { outline: 2px solid #fed7aa; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .sq span { transition: none !important; } }
const sq = document.querySelector('.sq');
sq.addEventListener('click', () => sq.setAttribute('aria-expanded', String(sq.getAttribute('aria-expanded') !== 'true')));

08. Hamburger with Dropdown List

MenuDropdown

A compact menu that drops down below the button instead of sliding in, suited to simple sites with four or five pages. Clicking outside closes it, Escape closes it and returns focus, and the open list is a normal set of links.

View code
<div class="dd">
  <button class="ddb" aria-expanded="false" aria-controls="ddl"><span aria-hidden="true">&#9776;</span> Menu</button>
  <ul class="ddl" id="ddl" hidden>
    <li><a href="#">Recipes</a></li><li><a href="#">Meal plans</a></li><li><a href="#">Shopping lists</a></li><li><a href="#">Account</a></li>
  </ul>
</div>
.dd { position: relative; font-family: Inter, system-ui, sans-serif; }
.ddb { display: inline-flex; align-items: center; gap: 8px; padding: 10px 16px; border: 1.5px solid #d0d5dd; border-radius: 10px; background: #fff; color: #101828; font: 700 14px Inter, sans-serif; cursor: pointer; }
.ddb:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
.ddl { position: absolute; top: calc(100% + 6px); left: 0; min-width: 190px; margin: 0; padding: 6px; list-style: none; border: 1px solid #e4e7ec; border-radius: 12px; background: #fff; box-shadow: 0 16px 36px -14px rgba(16,24,40,.35); }
.ddl a { display: block; padding: 10px 12px; border-radius: 8px; color: #344054; font-size: 14px; text-decoration: none; }
.ddl a:hover, .ddl a:focus-visible { background: #f2f4f7; color: #101828; outline: none; }
const db = document.querySelector('.ddb'), dl = document.getElementById('ddl');
function setOpen(o) { dl.hidden = !o; db.setAttribute('aria-expanded', String(o)); }
db.addEventListener('click', () => setOpen(dl.hidden));
document.addEventListener('click', e => { if (!e.target.closest('.dd')) setOpen(false); });
document.addEventListener('keydown', e => { if (e.key === 'Escape' && !dl.hidden) { setOpen(false); db.focus(); } });

About these CSS Hamburger Menus

The button, not the icon

A hamburger is a button. Use <button> with an accessible name such as Menu, add aria-expanded that switches between true and false, and point aria-controls at the menu it opens. Screen readers then announce Menu, collapsed or expanded.

Animating the bars

The three bars are spans. When aria-expanded becomes true, CSS rotates the outer bars with transform and fades the middle one. Styling from the ARIA attribute keeps the visual state and the announced state in sync automatically.

Say Menu

Many people do not recognise the three-line icon. Adding the visible word Menu, as in the labelled demo, makes the control clearer for everyone and doubles as the accessible name.

Escape and focus

When the menu opens, move focus to the first link. When Escape is pressed, close it and return focus to the button, so keyboard users never get lost behind an overlay.

Frequently asked questions

Do hamburger menus need JavaScript?

Toggling aria-expanded needs a few lines of JavaScript. The animation itself is pure CSS.

Should desktop sites use a hamburger?

Usually not. Show the links directly when there is room and keep the hamburger for small screens.

How do I respect reduced motion?

Wrap transitions in a prefers-reduced-motion check, as every demo here does.