Técnicas de botón hamburguesa

Cinco formas de animar la transición de ≡ a ✕. Hacé clic en cada botón para ver la transición. Debajo, las técnicas documentadas con su explicación y el código listo para copiar y pegar.

clásica

Rotate + fade

La línea del medio desaparece. Las otras rotan ±45° en su lugar.

rebote

Rebote + rotate

Rebote elástico: las laterales giran con overshoot y encastran en la cruz.

avanzada

SVG path morph

Los paths se redibujan directamente de ≡ a ✕ con transición de coordenadas.

asimétrica

Bisagra izquierda

Cruz con bisagra a la izquierda: las laterales giran desde el extremo, gesto asimétrico.

expresiva

Spin 180°

El icono completo gira medio círculo mientras las líneas se transforman.

La más clásica y reconocida. Ideal para menús genéricos donde querés una transición directa y sin sorpresas.

La línea central se desvanece (opacity 0 + scaleX 0) mientras las laterales suben y bajan hasta el centro y giran 45°. Como pivotan sobre su propio centro, quedan cruzadas justo en el medio del botón.

HTML
<button class="btn btn--t1" type="button" aria-label="Abrir menú" aria-expanded="false">
  <span class="lines">
    <span class="line l1"></span>
    <span class="line l2"></span>
    <span class="line l3"></span>
  </span>
</button>
CSS
:root{
  --bg:hsl(214,5%,11%);
  --surface2:hsl(214,4%,5%);
  --border:hsl(214,35%,22%);
  --text:hsl(215,25%,85%);
  --radius-sm:0.375rem;
}

*{
  box-sizing:border-box;
  margin:0;
}

.btn{
  width:52px;
  height:52px;
  background:var(--surface2);
  border:2px solid var(--border);
  border-radius:var(--radius-sm);
  cursor:pointer;
  display:flex;
  align-items:center;
  justify-content:center;
  position:relative;
  overflow:hidden;
}

.btn:hover{
  border-color:hsl(22,100%,62%);
}

.lines{
  position:relative;
  width:22px;
  height:22px;
}

.line{
  position:absolute;
  top:50%;
  left:50%;
  width:22px;
  height:2px;
  background:var(--text);
  border-radius:2px;
  transform-origin:center;
  transition-timing-function:cubic-bezier(.4,0,.2,1);
}

.btn--t1 .line{
  transition:transform .3s,opacity .3s;
}

.btn--t1 .l1{
  transform:translate(-50%,calc(-50% - 7px));
}

.btn--t1 .l2{
  transform:translate(-50%,-50%);
}

.btn--t1 .l3{
  transform:translate(-50%,calc(-50% + 7px));
}

.btn--t1.is-open .l1{
  transform:translate(-50%,-50%) rotate(45deg);
}

.btn--t1.is-open .l2{
  opacity:0;
  transform:translate(-50%,-50%) scaleX(0);
}

.btn--t1.is-open .l3{
  transform:translate(-50%,-50%) rotate(-45deg);
}

JS
const btn = document.querySelector('.btn--t1');
btn.addEventListener('click', () => {
  const open = btn.classList.toggle('is-open');
  btn.setAttribute('aria-expanded', String(open));
  btn.setAttribute('aria-label', open ? 'Cerrar menú' : 'Abrir menú');
});

Variante con rebote: las laterales exageran el giro y vuelven a encastrar (easing con overshoot), un gesto más elástico que la clásica.

Mismo resultado final (cruz centrada) pero con un easing de overshoot: las líneas pasan del ángulo objetivo y rebotan al encastrar, dándole un toque más vivo que la clásica.

HTML
<button class="btn btn--t2" type="button" aria-label="Abrir menú" aria-expanded="false">
  <span class="lines">
    <span class="line l1"></span>
    <span class="line l2"></span>
    <span class="line l3"></span>
  </span>
</button>
CSS
:root{
  --bg:hsl(214,5%,11%);
  --surface2:hsl(214,4%,5%);
  --border:hsl(214,35%,22%);
  --text:hsl(215,25%,85%);
  --radius-sm:0.375rem;
}

*{
  box-sizing:border-box;
  margin:0;
}

.btn{
  width:52px;
  height:52px;
  background:var(--surface2);
  border:2px solid var(--border);
  border-radius:var(--radius-sm);
  cursor:pointer;
  display:flex;
  align-items:center;
  justify-content:center;
  position:relative;
  overflow:hidden;
}

