Retour au catalogue

Content Tabs Pill

Tabs en pill/segment avec animation de slide, contenu texte avec stats et visuel a droite.

content-tabsmedium Both Responsive a11y
minimalplayfuluniversalsaasagencycentered
Theme

Créer des onglets pill animés en React avec Framer Motion

Les onglets pill animés en React utilisent le layoutId de Framer Motion pour partager un seul élément animé entre les boutons, créant un indicateur qui glisse lors du changement d'onglet actif. La zone de contenu permute avec AnimatePresence en mode 'wait', laissant le contenu sortant partir avant que le nouveau n'entre.

  • Stack : React 18 + Framer Motion 11 + Lucide React, ~240 lignes, zéro dépendance supplémentaire.
  • Indicateur pill actif : layoutId partagé 'pill-indicator' avec un spring (stiffness 400, damping 30).
  • Transition de contenu : AnimatePresence mode='wait' avec glissement sur l'axe Y (entrée +16px, sortie -10px, durée 350ms).
  • Accessible : les onglets sont de vrais éléments <button> ; la navigation clavier fonctionne nativement.
  • Responsive : grille deux colonnes sur md+ (texte à gauche, visuel à droite), une colonne sur mobile.

Content Tabs Pill est une section React avec une barre d'onglets segmentée où un indicateur partagé glisse entre les options et fait apparaître le panneau de contenu correspondant. Ce pattern couvre le cas classique 'une section, plusieurs audiences ou fonctionnalités' que l'on retrouve sur les pages pricing SaaS, les aperçus de fonctionnalités et les pages de solutions, sans le poids visuel des onglets soulignés ou des cartes complètes.

Anatomie

La section comporte trois zones empilées. En haut, un header centré avec un badge optionnel, un titre h2 et un sous-titre, révélés par un seul fade-up whileInView. En dessous, la barre d'onglets pill : un conteneur arrondi avec une bordure contient les boutons côte à côte ; chaque bouton est positionné en relative pour que le div indicateur puisse s'y positionner en absolu inset. L'indicateur actif est rendu uniquement dans le bouton actif, ce qui permet au système de layout de Framer Motion de l'animer entre les siblings. La zone de contenu est une grille deux colonnes sur desktop : texte (titre, description, stats optionnelles) à gauche, placeholder visuel à droite.

Comment ça marche

L'indicateur pill utilise layoutId='pill-indicator' sur une motion.div rendue conditionnellement dans chaque bouton. Quand l'onglet actif change, l'ancien indicateur se démonte et le nouveau se monte, Framer Motion détecte le layoutId partagé et anime la transition entre les deux positions via un spring (stiffness 400, damping 30). L'indicateur passe derrière le label via z-index et porte 12% d'opacité sur la couleur accent, pour garder le texte lisible. Les permutations de contenu utilisent AnimatePresence mode='wait' : le panneau sortant monte et disparaît avant que le panneau entrant glisse depuis le bas, tous deux pilotés par une constante EASE [0.16, 1, 0.3, 1] pour une sensation vive et naturelle.

Comment le coder en React

  1. Mettre en place l'état et les données

    Stocke l'identifiant de l'onglet actif avec useState, initialisé sur le premier onglet. Garde la liste des onglets en prop pour que le composant reste purement présentationnel. Chaque onglet porte un id, un label, un nom d'icône Lucide optionnel, un titre, une description et un tableau de stats optionnel.

    const [activeTab, setActiveTab] = useState(tabs[0]?.id ?? "");
    const current = tabs.find((t) => t.id === activeTab);
  2. Construire la barre pill glissante

    Rends chaque onglet comme un bouton natif dans un conteneur flex arrondi. Dans le bouton actif, rends une motion.div avec layoutId='pill-indicator' positionnée en absolu avec inset:0. Framer Motion l'animera entre les boutons qui deviennent actifs, donnant l'effet de glissement sans aucun calcul manuel.

    {isActive && (
      <motion.div
        layoutId="pill-indicator"
        style={{ position: "absolute", inset: 0, borderRadius: "var(--radius-full)", background: "var(--color-accent)", opacity: 0.12 }}
        transition={{ type: "spring", stiffness: 400, damping: 30 }}
      />
    )}
  3. Animer le panneau de contenu

    Enveloppe le bloc de contenu dans AnimatePresence avec mode='wait' et donne-lui current.id comme key. Cela garantit que le contenu sortant part complètement avant que le contenu entrant n'apparaisse, sans chevauchement. Utilise un fade avec décalage y : initial y:16, animate y:0, exit y:-10 avec ton cubic-bezier EASE pour une sensation directionnelle soignée.

    const EASE = [0.16, 1, 0.3, 1] as const;
    
    <AnimatePresence mode="wait">
      {current && (
        <motion.div
          key={current.id}
          initial={{ opacity: 0, y: 16 }}
          animate={{ opacity: 1, y: 0 }}
          exit={{ opacity: 0, y: -10 }}
          transition={{ duration: 0.35, ease: EASE }}
        >
          {/* content */}
        </motion.div>
      )}
    </AnimatePresence>
  4. Ajouter les icônes et les stats optionnelles

    Résous les icônes Lucide dynamiquement à partir du nom d'icône stocké dans chaque onglet. Rends les stats en flex row sous la description quand elles sont présentes. Les stats utilisent la couleur accent pour la valeur et une couleur atténuée pour le label, créant un ancrage visuel rapide sans librairie de graphiques.

    function getIcon(name?: string) {
      if (!name) return null;
      return (LucideIcons as unknown as Record<string, React.ElementType>)[name] ?? null;
    }

