Retour au catalogue

Sidebar Floating Dock

Dock flottant style macOS avec effet de grossissement au survol et animations fluides.

sidebarmedium Both Responsive a11y
boldelegantsaassaasportfolioasymmetric
Theme

Cómo crear una barra de navegación tipo dock con ampliación en React

Un dock estilo macOS en React sigue la posición del ratón con useMotionValue de Framer Motion, calcula la distancia desde el centro de cada icono y pasa esa distancia a un useTransform respaldado por useSpring para escalar el tamaño del icono en tiempo real. Los parámetros del spring (mass, stiffness, damping) controlan cuán elástica se siente la ampliación.

  • Stack: React + Framer Motion 11 + Lucide React + Tailwind v4, ~225 líneas, cero dependencias adicionales.
  • API principales de Framer Motion: useMotionValue, useSpring, useTransform, AnimatePresence.
  • Admite orientación horizontal (dock inferior) y vertical (dock lateral) mediante una prop.
  • El item activo usa layoutId para un indicador de punto en shared-layout que se desliza entre los items.
  • Los atributos aria de accesibilidad aún no están conectados; los tooltips se degradan correctamente sin JS.

Este dock flotante lleva la bandeja de aplicaciones de macOS al navegador. Cada icono detecta la proximidad del cursor y crece suavemente a medida que el puntero se acerca, y se encoge cuando se aleja. La barra completa aparece con un slide-up en spring, las etiquetas de tooltip surgen al pasar el ratón y un punto en shared-layout se desliza entre el item activo. Funciona como barra de navegación inferior, barra de herramientas lateral o lanzador de paleta de comandos.

Anatomía

El wrapper raíz captura onMouseMove y escribe clientX/clientY en dos motion values de nivel superior. El contenedor del dock es un flex en fila (o columna) con forma de píldora redondeada y fondo glass-morphism. Cada slot de icono es un componente DockIcon que contiene un botón, un badge de notificación, un punto activo y un tooltip. Un separador visual y un botón Plus fijo cierran la lista, separados de los items dinámicos por una regla de 1px.

Cómo funciona

Dentro de cada DockIcon, useTransform lee la motion value compartida mouseX (o mouseY en vertical) y calcula la distancia desde el centro del icono mediante getBoundingClientRect. Esa distancia bruta alimenta un useTransform que mapea distancia a tamaño ([0, 100, 200] → [56, 48, 44]px). Un useSpring envuelve la salida con mass 0.1, stiffness 200 y damping 15, dando a la ampliación una sensación ágil pero no brusca. El resultado se aplica al width y al height del motion.button, para que el icono crezca desde su centro sin desplazar el layout.

Cómo crearlo en React

  1. Crea las motion values del ratón a nivel del dock

    Al inicio del componente dock, llama a useMotionValue(0) tanto para X como para Y. Adjunta un handler onMouseMove al div raíz que llame a mouseX.set(e.clientX) y mouseY.set(e.clientY). Pasa ambos valores a cada icono como props.

    const mouseX = useMotionValue(0);
    const mouseY = useMotionValue(0);
    
    <div onMouseMove={(e) => { mouseX.set(e.clientX); mouseY.set(e.clientY); }}>
  2. Calcula la distancia entre el puntero y cada icono

    Dentro de DockIcon, adjunta un ref al botón. Usa useTransform sobre mouseX (o mouseY) con un callback que lea getBoundingClientRect() para obtener el centro del icono y luego devuelva Math.abs(posiciónPuntero - centro). Esto produce un valor de distancia en vivo que se actualiza en cada frame que el puntero se mueve.

    const distance = useTransform(mouseX, (val) => {
      const bounds = ref.current?.getBoundingClientRect();
      if (!bounds) return 200;
      return Math.abs(val - (bounds.x + bounds.width / 2));
    });
  3. Mapea la distancia al tamaño con un spring

    Usa useTransform sobre distance con un rango de entrada [0, 100, 200] y un rango de salida [56, 48, 44]. Envuélvelo en useSpring con una mass baja y una stiffness moderada para que el icono se estabilice rápidamente sin rebotar. Aplica la motion value resultante al width y al height de tu motion.button.

    const size = useSpring(
      useTransform(distance, [0, 100, 200], [56, 48, 44]),
      { mass: 0.1, stiffness: 200, damping: 15 }
    );
    
    <motion.button style={{ width: size, height: size }} />
  4. Añade el tooltip y el indicador de activo

    Envuelve el tooltip en AnimatePresence y renderízalo solo cuando isHovered sea true. Usa initial/animate/exit en una motion.div para un fade-scale rápido. Para el indicador de activo, da a todas las instancias el mismo layoutId ('dock-active') para que Framer Motion anime automáticamente el deslizamiento del punto de un item al siguiente.

    <AnimatePresence>
      {isHovered && (
        <motion.div
          initial={{ opacity: 0, y: 4, scale: 0.9 }}
          animate={{ opacity: 1, y: 0, scale: 1 }}
          exit={{ opacity: 0, y: 4, scale: 0.9 }}
        >
          {item.label}
        </motion.div>
      )}
    </AnimatePresence>
    
    {item.active && (
      <motion.div layoutId="dock-active" className="w-1 h-1 rounded-full" />
    )}

