Retour au catalogue

Careers Hover Reveal

Liste de postes minimaliste. Au hover, un panneau se revele avec description, requirements et bouton postuler. Slide depuis la droite avec AnimatePresence.

careerscomplex Both Responsive a11y
minimalelegantsaasuniversalagencysplit
Theme

Cómo crear una sección de empleos con panel reveal al pasar el ratón en React

Una lista de empleos con reveal al pasar el ratón sigue la fila activa con un único índice useState, y luego usa AnimatePresence de Framer Motion para deslizar un panel de detalle desde la derecha cada vez que cambia el índice activo. El panel anterior sale antes de que entre el siguiente gracias a mode="wait", evitando que dos paneles se superpongan.

  • Stack: React 18 + Framer Motion 11 + Lucide React, ~160 líneas, cero dependencias adicionales.
  • API clave: useState para el índice activo, AnimatePresence mode="wait", motion.div con transiciones x/opacity.
  • El panel de detalle es sticky (top: 6rem) para seguir visible mientras el usuario recorre una lista larga de empleos.
  • Accesible: los títulos de los puestos usan encabezados h3; el botón de postular es un anchor nativo con una URL real.
  • En móvil las dos columnas se apilan, el panel de detalle se coloca debajo de la lista y sigue siendo totalmente usable al tacto mediante onClick.

Careers Hover Reveal es una sección React con split-layout pensada para páginas de empleo y reclutamiento. A la izquierda hay una lista legible de los puestos abiertos; al pasar el ratón sobre una fila se desliza desde la derecha un panel de detalle con la descripción completa, los requisitos y un CTA para postular. El resultado es un flujo de lectura tipo revista que permite a los candidatos explorar los puestos sin salir de la página.

Anatomía

La maquetación es una fila flex con dos hijos flex que se apilan en pantallas estrechas. La columna izquierda es una lista apilada de filas de puestos, cada una mostrando título, badge de departamento, ubicación y tipo de contrato. Un icono de flecha anima su opacidad y su desplazamiento x para señalar la fila activa. La columna derecha contiene un contenedor sticky con un único slot AnimatePresence: cuando active no es nulo, se renderiza una tarjeta de detalle con la etiqueta de departamento, el título, el párrafo de descripción, una lista de requisitos animada y un anchor para postular.

Cómo funciona

Toda la interacción depende de una sola pieza de state: `const [active, setActive] = useState<number | null>(null)`. Cada fila de puesto actualiza active en mouseEnter y lo alterna al hacer clic (para usuarios táctiles). El panel de detalle se envuelve en AnimatePresence con mode="wait", que vacía el elemento saliente antes de montar el siguiente. El propio panel entra con `{ opacity: 0, x: 30 }` y sale con los mismos valores, por lo que la transición se lee como un intercambio lateral en vez de un simple fundido. Dentro del panel, cada `<li>` de requisito se escalona mediante un retardo por elemento (`delay: ri * 0.04`) para que la lista entre en cascada una vez que la tarjeta se asienta.

