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

Cómo crear pestañas pill animadas en React con Framer Motion

Las pestañas pill animadas en React usan el layoutId de Framer Motion para compartir un único elemento animado entre los botones, creando un indicador que se desliza al cambiar la pestaña activa. La zona de contenido se intercambia con AnimatePresence en modo 'wait', dejando que el contenido saliente desaparezca antes de que entre el nuevo.

  • Stack: React 18 + Framer Motion 11 + Lucide React, ~240 líneas, sin dependencias adicionales.
  • Indicador pill activo: layoutId compartido 'pill-indicator' con un spring (stiffness 400, damping 30).
  • Transición de contenido: AnimatePresence mode='wait' con deslizamiento en el eje Y (entrada +16px, salida -10px, duración 350ms).
  • Accesible: las pestañas son elementos <button> nativos; la navegación por teclado funciona desde el inicio.
  • Responsive: cuadrícula de dos columnas en md+ (texto a la izquierda, visual a la derecha), una columna en móvil.

Content Tabs Pill es una sección React con una barra de pestañas segmentada donde un indicador compartido se desliza entre las opciones y revela el panel de contenido correspondiente. Este patrón cubre el caso habitual de 'una sección, varias audiencias o funcionalidades' que se ve en las páginas de pricing SaaS, los resúmenes de funcionalidades y las páginas de soluciones, sin el peso visual de las pestañas subrayadas o de las tarjetas completas.

Anatomía

La sección tiene tres zonas apiladas. Arriba, un encabezado centrado con un badge opcional, un título h2 y un párrafo de subtítulo, revelados por un único fade-up whileInView. Debajo, la barra de pestañas pill: un contenedor redondeado con un borde contiene los botones uno al lado del otro; cada botón se posiciona en relative para que el div indicador pueda colocarse en absolute inset. El indicador activo se renderiza únicamente dentro del botón activo, de modo que el sistema de layout de Framer Motion puede animarlo entre los siblings. La zona de contenido es una cuadrícula de dos columnas en desktop: texto (título, descripción, fila de stats opcional) a la izquierda, placeholder visual a la derecha.

Cómo funciona

El indicador pill usa layoutId='pill-indicator' en una motion.div renderizada condicionalmente dentro de cada botón. Cuando la pestaña activa cambia, el indicador antiguo se desmonta y el nuevo se monta, Framer Motion detecta el layoutId compartido y anima la transición entre las dos posiciones mediante un spring (stiffness 400, damping 30). El indicador se sitúa detrás del label mediante z-index y lleva un 12% de opacidad sobre el color accent, para que el texto siga siendo legible. Los intercambios de contenido usan AnimatePresence mode='wait': el panel saliente sube y se desvanece antes de que el panel entrante se deslice desde abajo, ambos impulsados por una constante EASE [0.16, 1, 0.3, 1] para una sensación ágil y natural.

Cómo crearlo en React

  1. Configurar el estado y los datos

    Guarda el id de la pestaña activa con useState, inicializado con el id de la primera pestaña. Mantén la lista de pestañas como prop para que el componente siga siendo puramente presentacional. Cada pestaña lleva un id, un label, un nombre de icono Lucide opcional, un título, una descripción y un array de stats opcional.

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

    Renderiza cada pestaña como un botón nativo dentro de un contenedor flex redondeado. Dentro del botón activo, renderiza una motion.div con layoutId='pill-indicator' posicionada en absolute con inset:0. Framer Motion la animará entre los botones que se vuelven activos, dándote el efecto de deslizamiento sin ningún cálculo manual.

    {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. Animar el panel de contenido

    Envuelve el bloque de contenido en AnimatePresence con mode='wait' y asígnale current.id como key. Esto garantiza que el contenido saliente se vaya por completo antes de que aparezca el contenido entrante, evitando solapamientos. Usa un fade con desplazamiento en y: initial y:16, animate y:0, exit y:-10 con tu cubic-bezier EASE para una sensación direccional cuidada.

    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. Añadir iconos y stats opcionales

    Resuelve los iconos Lucide dinámicamente a partir del nombre de icono almacenado en cada pestaña. Renderiza las stats como flex row debajo de la descripción cuando estén presentes. Las stats usan el color accent para el valor y un color atenuado para el label, creando un anclaje visual rápido sin librería de gráficos.

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

Cuándo usarlo

Usa este patrón en secciones de resumen de funcionalidades, páginas de soluciones o niveles de precios donde tienes de tres a cinco opciones distintas que presentar y quieres mantener el espacio vertical compacto. Funciona bien justo después de un hero o de una fila de stats. Evítalo si tienes más de seis pestañas, la barra pill se ajusta mal en pantallas pequeñas, y descártalo cuando el contenido de cada panel es demasiado similar; los usuarios dejan de cambiar de pestaña si no perciben una diferencia real entre los paneles.

Usado por

  • Stripe, Usa pestañas pill segmentadas para alternar entre las categorías de productos en sus principales páginas de marketing.
  • Notion, Aparecen barras de pestañas estilo pill en las secciones de comparación de funcionalidades y de soluciones por equipo del sitio de marketing.
  • Linear, Selectores de pestañas animados con indicadores deslizantes presentan las funcionalidades de workflow en la landing page del producto.
  • Loom, Controles segmentados alternan entre los paneles de casos de uso en las páginas de soluciones y de precios.

FAQ

¿Por qué usar layoutId en vez de animar left/width manualmente?

layoutId deja que Framer Motion mida automáticamente las posiciones de los botones antiguo y nuevo e interpole entre ellas. Una animación manual de left/width obliga a rastrear las medidas del DOM en cada redimensionamiento y cambio de pestaña, lo cual es frágil e innecesario cuando el sistema de layout se encarga de ello.

¿Qué ocurre si tengo demasiadas pestañas?

La barra pill es un flex row sin salto de línea ni scroll de overflow, así que más allá de cinco o seis elementos los labels se cortan en móvil. Para más opciones, cambia a una lista vertical o un select desplegable en pantallas pequeñas, o añade overflow-x:auto con scroll-snap al contenedor de pestañas.

¿Puedo añadir una imagen o captura de pantalla en lugar del placeholder?

Sí. Reemplaza el div placeholder de la columna derecha por un <Image> de Next.js o una etiqueta <video>. La transición de AnimatePresence sobre la key ya anima todo el panel de la cuadrícula, así que la imagen se desvanecerá y se deslizará junto con el resto del contenido sin coste adicional.

¿Es el componente accesible por teclado?

Cada pestaña es un <button> nativo, así que Tab y Shift+Tab recorren las pestañas y Enter/Espacio activa la que tiene el foco. El componente no implementa el patrón ARIA tabs completo (roving tabindex con teclas de flecha), que es la spec completa de WAI-ARIA. Si se requiere conformidad WCAG, añade role='tablist', role='tab', aria-selected y manejadores de teclas de flecha.

"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

Reseñas

Pestañas pill animadas en React, tutorial de Framer Motion