Quand l'utiliser

Ce pattern convient aux sections d'aperçu de fonctionnalités, aux pages de solutions ou aux niveaux tarifaires où tu as trois à cinq options distinctes à présenter tout en gardant un espace vertical compact. Il fonctionne bien juste après un hero ou une ligne de stats. À éviter si tu as plus de six onglets, la barre pill passe mal à la ligne sur petits écrans, et à proscrire quand le contenu de chaque panneau est trop similaire ; les utilisateurs cessent de naviguer s'ils ne perçoivent pas de vraie différence entre les panneaux.

Utilisé par

  • Stripe, Utilise des onglets pill segmentés pour basculer entre les catégories de produits sur ses pages marketing principales.
  • Notion, Des barres d'onglets style pill apparaissent dans les sections de comparaison de fonctionnalités et de solutions par équipe sur le site marketing.
  • Linear, Des sélecteurs d'onglets animés avec indicateurs glissants présentent les fonctionnalités de workflow sur la landing page produit.
  • Loom, Des contrôles segmentés font basculer entre les panneaux de cas d'usage sur les pages de solutions et de tarification.

FAQ

Pourquoi utiliser layoutId plutôt qu'animer left/width manuellement ?

layoutId laisse Framer Motion mesurer automatiquement les positions des anciens et nouveaux boutons et interpoler entre elles. Une animation manuelle de left/width impose de suivre les mesures DOM à chaque redimensionnement et changement d'onglet, c'est fragile et inutile quand le système de layout s'en charge.

Que se passe-t-il si j'ai trop d'onglets ?

La barre pill est un flex row sans retour à la ligne ni défilement, donc au-delà de cinq ou six éléments les labels se coupent sur mobile. Pour plus d'options, passe à une liste verticale ou un select déroulant sur petits écrans, ou ajoute overflow-x:auto avec scroll-snap au conteneur d'onglets.

Puis-je ajouter une image ou capture d'écran à la place du placeholder ?

Oui. Remplace le div placeholder dans la colonne de droite par un <Image> Next.js ou une balise <video>. La transition AnimatePresence sur la key anime déjà tout le panneau de grille, donc l'image s'estompera et glissera avec le reste du contenu sans coût supplémentaire.

Le composant est-il accessible au clavier ?

Chaque onglet est un vrai <button> natif, donc Tab et Shift+Tab les parcourent et Entrée/Espace active celui qui a le focus. Le composant n'implémente pas le pattern ARIA tabs complet (tabindex tournant avec les flèches), qui correspond à la spec WAI-ARIA complète. Si la conformité WCAG est requise, ajouter role='tablist', role='tab', aria-selected et des gestionnaires de touches fléchées.

"use client";

import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import * as LucideIcons from "lucide-react";
import React from "react";

interface TabItem {
  id: string;
  label: string;
  icon?: string;
  title: string;
  description: string;
  stats?: { label: string; value: string }[];
}

interface ContentTabsPillProps {
  badge?: string;
  title?: string;
  subtitle?: string;
  tabs: TabItem[];
}

Code complet réservé à Pro

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

Passer en Pro, 9,99€/mois

Avis

Onglets pill React animés, Tutoriel Framer Motion