Retour au catalogue

About Interactive Map

Carte SVG interactive montrant la presence et les bureaux de l'entreprise.

aboutcomplex Both Responsive a11y
corporateboldsaasagencyuniversalcentered
Theme

Créer une carte interactive de bureaux en React

Une carte interactive de bureaux en React positionne des pins en absolu sur un fond SVG quadrillé en utilisant des coordonnées relatives en pourcentage. Chaque pin pulse grâce à une animation Framer Motion en boucle et affiche un tooltip au survol via AnimatePresence. La liste de bureaux en dessous de la carte partage le même état hover pour que les deux panneaux restent synchronisés.

  • Stack : React 18 + Framer Motion 11 + Tailwind v4 + lucide-react, ~117 lignes au total.
  • Les positions des pins sont définies en pourcentage x/y, sans aucune librairie de cartographie.
  • L'état hover est un simple useState string (id du bureau actif) partagé entre la couche SVG et la grille de cartes.
  • Accessible : le focus clavier n'est pas câblé, interaction hover uniquement ; associer à une liste visible pour les lecteurs d'écran.
  • Responsive via aspect-ratio 2/1 ; les coordonnées des pins se mettent à l'échelle automatiquement avec le conteneur.

About Interactive Map est une section de page À propos React qui transforme une liste de bureaux en une carte SVG interactive. Chaque ville reçoit un pulse-dot animé positionné par coordonnées relatives ; au survol, une carte tooltip affiche ville, pays et une courte description. Une liste miroir sous la carte met en surbrillance le même bureau, offrant deux modes d'exploration. Aucune librairie de cartographie requise : tout repose sur un div positionné avec des lignes SVG.

Anatomie

La section comporte trois blocs empilés. En haut, un en-tête centré avec badge optionnel, titre et sous-titre s'anime au scroll. En dessous, deux compteurs de stats (employés, pays) s'affichent côte à côte. Le bloc principal est un conteneur au ratio 2:1 avec une grille SVG 9x9 comme fond décoratif ; chaque pin est un motion.div positionné en absolu dans ce conteneur. Sous la carte, une grille responsive de cartes de villes (2 colonnes mobile, 5 desktop) sert également de zone de survol partageant l'état avec les pins.

Comment ça marche

Chaque pin est un motion.div avec une entrée spring `initial: { scale: 0 }` (stiffness 200, délai échelonné de 0.15s par pin). À l'intérieur de chaque pin, un div frère exécute `animate={{ scale: [1, 2.5, 1], opacity: [0.3, 0, 0.3] }}` avec `repeat: Infinity` sur 2 secondes, cela crée l'effet pulse radar. Le tooltip est monté via AnimatePresence, entrant depuis `y: 5, scale: 0.95` et ressortant vers le même état. L'id du pin actif vit dans un simple `useState<string | null>` mis à jour sur `onMouseEnter` / `onMouseLeave` du pin et des cartes, maintenant les deux panneaux synchronisés sans gestion d'état supplémentaire.

Comment le coder en React

  1. Définir les bureaux en coordonnées relatives

    Donne à chaque bureau un `x` et un `y` entre 0 et 100 représentant sa position dans le conteneur de la carte. Le rendu reste entièrement responsive sans coordonnées en pixels. Un booléen `isHQ` permet d'agrandir le pin du siège et d'y ajouter une icône MapPin de lucide-react.

    interface Location {
      id: string; city: string; country: string;
      description: string; x: number; y: number;
      isHQ?: boolean;
    }
  2. Afficher une grille SVG comme fond décoratif

    Dans le conteneur de la carte, place un SVG positionné en absolu avec `viewBox='0 0 100 60'` et `preserveAspectRatio='none'`. Trace des lignes verticales et horizontales à intervalles réguliers en utilisant `var(--color-border)` pour le trait afin de respecter le thème actif.

    <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>
  3. Animer les pins avec entrée spring et boucle pulse

    Positionne chaque pin avec `style={{ left: `${loc.x}%`, top: `${loc.y}%` }}` et `-translate-x-1/2 -translate-y-1/2`. Utilise `whileInView` pour l'entrée spring et un div interne avec `animate={{ scale: [1, 2.5, 1] }}` et `repeat: Infinity` pour l'anneau pulse. Décale l'entrée de chaque pin avec `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>
  4. Synchroniser la liste de cartes avec l'état hover de la carte

    Affiche une grille de cartes de villes sous la carte. Chaque carte écoute `onMouseEnter` / `onMouseLeave` et écrit dans le même état `activeLocation`. Utilise un style conditionnel sur `borderColor` pour surligner la carte active, aucune librairie supplémentaire, juste un ternaire.

    <div style={{
      borderColor: activeLocation === loc.id
        ? "var(--color-accent)"
        : "var(--color-border)"
    }}>

