Cómo crear un mapa interactivo de ubicaciones de oficinas en React
Un mapa de oficinas interactivo en React coloca pines posicionados de forma absoluta sobre un fondo SVG cuadriculado usando coordenadas en porcentaje. Cada pin late con una animación en bucle de Framer Motion y muestra un tooltip al pasar el cursor mediante AnimatePresence. La lista de ubicaciones debajo del mapa refleja el mismo estado hover para que ambos paneles permanezcan sincronizados.
- Stack: React 18 + Framer Motion 11 + Tailwind v4 + lucide-react, ~117 líneas en total.
- Las posiciones de los pines se definen como valores x/y en porcentaje, sin necesidad de ninguna librería de mapas.
- El estado hover es un único useState string (id de la ubicación activa) compartido entre la capa SVG y la cuadrícula de tarjetas de abajo.
- Accesibilidad: el foco de teclado no está conectado, interacción solo por hover; combínalo con una lista visible para usuarios de lectores de pantalla.
- Responsive mediante aspect-ratio 2/1; las coordenadas de los pines se escalan automáticamente con el contenedor.
About Interactive Map es una sección de página About en React que convierte una lista de ubicaciones de oficinas en un mapa SVG con interacción al pasar el cursor. Cada ciudad recibe un pulse-dot animado colocado mediante coordenadas en porcentaje; al pasar el cursor, una tarjeta tooltip muestra la ciudad, el país y una breve descripción. Una lista espejo bajo el mapa resalta la misma ubicación, ofreciendo dos formas de explorar. No se necesita ninguna librería de mapas: todo se basa en un div posicionado con líneas SVG.
Anatomía
La sección tiene tres bloques apilados. En la parte superior, una cabecera centrada con un badge opcional, título y subtítulo aparece con un fade al hacer scroll. Debajo, dos contadores de estadísticas (empleados, países) se muestran uno al lado del otro. El bloque principal es un contenedor con aspect-ratio 2:1 con una grilla SVG de 9x9 líneas como fondo decorativo; cada pin de ubicación es un motion.div posicionado de forma absoluta dentro de este contenedor. Bajo el mapa, una cuadrícula responsive de tarjetas de ciudades (2 columnas en móvil, 5 en escritorio) funciona también como zona de hover que comparte estado con los pines de arriba.
Cómo funciona
Cada pin es un motion.div con una entrada spring `initial: { scale: 0 }` (stiffness 200, damping sin especificar, delay escalonado de 0.15s por pin). Dentro de cada pin, un div hermano ejecuta en bucle `animate={{ scale: [1, 2.5, 1], opacity: [0.3, 0, 0.3] }}` con `repeat: Infinity` y una duración de 2 segundos, esto crea el efecto de pulso radar. El tooltip se monta con AnimatePresence, entrando desde `y: 5, scale: 0.95` y saliendo de vuelta al mismo estado. El id del pin activo vive en un único `useState<string | null>` y se actualiza en `onMouseEnter` / `onMouseLeave` tanto del div del pin como del elemento de la cuadrícula de tarjetas, manteniendo ambos paneles sincronizados sin ninguna gestión de estado adicional.
Cómo crearlo en React
Definir las ubicaciones como coordenadas en porcentaje
Dale a cada ubicación un `x` y una `y` entre 0 y 100 que representen su posición dentro del contenedor del mapa. Esto mantiene el mapa totalmente responsive, sin coordenadas fijas en píxeles. Un booleano `isHQ` te permite agrandar el pin de la sede central y añadir un icono MapPin de lucide-react.
interface Location { id: string; city: string; country: string; description: string; x: number; y: number; isHQ?: boolean; }Renderizar una grilla SVG como fondo decorativo
Dentro del contenedor del mapa, coloca un SVG posicionado de forma absoluta con `viewBox='0 0 100 60'` y `preserveAspectRatio='none'`. Dibuja líneas verticales y horizontales a intervalos regulares usando `var(--color-border)` para el trazo, de modo que respete el preset de tema activo.
<svg className="absolute inset-0 w-full h-full" viewBox="0 0 100 60" preserveAspectRatio="none"> {Array.from({ length: 10 }, (_, i) => ( <React.Fragment key={i}> <line x1={i*10+10} y1="0" x2={i*10+10} y2="60" stroke="var(--color-border)" strokeWidth="0.15" /> <line x1="0" y1={i*6+6} x2="100" y2={i*6+6} stroke="var(--color-border)" strokeWidth="0.15" /> </React.Fragment> ))} </svg>Animar los pines con entrada spring y bucle de pulso
Posiciona cada pin con `style={{ left: `${loc.x}%`, top: `${loc.y}%` }}` y `-translate-x-1/2 -translate-y-1/2`. Usa `whileInView` para la entrada spring y un div interno aparte con `animate={{ scale: [1, 2.5, 1] }}` y `repeat: Infinity` para el anillo de pulso. Escalona la entrada de cada pin con `delay: 0.3 + i * 0.15`.
<motion.div initial={{ opacity: 0, scale: 0 }} whileInView={{ opacity: 1, scale: 1 }} transition={{ delay: 0.3 + i * 0.15, type: "spring", stiffness: 200 }} onMouseEnter={() => setActiveLocation(loc.id)} onMouseLeave={() => setActiveLocation(null)} > <motion.div className="absolute inset-0 rounded-full" animate={{ scale: [1, 2.5, 1], opacity: [0.3, 0, 0.3] }} transition={{ duration: 2, repeat: Infinity }} /> </motion.div>Sincronizar la lista de tarjetas con el estado hover del mapa
Renderiza una cuadrícula de tarjetas de ciudades debajo del mapa. Cada tarjeta escucha `onMouseEnter` / `onMouseLeave` y escribe en el mismo estado `activeLocation`. Usa un estilo condicional sobre `borderColor` para resaltar la tarjeta activa, sin librerías adicionales, solo un ternario.
<div style={{ borderColor: activeLocation === loc.id ? "var(--color-accent)" : "var(--color-border)" }}>
Cuándo usarlo
Usa esta sección en páginas About de empresa, páginas de equipo o decks para inversores donde mostrar una presencia física en varias ciudades añade credibilidad. Funciona especialmente bien para empresas SaaS, agencias y scale-ups que quieren comunicar su alcance global sin incrustar un mapa pesado. Omítela si solo tienes una o dos ubicaciones, ahí una simple línea de dirección tiene más peso. En dispositivos táctiles los tooltips de hover no se activan; siempre se muestra debajo una lista estática de ubicaciones, de modo que la información esencial permanece accesible.
Usado por
- Stripe, Muestra sus oficinas globales con un mapa mundial interactivo en su página About para reforzar su alcance en docenas de países.
- Intercom, Usa una sección de mapa con pines de ciudades y contadores de empleados para comunicar su cultura de equipo distribuido.
- Shopify, Presenta una visualización de presencia global en su página About, combinando los pines de ubicación con estadísticas de tamaño de equipo.
- Figma, Destaca sus oficinas en Estados Unidos, Europa y Asia con un mapa interactivo como parte de la historia de la empresa.
FAQ
¿Necesito una API de mapas (Google Maps, Mapbox) para esto?
No. El componente usa una simple grilla SVG como fondo decorativo y coloca los pines con porcentajes CSS. No hay ninguna proyección geográfica real, los pines se colocan manualmente ajustando valores x/y entre 0 y 100 para aproximar las regiones del mundo.
¿Cómo añado coordenadas geográficas reales?
Convierte latitud y longitud en valores de porcentaje: `x = (lng + 180) / 360 * 100` e `y = (90 - lat) / 180 * 100`. Esto da una proyección equirrectangular que encaja con el contenedor rectangular sin ninguna librería externa.
Los tooltips se recortan en los bordes. ¿Cómo lo soluciono?
El contenedor del mapa tiene `overflow: hidden`. Cámbialo a `overflow: visible` y añade una capa transparente para conservar visualmente el border-radius, o limita la posición horizontal del tooltip para que nunca salga de los límites del contenedor.
¿Cómo hago que los pines sean accesibles por teclado?
Añade `tabIndex={0}` a cada div de pin y maneja `onFocus` / `onBlur` de la misma forma que `onMouseEnter` / `onMouseLeave`. El tooltip aparecerá entonces al recibir el foco, lo que cubre la navegación por teclado y por lector de pantalla sin ningún cambio de aria.