Ir al contenido
Evaluar mi proyecto
Componentes · MicrointeraccionesDisponible

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.

menu-hamburguesa.tsx68 líneas
Descargar
"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.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/menu-hamburguesa

Ejemplo 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.

ejemplo.tsx5 líneas
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.

PropiedadTipoDescripción
variant?MenuHamburguesaVarianteOpcional.
open?booleanOpcional.
defaultOpen?booleanOpcional.
onChange?(open: boolean) => voidOpcional.
label?stringOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

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.

VariableValor 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".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)