Barra con megamenú
Panel de tres columnas con descripciones, abierto por clic o flecha abajo; Escape y clic fuera lo cierran.
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import { useEffect, useId, useRef, useState, type CSSProperties } from "react";
/** Barra con megamenú: un panel de tres columnas con descripciones se abre con clic, hover o flecha abajo; Escape y clic fuera lo cierran. Estilo minimalista claro. React + TypeScript, sin dependencias. */
export type NavMegamenuColumna = { title: string; items: { label: string; desc: string }[] };
export type NavMegamenuProps = {
brand?: string;
triggerLabel?: string;
columns?: NavMegamenuColumna[];
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcnmm-root { box-sizing: border-box; position: relative; width: 100%; max-width: 860px; color: #18181b; font: 500 14px/1.4 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
.gcnmm-root *, .gcnmm-root *::before, .gcnmm-root *::after { box-sizing: border-box; }
.gcnmm-root button, .gcnmm-root a { -webkit-tap-highlight-color: transparent; }
.gcnmm-root :focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .gcnmm-root * { animation: none !important; transition-duration: .01ms !important; } }
.gcnmm-bar { display: flex; align-items: center; gap: 6px; height: 60px; padding: 0 12px 0 20px; border: 1px solid #e4e4e7; border-radius: 16px; background: #fff; box-shadow: 0 1px 2px rgba(0, 0, 0, .04); }
.gcnmm-brand { margin-right: auto; font-weight: 800; font-size: 17px; letter-spacing: -.02em; }
.gcnmm-link, .gcnmm-trig { display: inline-flex; align-items: center; gap: 6px; height: 40px; padding: 0 14px; border: 0; border-radius: 10px; background: none; color: #3f3f46; text-decoration: none; cursor: pointer; font: inherit; transition: background .15s, color .15s; }
.gcnmm-link:hover, .gcnmm-trig:hover, .gcnmm-trig[aria-expanded="true"] { background: #f4f4f5; color: #18181b; }
.gcnmm-trig svg { transition: transform .25s; }
.gcnmm-trig[aria-expanded="true"] svg { transform: rotate(180deg); }
.gcnmm-cta { display: inline-flex; align-items: center; height: 40px; padding: 0 16px; border-radius: 10px; background: #18181b; color: #fff; text-decoration: none; font-weight: 600; }
.gcnmm-cta:hover { background: #3f3f46; }
.gcnmm-panel { position: absolute; z-index: 5; left: 0; right: 0; top: calc(100% + 10px); display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px 24px; padding: 20px; border: 1px solid #e4e4e7; border-radius: 18px; background: #fff; box-shadow: 0 24px 48px -16px rgba(0, 0, 0, .25); transform-origin: top center; animation: gcnmm-in .25s cubic-bezier(.16, 1, .3, 1); }
.gcnmm-col h3 { margin: 0 0 8px 10px; font-size: 12px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: #a1a1aa; }
.gcnmm-item { display: block; padding: 10px; border-radius: 10px; color: inherit; text-decoration: none; transition: background .15s; }
.gcnmm-item:hover { background: #f4f4f5; }
.gcnmm-item b { display: block; font-weight: 600; }
.gcnmm-item span { color: #71717a; font-size: 13px; }
@keyframes gcnmm-in { from { opacity: 0; transform: translateY(-6px) scale(.98); } }
@media (max-width: 680px) { .gcnmm-panel { grid-template-columns: 1fr; } .gcnmm-link { display: none; } }`;
const DEFAULT: NavMegamenuColumna[] = [
{ title: "Construir", items: [{ label: "Software a medida", desc: "Sistemas para tu operación." }, { label: "Aplicaciones web", desc: "Paneles y portales." }, { label: "Aplicaciones móviles", desc: "Para campo y clientes." }] },
{ title: "Conectar", items: [{ label: "Integraciones", desc: "Une tus sistemas actuales." }, { label: "Automatización", desc: "Quita tareas repetitivas." }, { label: "Datos y reportes", desc: "Indicadores a la vista." }] },
{ title: "Proteger", items: [{ label: "Ciberseguridad", desc: "Revisión y buenas prácticas." }, { label: "Nube", desc: "Migración ordenada." }, { label: "Soporte", desc: "Acompañamiento continuo." }] },
];
export function NavMegamenu({ brand = "Taller", triggerLabel = "Servicios", columns = DEFAULT, className = "", style }: NavMegamenuProps) {
const id = useId();
const [open, setOpen] = useState(false);
const root = useRef<HTMLDivElement>(null);
const trig = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (!open) return;
const away = (event: PointerEvent) => { if (!root.current?.contains(event.target as Node)) setOpen(false); };
const esc = (event: KeyboardEvent) => { if (event.key === "Escape") { setOpen(false); trig.current?.focus(); } };
document.addEventListener("pointerdown", away);
document.addEventListener("keydown", esc);
return () => { document.removeEventListener("pointerdown", away); document.removeEventListener("keydown", esc); };
}, [open]);
return (
<div ref={root} className={`gcnmm-root ${className}`} style={style}>
<style>{CSS}</style>
<nav className="gcnmm-bar" aria-label="Principal">
<span className="gcnmm-brand">{brand}</span>
<button ref={trig} className="gcnmm-trig" type="button" aria-expanded={open} aria-controls={`${id}-p`} onClick={() => setOpen((value) => !value)} onKeyDown={(event) => { if (event.key === "ArrowDown") { event.preventDefault(); setOpen(true); } }}>
{triggerLabel}
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
</button>
<a className="gcnmm-link" href="#" onClick={(event) => event.preventDefault()}>Proyectos</a>
<a className="gcnmm-link" href="#" onClick={(event) => event.preventDefault()}>Nosotros</a>
<a className="gcnmm-cta" href="#" onClick={(event) => event.preventDefault()}>Contacto</a>
</nav>
{open ? (
<div id={`${id}-p`} className="gcnmm-panel">
{columns.map((column) => (
<div key={column.title} className="gcnmm-col">
<h3>{column.title}</h3>
{column.items.map((item) => (
<a key={item.label} className="gcnmm-item" href="#" onClick={(event) => { event.preventDefault(); setOpen(false); }}>
<b>{item.label}</b>
<span>{item.desc}</span>
</a>
))}
</div>
))}
</div>
) : null}
</div>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/nav-megamenuEjemplo de uso de Barra con megamenú
Después de copiar nav-megamenu.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { NavMegamenu } from "./nav-megamenu";
export default function Ejemplo() {
return <NavMegamenu brand="Ejemplo" />;
}Propiedades de NavMegamenu
NavMegamenu acepta 5 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
brand? | string | Opcional. |
triggerLabel? | string | Opcional. |
columns? | NavMegamenuColumna[] | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Accesibilidad y rendimiento
El archivo tiene 91 líneas (6 KB) y define 1 animación con @keyframes. Esto es lo que hace Barra con megamenú 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-*.
- Responde al teclado, no solo al ratón.
- Es un componente de cliente de Next.js: incluye la directiva "use client".