Créer une navbar dock style macOS en React avec Framer Motion
Une navbar dock style macOS en React utilise useMotionValue de Framer Motion pour suivre la position X du curseur et useTransform pour convertir la distance entre le curseur et chaque icône en taille, puis useSpring pour lisser le résultat. Les icônes les plus proches du pointeur grandissent le plus, créant l'effet de vague caractéristique.
- Stack : React 19, Framer Motion 11, Lucide React, tokens CSS Tailwind v4, ~137 lignes.
- API clé : useMotionValue, useTransform, useSpring avec stiffness 300, damping 25, mass 0.5.
- Les icônes passent de 44px (taille de base) à 68px (taille max) dans un rayon d'influence de 140px.
- Accessible : chaque icône a un aria-label et un tooltip CSS ; le hover bascule le fond sur --color-accent.
- Limitation tactile : le grossissement dépend de la position du pointeur, il revient silencieusement aux icônes statiques sur mobile.
La Navbar Dock est une barre de navigation fixe en bas d'écran qui imite le Dock de macOS. Chaque icône grossit à l'approche du curseur et les voisines suivent avec une intensité décroissante, produisant un effet de vague fluide. Elle remplace totalement le pattern de navbar en haut et fonctionne particulièrement bien sur les sites portfolio, one-page ou orientés outil où la navigation est secondaire au contenu.
Anatomie
Le composant est un `<nav>` fixe centré horizontalement en bas du viewport avec un fond verre dépoli (backdrop-filter blur + transparence color-mix). Il contient une rangée de composants `DockIcon`, chacun rendant un `motion.a` dont la taille est pilotée par une spring motion value. Un `<span>` de tooltip flotte au-dessus de chaque icône et apparaît au :hover via une règle CSS injectée inline. Le conteneur nav suit le X du pointeur via onMouseMove et le réinitialise à -999 au mouseleave, ce qui ramène toutes les icônes à leur taille de base.
Comment ça marche
Un unique `useMotionValue(-999)` vit dans le composant parent `NavbarDock` et est transmis à chaque `DockIcon`. Dans chaque icône, `useTransform` lit la valeur mouseX partagée et calcule la distance en pixels entre le curseur et le centre de cette icône en appelant `getBoundingClientRect()` dans le callback de transformation. Cette distance est ensuite mappée linéairement de 0 à 140px sur la plage de taille 68px à 44px, accordant aux icônes proches une valeur plus grande. La taille brute est passée dans `useSpring` (stiffness 300, damping 25, mass 0.5) pour un redimensionnement fluide et sans saccade. Quand mouseX vaut -999 (curseur hors du dock), chaque calcul de distance retourne DISTANCE (140) et toutes les icônes reviennent à BASE_SIZE.
Comment le coder en React
Crée la motion value partagée pour la souris
Dans le composant parent, déclare un unique `useMotionValue(-999)` et connecte-le à `onMouseMove` et `onMouseLeave` sur l'élément nav. La valeur sentinelle -999 signale que le curseur est en dehors du dock.
const mouseX = useMotionValue(-999); <nav onMouseMove={(e) => mouseX.set(e.clientX)} onMouseLeave={() => mouseX.set(-999)} >Calcule la distance entre le curseur et le centre de l'icône
Dans chaque `DockIcon`, attache une ref à l'élément anchor. Passe le mouseX partagé dans `useTransform` et lis le bounding rect de l'icône dans le callback pour obtenir la distance curseur-centre en temps réel.
const distance = useTransform(mouseX, (val: number) => { const el = ref.current; if (!el || val === -999) return DISTANCE; // collapse const rect = el.getBoundingClientRect(); const center = rect.left + rect.width / 2; return Math.abs(val - center); });Convertis la distance en taille via un spring
Utilise un second `useTransform` pour convertir la plage [0, DISTANCE] en [MAX_SIZE, BASE_SIZE]. Enveloppe le résultat dans `useSpring` pour que le redimensionnement soit lissé et physiquement pondéré plutôt qu'instantané.
const sizeRaw = useTransform(distance, [0, DISTANCE], [MAX_SIZE, BASE_SIZE]); const size = useSpring(sizeRaw, { stiffness: 300, damping: 25, mass: 0.5 }); // Apply to the motion element <motion.a style={{ width: size, height: size }} ... />Ajoute le conteneur verre dépoli et les tooltips
Stylise le nav avec `backdrop-filter: blur(16px)` et un fond `color-mix(in srgb, var(--color-background) 80%, transparent)` pour qu'il flotte au-dessus du contenu. Pour chaque icône, positionne un `<span>` en haut avec `opacity: 0` et affiche-le au :hover de l'anchor via un bloc `<style>` inline.
Quand l'utiliser
La navbar dock convient aux sites portfolio, applications single-page, outils de design et agences créatives où la navigation est compacte (4 à 7 items) et où l'interaction elle-même fait partie de l'identité de marque. À éviter sur les sites à fort contenu ou les e-commerces où une navbar en haut avec menus déroulants est plus pratique, et à écarter si ton audience est principalement mobile, l'effet de grossissement nécessitant un pointeur.
Utilisé par
- Apple, A inventé le pattern du Dock macOS en 2001 ; le comportement de grossissement est la référence canonique de ce composant.
- Linear, Utilise un rail d'icônes compact dans sa sidebar applicative avec des transitions hover fluides qui rappellent la manipulation directe du dock.
- Raycast, Son site marketing présente des grilles d'icônes animées en spring avec grossissement au survol, directement inspirées du Dock macOS.
- Framer, Présente le grossissement style dock comme une interaction intégrée dans son éditeur visuel, ce qui en fait un classique des sites construits avec Framer.
FAQ
Pourquoi chaque DockIcon a-t-il besoin de sa propre ref ?
La ref donne accès au bounding rect de l'icône dans le callback useTransform, ce qui permet de calculer la distance en pixels réels entre le curseur et le centre précis de cette icône. Sans elle, le grossissement ne pourrait pas tenir compte de la position.
Peut-on utiliser Next.js Link à la place des anchors classiques ?
Oui. Remplace `motion.a` par `motion(Link)` via la factory `motion()` de Framer Motion et passe `href` et `ref` normalement. Assure-toi que `legacyBehavior={false}` (défaut dans Next 13+) afin que Link rende un seul élément anchor.
Comment ajouter d'autres types d'icônes au-delà de la map intégrée ?
Étends l'objet `ICON_MAP` avec des composants Lucide supplémentaires (ou tout SVG React) indexés par un identifiant string. Passe ensuite cet identifiant dans le champ `icon` de chaque item du dock. Le composant se rabat sur l'icône `Home` pour les clés inconnues.
La configuration du spring influence-t-elle la fluidité perçue ?
Fortement. Une stiffness plus élevée fait claquer les icônes plus vite vers leur taille cible ; un damping plus faible ajoute de l'oscillation. Les valeurs par défaut (stiffness 300, damping 25, mass 0.5) donnent une réponse vive sans tremblements. Pour un dock plus doux, essaie stiffness 150 et damping 20.