Rotate + fade
La línea del medio desaparece. Las otras rotan ±45° en su lugar.
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.
La línea del medio desaparece. Las otras rotan ±45° en su lugar.
Rebote elástico: las laterales giran con overshoot y encastran en la cruz.
Los paths se redibujan directamente de ≡ a ✕ con transición de coordenadas.
Cruz con bisagra a la izquierda: las laterales giran desde el extremo, gesto asimétrico.
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.
<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>
: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);
}
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.
<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>
: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);
}
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.
<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>
: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;
}
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.
<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>
: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);
}
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.
<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>
: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);
}
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ú');
});