Botones de menú que se vuelven cruz
Tres animaciones distintas de hamburguesa a cruz (giro, colapso y deslizamiento) con aria-expanded.
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import { useState, type CSSProperties } from "react";
/** Botones de menú que se transforman en cruz: tres animaciones distintas (giro, colapso y rotación de líneas) con aria-expanded. React + TypeScript, sin dependencias. */
export type MenuHamburguesaVariante = "giro" | "colapso" | "doble";
export type MenuHamburguesaProps = {
variant?: MenuHamburguesaVariante;
open?: boolean;
defaultOpen?: boolean;
onChange?: (open: boolean) => void;
label?: string;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcmhb-root { box-sizing: border-box; display: inline-block; color: var(--gcmhb-fg, #111); }
.gcmhb-root *, .gcmhb-root *::before, .gcmhb-root *::after { box-sizing: border-box; }
.gcmhb-root :focus-visible { outline: 3px solid #2563eb; outline-offset: 3px; border-radius: 14px; }
@media (prefers-reduced-motion: reduce) { .gcmhb-root * { transition-duration: .01ms !important; } }
.gcmhb-btn { position: relative; display: block; width: 56px; height: 56px; padding: 0; border: 2px solid currentColor; border-radius: 16px; background: transparent; color: inherit; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: background .25s, color .25s; }
.gcmhb-btn:hover { background: rgba(127, 127, 127, .12); }
.gcmhb-bar { position: absolute; left: 14px; right: 14px; height: 2.5px; border-radius: 2px; background: currentColor; transition: transform .45s cubic-bezier(.65, 0, .35, 1), opacity .25s, width .3s, left .3s; }
.gcmhb-bar:nth-child(1) { top: 17px; }
.gcmhb-bar:nth-child(2) { top: 26px; }
.gcmhb-bar:nth-child(3) { top: 35px; }
/* giro */
.gcmhb-btn[data-v="giro"][aria-expanded="true"] { transform: rotate(180deg); transition: transform .5s cubic-bezier(.65, 0, .35, 1), background .25s; }
.gcmhb-btn[data-v="giro"][aria-expanded="true"] .gcmhb-bar:nth-child(1) { transform: translateY(9px) rotate(45deg); }
.gcmhb-btn[data-v="giro"][aria-expanded="true"] .gcmhb-bar:nth-child(2) { opacity: 0; }
.gcmhb-btn[data-v="giro"][aria-expanded="true"] .gcmhb-bar:nth-child(3) { transform: translateY(-9px) rotate(-45deg); }
.gcmhb-btn[data-v="giro"] { transition: transform .5s cubic-bezier(.65, 0, .35, 1), background .25s; }
/* colapso */
.gcmhb-btn[data-v="colapso"][aria-expanded="true"] .gcmhb-bar:nth-child(1) { transform: translateY(9px); }
.gcmhb-btn[data-v="colapso"][aria-expanded="true"] .gcmhb-bar:nth-child(3) { transform: translateY(-9px); }
.gcmhb-btn[data-v="colapso"][aria-expanded="true"] .gcmhb-bar:nth-child(2) { transform: scaleX(0); }
.gcmhb-btn[data-v="colapso"][aria-expanded="true"] .gcmhb-bar:nth-child(1), .gcmhb-btn[data-v="colapso"][aria-expanded="true"] .gcmhb-bar:nth-child(3) { transition-delay: 0s; }
.gcmhb-btn[data-v="colapso"] .gcmhb-cross { position: absolute; inset: 0; opacity: 0; transition: opacity .2s .25s; }
.gcmhb-btn[data-v="colapso"][aria-expanded="true"] .gcmhb-cross { opacity: 1; }
.gcmhb-cross::before, .gcmhb-cross::after { content: ""; position: absolute; left: 14px; right: 14px; top: 50%; height: 2.5px; margin-top: -1.25px; border-radius: 2px; background: currentColor; }
.gcmhb-cross::before { transform: rotate(45deg); }
.gcmhb-cross::after { transform: rotate(-45deg); }
.gcmhb-btn[data-v="colapso"][aria-expanded="true"] .gcmhb-bar { opacity: 0; transition-delay: .2s; }
/* doble: el menú se vuelve una X sin perder la línea central, que se desliza */
.gcmhb-btn[data-v="doble"][aria-expanded="true"] .gcmhb-bar:nth-child(1) { transform: translateY(9px) rotate(135deg); width: auto; }
.gcmhb-btn[data-v="doble"][aria-expanded="true"] .gcmhb-bar:nth-child(3) { transform: translateY(-9px) rotate(-135deg); }
.gcmhb-btn[data-v="doble"][aria-expanded="true"] .gcmhb-bar:nth-child(2) { transform: translateX(30px); opacity: 0; }
.gcmhb-btn[data-v="doble"] .gcmhb-bar:nth-child(2) { left: 14px; right: 24px; }
.gcmhb-btn[data-v="doble"]:hover .gcmhb-bar:nth-child(2):not([aria-hidden]) { right: 14px; }`;
export function MenuHamburguesa({ variant = "giro", open, defaultOpen = false, onChange, label = "Menú", className = "", style }: MenuHamburguesaProps) {
const [inner, setInner] = useState(defaultOpen);
const on = open ?? inner;
return (
<span className={`gcmhb-root ${className}`} style={style}>
<style>{CSS}</style>
<button className="gcmhb-btn" type="button" data-v={variant} aria-expanded={on} aria-label={on ? "Cerrar menú" : `Abrir ${label.toLowerCase()}`} onClick={() => { setInner(!on); onChange?.(!on); }}>
<span className="gcmhb-bar" />
<span className="gcmhb-bar" />
<span className="gcmhb-bar" />
{variant === "colapso" ? <span className="gcmhb-cross" aria-hidden="true" /> : null}
</button>
</span>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/menu-hamburguesaEjemplo de uso de Botones de menú que se vuelven cruz
Después de copiar menu-hamburguesa.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { MenuHamburguesa } from "./menu-hamburguesa";
export default function Ejemplo() {
return <MenuHamburguesa variant="giro" label="Ejemplo" open />;
}Propiedades de MenuHamburguesa
MenuHamburguesa acepta 7 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
variant? | MenuHamburguesaVariante | Opcional. |
open? | boolean | Opcional. |
defaultOpen? | boolean | Opcional. |
onChange? | (open: boolean) => void | Opcional. |
label? | string | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Variantes disponibles
variant:giro,colapso,doble
Personaliza Botones de menú que se vuelven cruz con variables CSS
Define estas variables en el contenedor o en tu hoja de estilos para cambiar los colores sin tocar el componente. Si no las defines, se usa el valor por defecto.
| Variable | Valor por defecto |
|---|---|
--gcmhb-fg | #111 |
Accesibilidad y rendimiento
El archivo tiene 68 líneas (5 KB) y no necesita @keyframes. Esto es lo que hace Botones de menú que se vuelven cruz por quien lo usa:
- Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
- Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
- Expone su estado a tecnologías de apoyo con atributos aria-*.
- Es un componente de cliente de Next.js: incluye la directiva "use client".