Cuándo usarlo

Recurre a este dock en shells de aplicación donde quieras una navegación compacta y siempre visible que no ocupe una columna de sidebar entera. Encaja en dashboards SaaS, herramientas creativas, sitios de portfolio y cualquier contexto con seis a diez destinos principales que necesiten acceso rápido. Evítalo en páginas cargadas de contenido donde una sidebar persistente con etiquetas mejora la legibilidad, y desactiva el efecto de ampliación en dispositivos táctiles ya que la detección de proximidad requiere un puntero real.

Usado por

  • Apple macOS, El dock ampliador original que inspiró este patrón, usado como lanzador principal de aplicaciones en cada Mac.
  • Notion, Usa una barra de acciones flotante compacta en sus vistas de canvas que retoma la metáfora del dock.
  • Framer, El editor de canvas de Framer muestra una barra de herramientas flotante con herramientas basadas en iconos que se escalan y resaltan al pasar el ratón.
  • Linear, La barra de comandos y la paleta de acciones rápidas usan una navegación compacta centrada en iconos con indicadores de selección animados.

FAQ

¿Por qué la ampliación usa getBoundingClientRect en lugar de una prop de posición?

getBoundingClientRect da la posición real renderizada del icono en coordenadas del viewport, lo que coincide con los valores clientX/clientY del evento del ratón. Pasar una prop de posición precalculada se rompería si el dock cambia de layout, se hace scroll o se reposiciona dinámicamente.

¿Cómo fijo el dock en la parte inferior de la pantalla?

Envuelve el dock en un contenedor fijo: `position: fixed; bottom: 1.5rem; left: 50%; transform: translateX(-50%)`. Mueve el listener onMouseMove a window en lugar del div del dock para que la detección de proximidad funcione incluso cuando el cursor está por encima del dock.

¿Puedo usar el dock en vertical?

Sí. Pasa orientation='vertical' para cambiar la dirección flex a columna y modificar el cálculo de proximidad de mouseX a mouseY. El separador también gira de una regla vertical a una horizontal automáticamente mediante el className condicional.

¿El punto activo en shared-layout se anima entre los items?

Sí, siempre que solo un item tenga active: true a la vez. El layoutId='dock-active' de Framer Motion sigue la única instancia montada y la reposiciona con suavidad cuando active cambia a otro item, sin necesidad de código de animación manual.

"use client";

import { useState, useRef } from "react";
import {
  motion,
  useMotionValue,
  useSpring,
  useTransform,
  AnimatePresence,
  type MotionValue,
} from "framer-motion";
import {
  Home,
  Search,
  FolderOpen,
  MessageSquare,
  Calendar,
  Settings,
  User,
  Plus,
} from "lucide-react";

Code complet réservé à Pro

Code source intégral, export multi-framework et playground.

Passer en Pro, 9,99€/mois

Reseñas

Dock estilo macOS en React con ampliación mediante Framer