Quand l'utiliser

Utilise cette section sur les pages À propos, les pages équipe ou les decks investisseurs où montrer une présence physique dans plusieurs villes renforce la crédibilité. Elle fonctionne particulièrement bien pour les SaaS, agences et scale-ups qui veulent communiquer leur portée mondiale sans embarquer une librairie de cartographie lourde. À éviter si tu n'as qu'un ou deux bureaux, une simple ligne d'adresse est plus directe. Sur mobile, les tooltips au survol ne se déclenchent pas ; la liste de villes sous la carte reste affichée et garde l'information accessible.

Utilisé par

  • Stripe, Affiche ses bureaux mondiaux avec une carte interactive sur sa page À propos pour renforcer sa présence dans des dizaines de pays.
  • Intercom, Utilise une section carte avec pins de villes et compteurs d'employés pour communiquer sa culture d'équipe distribuée.
  • Shopify, Présente une visualisation de présence mondiale sur sa page À propos, associant les pins de bureaux aux statistiques d'effectifs.
  • Figma, Met en avant ses bureaux aux États-Unis, en Europe et en Asie avec une carte interactive dans le récit de l'entreprise.

FAQ

Ai-je besoin d'une API cartographique (Google Maps, Mapbox) ?

Non. Le composant utilise une grille SVG ordinaire comme fond décoratif et positionne les pins en pourcentage CSS. Il n'y a pas de projection géographique réelle : les pins se placent manuellement en réglant x/y entre 0 et 100 pour approximer les régions du monde.

Comment utiliser de vraies coordonnées géographiques ?

Convertis latitude et longitude en pourcentages : `x = (lng + 180) / 360 * 100` et `y = (90 - lat) / 180 * 100`. Cela donne une projection équirectangulaire qui correspond au conteneur rectangulaire sans aucune librairie externe.

Les tooltips se coupent en bord de carte. Comment corriger ça ?

Le conteneur de la carte a `overflow: hidden`. Passe-le en `overflow: visible` et ajoute une surcouche transparente pour conserver le border-radius visuellement, ou plafonne la position horizontale du tooltip pour qu'il ne sorte jamais du conteneur.

Comment rendre les pins accessibles au clavier ?

Ajoute `tabIndex={0}` à chaque div de pin et gère `onFocus` / `onBlur` de la même façon que `onMouseEnter` / `onMouseLeave`. Le tooltip apparaît alors au focus, couvrant la navigation clavier et les lecteurs d'écran sans modification d'aria.

"use client";

import React, { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { MapPin } from "lucide-react";

interface Location {
  id: string;
  city: string;
  country: string;
  description: string;
  x: number;
  y: number;
  isHQ?: boolean;
}

interface AboutInteractiveMapProps {
  badge?: string;
  title?: string;
  subtitle?: string;
  locations?: Location[];
  totalEmployees?: number;

Code complet réservé à Pro

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

Passer en Pro, 9,99€/mois

Avis

Carte bureaux interactive React, Framer Motion pins