Dock de navegación con aumento
Dock de enlaces con aumento del elemento activo y sus vecinos; tooltip en hover o foco, y navegación táctil estable.
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import { useState, type CSSProperties, type ReactNode } from "react";
/** Dock de navegación con aumento. React + TypeScript, sin dependencias. Personaliza con variables --gczd-*. */
export type DockItem = { label: string; href: string; icon?: ReactNode };
export type DockInteractivoProps = {
items?: DockItem[];
className?: string;
style?: CSSProperties;
};
const CSS = `
.gczd-root { box-sizing: border-box; min-width: 0; max-width: 100%; color: var(--gczd-fg, #202421); font: 400 15px/1.5 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.gczd-root *, .gczd-root *::before, .gczd-root *::after { box-sizing: border-box; }
.gczd-root button, .gczd-root input, .gczd-root textarea, .gczd-root select { font: inherit; }
.gczd-root button, .gczd-root a { -webkit-tap-highlight-color: transparent; }
.gczd-root :focus-visible { outline: 2px solid var(--gczd-accent, #25724b); outline-offset: 4px; }
.gczd-root ::selection { background: var(--gczd-accent, #25724b); color: #fff; }
.gczd-root button:disabled { opacity: .5; cursor: not-allowed; }
.gczd-root svg { flex-shrink: 0; }
@media (prefers-reduced-motion: reduce) { .gczd-root *, .gczd-root *::before, .gczd-root *::after { animation: none !important; transition-duration: .01ms !important; scroll-behavior: auto !important; } }
.gczd-root { display:flex; flex-wrap:wrap; gap:8px; align-items:flex-end; justify-content:center; padding:12px; border:1px solid #ccd4c9; border-radius:16px; background:var(--gczd-bg,#f2f6ee); }
.gczd-root a { position:relative; width:48px; height:48px; display:grid; place-items:center; border-radius:10px; color:#315138; background:#dfe8d9; text-decoration:none; }
.gczd-icon { display:grid; place-items:center; width:100%; height:100%; background:#dfe8d9; border-radius:10px; transform-origin:center bottom; transition:transform .3s cubic-bezier(.16,1,.3,1),background .2s; }
.gczd-root a[data-distance="active"] .gczd-icon { transform:translateY(-8px) scale(1.2); background:#c6d6bb; }
.gczd-root a[data-distance="near"] .gczd-icon { transform:translateY(-3px) scale(1.05); }
.gczd-tooltip { position:absolute; bottom:calc(100% + 23px); left:50%; transform:translateX(-50%) translateY(4px); background:#2f472e; color:#fff; padding:5px 9px; border-radius:4px; font-size:11px; opacity:0; pointer-events:none; white-space:nowrap; transition:opacity .15s,transform .15s; }
.gczd-root a[data-distance="active"] .gczd-tooltip { opacity:1; transform:translateX(-50%) translateY(0); }
@media(prefers-reduced-motion:reduce) { .gczd-icon { transform:none!important; } }`;
export function DockInteractivo({
items = [
{ label: "Inicio", href: "#inicio" },
{ label: "Proyectos", href: "#proyectos" },
{ label: "Archivos", href: "#archivos" },
{ label: "Mensajes", href: "#mensajes" },
{ label: "Ajustes", href: "#ajustes" },
],
className = "",
style,
}: DockInteractivoProps) {
const [active, setActive] = useState<number | null>(null);
const paths = [
"M3 10 12 3l9 7v10H3ZM9 20v-7h6v7",
"M3 5h7l2 3h9v12H3Z",
"M6 3h8l4 4v14H6ZM14 3v5h4M9 13h6M9 17h6",
"M3 4h18v13H8l-5 4ZM7 8h10M7 12h6",
"M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8M12 2v3M12 19v3M2 12h3M19 12h3M5 5l2 2M17 17l2 2M19 5l-2 2M7 17l-2 2",
];
return (
<nav
className={`gczd-root ${className}`}
style={style}
aria-label="Navegación rápida"
onPointerLeave={() => setActive(null)}
>
<style>{CSS}</style>
{items.map((item, i) => (
<a
key={item.href}
href={item.href}
aria-label={item.label}
title={item.label}
data-distance={
active === null
? "far"
: Math.abs(active - i) > 1
? "far"
: active === i
? "active"
: "near"
}
onPointerEnter={(e) => {
if (e.pointerType === "mouse") setActive(i);
}}
onFocus={() => setActive(i)}
onBlur={() => setActive(null)}
>
<span className="gczd-tooltip" aria-hidden="true">
{item.label}
</span>
<span className="gczd-icon">
{item.icon ?? (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d={paths[i % paths.length]} />
</svg>
)}
</span>
</a>
))}
</nav>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/dock-interactivoEjemplo de uso de Dock de navegación con aumento
Después de copiar dock-interactivo.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { DockInteractivo } from "./dock-interactivo";
export default function Ejemplo() {
return <DockInteractivo />;
}Propiedades de DockInteractivo
DockInteractivo acepta 3 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
items? | DockItem[] | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Personaliza Dock de navegación con aumento 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 |
|---|---|
--gczd-fg | #202421 |
--gczd-accent | #25724b |
--gczd-bg | #f2f6ee |
Accesibilidad y rendimiento
El archivo tiene 105 líneas (5 KB) y no necesita @keyframes. Esto es lo que hace Dock de navegación con aumento 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".