.btn:hover{
  border-color:hsl(22,100%,62%);
}

.lines{
  position:relative;
  width:22px;
  height:22px;
}

.line{
  position:absolute;
  top:50%;
  left:50%;
  width:22px;
  height:2px;
  background:var(--text);
  border-radius:2px;
  transform-origin:center;
  transition-timing-function:cubic-bezier(.4,0,.2,1);
}

.btn--t2 .line{
  transition:transform .45s cubic-bezier(0.34,1.56,0.64,1),opacity .2s;
}

.btn--t2 .l1{
  transform:translate(-50%,calc(-50% - 7px));
}

.btn--t2 .l2{
  transform:translate(-50%,-50%);
}

.btn--t2 .l3{
  transform:translate(-50%,calc(-50% + 7px));
}

.btn--t2.is-open .l1{
  transform:translate(-50%,-50%) rotate(45deg);
}

.btn--t2.is-open .l2{
  opacity:0;
}

.btn--t2.is-open .l3{
  transform:translate(-50%,-50%) rotate(-45deg);
}

JS
const btn = document.querySelector('.btn--t2');
btn.addEventListener('click', () => {
  const open = btn.classList.toggle('is-open');
  btn.setAttribute('aria-expanded', String(open));
  btn.setAttribute('aria-label', open ? 'Cerrar menú' : 'Abrir menú');
});

La más "premium". En lugar de tres barras, un único <svg> con tres paths que se redibujan de ≡ a ✕.

La transición anima la propiedad CSS d de cada path (≡ → ✕). Clave: hay que asignarla vía element.style.d = 'path(...)', no con setAttribute, para que el navegador la interpole. La línea central colapsa a un punto y se oculta con opacity.

HTML
<button class="btn" type="button" aria-label="Abrir menú" aria-expanded="false">
  <svg class="svg" viewBox="0 0 28 28" aria-hidden="true">
    <path class="path" id="p1" d="M5,8 L23,8"></path>
    <path class="path" id="p2" d="M5,14 L23,14"></path>
    <path class="path" id="p3" d="M5,20 L23,20"></path>
  </svg>
</button>
CSS
:root{
  --bg:hsl(214,5%,11%);
  --surface2:hsl(214,4%,5%);
  --border:hsl(214,35%,22%);
  --text:hsl(215,25%,85%);
  --radius-sm:0.375rem;
}

*{
  box-sizing:border-box;
  margin:0;
}

.btn{
  width:52px;
  height:52px;
  background:var(--surface2);
  border:2px solid var(--border);
  border-radius:var(--radius-sm);
  cursor:pointer;
  display:flex;
  align-items:center;
  justify-content:center;
}

.btn:hover{
  border-color:hsl(22,100%,62%);
}

.svg{
  width:28px;
  height:28px;
  overflow:visible;
}

.path{
  fill:none;
  stroke:var(--text);
  stroke-width:2;
  stroke-linecap:round;
  transition:d .4s cubic-bezier(.4,0,.2,1),opacity .3s;
}

JS
const btn = document.querySelector('.btn');
const paths = btn.querySelectorAll('.path');
const closed = ['M5,8 L23,8','M5,14 L23,14','M5,20 L23,20'];
const open = ['M7,7 L21,21','M14,14 L14,14','M21,7 L7,21'];
let isOpen = false;
btn.addEventListener('click', () => {
  isOpen = !isOpen;
  paths.forEach((p, i) => {
    p.style.d = 'path("' + (isOpen ? open[i] : closed[i]) + '")';
    p.style.opacity = (isOpen && i === 1) ? '0' : '1';
  });
  btn.setAttribute('aria-expanded', String(isOpen));
  btn.setAttribute('aria-label', isOpen ? 'Cerrar menú' : 'Abrir menú');
});

La cruz se forma pivotando desde el extremo izquierdo: las laterales "balancan" desde esa bisagra en vez de girar sobre su centro.

La línea central se difumina y las laterales rotan ±45°, pero con el pivote en su extremo izquierdo: la intersección queda en el borde, así el gesto es asimétrico en vez de simétrico.

HTML
<button class="btn btn--t4" type="button" aria-label="Abrir menú" aria-expanded="false">
  <span class="lines">
    <span class="line l1"></span>
    <span class="line l2"></span>
    <span class="line l3"></span>
  </span>
</button>
CSS
:root{
  --bg:hsl(214,5%,11%);
  --surface2:hsl(214,4%,5%);
  --border:hsl(214,35%,22%);
  --text:hsl(215,25%,85%);
  --radius-sm:0.375rem;
}