Cómo crearlo en React

  1. Configurar el split layout y el state

    Crea un contenedor flex con dos hijos: una columna de lista de empleos (flex: 1 1 340px) y una columna de detalle (flex: 1 1 380px). Declara `const [active, setActive] = useState<number | null>(null)` en la raíz del componente. Este único entero dirige toda la parte interactiva del componente.

    const [active, setActive] = useState<number | null>(null);
  2. Construir las filas de puestos con handlers de hover y clic

    Recorre tu array de empleos y renderiza una motion.div por cada entrada. Asocia `onMouseEnter={() => setActive(i)}` para escritorio y `onClick={() => setActive(active === i ? null : i)}` para alternar el panel en dispositivos táctiles. Anima el fondo de transparente a tu color alt-background cuando la fila está activa.

    onMouseEnter={() => setActive(i)}
    onClick={() => setActive(active === i ? null : i)}
  3. Envolver el panel de detalle en AnimatePresence

    Coloca AnimatePresence con mode="wait" en la columna derecha. Dentro, renderiza condicionalmente una motion.div con la key `active`. Define initial/exit en `{ opacity: 0, x: 30 }` y animate en `{ opacity: 1, x: 0 }`. El cambio de key obliga a AnimatePresence a sacar el panel antiguo y entrar el nuevo cada vez que cambia el puesto activo.

    <AnimatePresence mode="wait">
      {active !== null && (
        <motion.div
          key={active}
          initial={{ opacity: 0, x: 30 }}
          animate={{ opacity: 1, x: 0 }}
          exit={{ opacity: 0, x: 30 }}
          transition={{ duration: 0.35, ease: [0.16, 1, 0.3, 1] }}
        >
          {/* job detail content */}
        </motion.div>
      )}
    </AnimatePresence>
  4. Escalonar en cascada la lista de requisitos dentro del panel

    Mapea los requisitos a elementos `<motion.li>`. Dale a cada uno `initial={{ opacity: 0, x: -8 }}`, `animate={{ opacity: 1, x: 0 }}` y un retardo de transición de `ri * 0.04` segundos. Como AnimatePresence vuelve a montar el panel en cada cambio de active, la cascada se reproduce automáticamente para cada nuevo puesto.

    <motion.li
      key={ri}
      initial={{ opacity: 0, x: -8 }}
      animate={{ opacity: 1, x: 0 }}
      transition={{ duration: 0.25, delay: ri * 0.04 }}
    >

Cuándo usarlo

Recurre a este patrón cuando una empresa tiene de 4 a 12 puestos abiertos y quiere una sección de empleos dedicada en lugar de una simple página de lista. Funciona bien como sección al final de la página, junto a un bloque de cultura o de ventajas. Evítalo si tienes más de 20 puestos: una tabla filtrable o una interfaz de búsqueda escala mejor. En páginas con contenido crítico donde cada píxel cuenta, la columna del panel sticky ocupa la mitad del ancho del viewport en escritorio.

Usado por

  • Notion, Usa una página de empleos con split-layout depurada donde hacer clic en un puesto revela los detalles en línea sin navegar de página.
  • Linear, Lista de empleos con un diseño de fila minimalista y una vista de detalle contextual que mantiene al candidato centrado en la lista.
  • Vercel, Puestos agrupados por departamento con comportamiento de expansión inline, manteniendo la experiencia de navegación en una sola página.
  • Stripe, Navegador de empleos con panel dividido, filtros por departamento y una columna de detalle persistente para el puesto seleccionado.

FAQ

¿Por qué usar AnimatePresence mode="wait" en lugar de mode="sync"?

mode="wait" garantiza que el panel saliente se desmonte por completo antes de que entre el nuevo, evitando que dos tarjetas de detalle sean visibles a la vez. Con mode="sync" los dos paneles se superponen brevemente durante la transición, lo que resulta caótico cuando el usuario pasa el ratón rápidamente entre filas.

¿Cómo añado el panel en móvil donde no hay hover?

El handler onClick ya gestiona el tacto: tocar una fila la marca como activa y un segundo toque la deselecciona. En pantallas pequeñas las dos columnas flex se apilan, por lo que el panel de detalle queda debajo de la lista. Puede que quieras desplazar el panel a la vista con una ref y scrollIntoView cuando cambia active.

¿Cuántos puestos puede manejar este componente antes de volverse poco práctico?

El componente funciona mejor con 4 a 12 puestos. Más allá de eso, la columna izquierda se convierte en un scroll largo y el panel sticky empieza a sentirse desconectado. Para listas más grandes, cambia a una tabla filtrable con un drawer o un modal para la vista de detalle.

¿Puedo activar el panel desde un hash de URL para enlazar directamente a un puesto concreto?

Sí. En el montaje, lee window.location.hash, busca el índice del puesto correspondiente y llama a setActive con él. Combínalo con un effect que actualice el hash cuando cambia active. El componente ya usa índices numéricos, así que mapea cada puesto a un slug estable para el hash.

"use client";

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

interface Job {
  title: string;
  department: string;
  location: string;
  type: string;
  description: string;
  requirements: string[];
  url: string;
}

interface CareersHoverRevealProps {
  title?: string;
  subtitle?: string;
  jobs?: Job[];
}

Code complet réservé à Pro

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

Passer en Pro, 9,99€/mois

Reseñas

Lista de empleos React con panel reveal al pasar el ratón,