*{
  box-sizing:border-box;
  margin:0;
}

.btn{
  width:52px;
  height:52px;
  background:var(--surface2);
  border:2px solid var(--border);
  border-radius:var(--radius-sm);
  cursor:pointer;
  display:flex;
  align-items:center;
  justify-content:center;
  position:relative;
  overflow:hidden;
}

.btn:hover{
  border-color:hsl(22,100%,62%);
}

.lines{
  position:relative;
  width:22px;
  height:22px;
}

.line{
  position:absolute;
  top:50%;
  left:50%;
  width:22px;
  height:2px;
  background:var(--text);
  border-radius:2px;
  transform-origin:center;
  transition-timing-function:cubic-bezier(.4,0,.2,1);
}

.btn--t4 .line{
  transition:transform .3s,opacity .3s;
  transform-origin:0% 50%;
}

.btn--t4 .l1{
  transform:translate(-50%,calc(-50% - 6px));
}

.btn--t4 .l2{
  transform:translate(-50%,-50%);
}

.btn--t4 .l3{
  transform:translate(-50%,calc(-50% + 6px));
}

.btn--t4.is-open .l1{
  transform:translate(-50%,-50%) rotate(45deg);
}

.btn--t4.is-open .l2{
  opacity:0;
  transform:translate(-50%,-50%) scaleX(0);
}

.btn--t4.is-open .l3{
  transform:translate(-50%,-50%) rotate(-45deg);
}

JS
const btn = document.querySelector('.btn--t4');
btn.addEventListener('click', () => {
  const open = btn.classList.toggle('is-open');
  btn.setAttribute('aria-expanded', String(open));
  btn.setAttribute('aria-label', open ? 'Cerrar menú' : 'Abrir menú');
});

La más expresiva: todo el icono gira medio círculo mientras se transforma.

Un contenedor .wrap rota 180° (0.5s) y, al mismo tiempo, las líneas hacen la cruz. El giro suma energía sin cambiar el estado final.

HTML
<button class="btn btn--t5" type="button" aria-label="Abrir menú" aria-expanded="false">
  <span class="wrap">
    <span class="lines">
      <span class="line l1"></span>
      <span class="line l2"></span>
      <span class="line l3"></span>
    </span>
  </span>
</button>
CSS
:root{
  --bg:hsl(214,5%,11%);
  --surface2:hsl(214,4%,5%);
  --border:hsl(214,35%,22%);
  --text:hsl(215,25%,85%);
  --radius-sm:0.375rem;
}

*{
  box-sizing:border-box;
  margin:0;
}

.btn{
  width:52px;
  height:52px;
  background:var(--surface2);
  border:2px solid var(--border);
  border-radius:var(--radius-sm);
  cursor:pointer;
  display:flex;
  align-items:center;
  justify-content:center;
  position:relative;
  overflow:hidden;
}

.btn:hover{
  border-color:hsl(22,100%,62%);
}

.wrap{
  display:flex;
  align-items:center;
  justify-content:center;
  transition:transform .5s cubic-bezier(.4,0,.2,1);
}

.lines{
  position:relative;
  width:22px;
  height:22px;
}

.line{
  position:absolute;
  top:50%;
  left:50%;
  width:22px;
  height:2px;
  background:var(--text);
  border-radius:2px;
  transform-origin:center;
  transition-timing-function:cubic-bezier(.4,0,.2,1);
}

.btn--t5 .line{
  transition:transform .35s,opacity .2s;
}

.btn--t5 .l1{
  transform:translate(-50%,calc(-50% - 7px));
}

.btn--t5 .l2{
  transform:translate(-50%,-50%);
}

.btn--t5 .l3{
  transform:translate(-50%,calc(-50% + 7px));
}

.btn--t5.is-open .wrap{
  transform:rotate(180deg);
}

.btn--t5.is-open .l1{
  transform:translate(-50%,-50%) rotate(45deg);
}

.btn--t5.is-open .l2{
  opacity:0;
}

.btn--t5.is-open .l3{
  transform:translate(-50%,-50%) rotate(-45deg);
}

JS
const btn = document.querySelector('.btn--t5');
btn.addEventListener('click', () => {
  const open = btn.classList.toggle('is-open');
  btn.setAttribute('aria-expanded', String(open));
  btn.setAttribute('aria-label', open ? 'Cerrar menú' : 'Abrir menú');
});