Cómo crear una lista de tarjetas reordenables por arrastre en React
Una lista de tarjetas reordenables en React se construye con los componentes Reorder.Group y Reorder.Item de Framer Motion. El group gestiona el estado del orden, cada item recibe física spring al arrastrar (scale + sombra), y al soltar la tarjeta encaja en su nueva posición automáticamente.
- Stack: React + Framer Motion (API Reorder) + Lucide React + Tailwind v4, ~100 líneas.
- API clave: Reorder.Group, Reorder.Item, whileDrag, useState para el array ordenado.
- Config spring: stiffness 300, damping 25, asentamiento ágil sin rebote excesivo.
- Accesible: las tarjetas llevan encabezados h3 semánticos; el arrastre por teclado no está soportado nativamente por Reorder, así que los usuarios de lector de pantalla necesitan un mecanismo de reordenación alternativo.
- Compatible con táctil: el Reorder de Framer Motion funciona en dispositivos táctiles mediante pointer events.
Esta sección bento convierte una lista de características estática en una experiencia drag-to-reorder. Cada tarjeta se levanta con un spring (scale + sombra) cuando la agarras, y luego encaja en su nuevo hueco en cuanto la sueltas. Es el tipo de microinteracción que transmite cuidado en una landing de producto o en un flujo de onboarding.
Anatomía
La sección se divide en tres capas. Arriba, un bloque de cabecera opcional (badge, h2, subtítulo) aparece con un fundido desde abajo al entrar en el viewport. Debajo, un Reorder.Group envuelve una columna flex vertical. Cada Reorder.Item es una tarjeta redondeada que contiene un tirador GripVertical a la izquierda, un icono Lucide con color de acento, un título en negrita y un párrafo de descripción atenuado. La cuadrícula se mantiene en una sola columna y se limita a max-w-3xl para longitudes de línea legibles.
Cómo funciona
El Reorder.Group de Framer Motion recibe una prop `values` (el array del orden actual) y un callback `onReorder` que lo reemplaza por el nuevo array ordenado. Cada Reorder.Item recibe el `value` que representa, una referencia de objeto, no un índice. Cuando se arrastra una tarjeta, Framer Motion calcula las intersecciones con los demás items y dispara onReorder en tiempo real, produciendo una lista ordenada en vivo sin malabares manuales con índices. La prop `whileDrag` añade scale:1.03 y un box-shadow durante el gesto de arrastre; `transition` con spring stiffness 300 / damping 25 gobierna cómo se asienta la tarjeta tras soltarla.
Cómo crearlo en React
Configura el estado ordenado
Guarda tu array de items en un hook useState. Pasa el valor del estado a la prop `values` de Reorder.Group y el setter a `onReorder`. El group se encarga del resto, sin necesidad de seguir índices por tu parte.
const [items, setItems] = React.useState(initialItems); <Reorder.Group axis="y" values={items} onReorder={setItems}> {items.map((item) => ( <DraggableCard key={item.id} item={item} /> ))} </Reorder.Group>Envuelve cada tarjeta en Reorder.Item
Pasa el propio objeto item como prop `value`. Framer Motion usa la igualdad por referencia para identificar qué item se movió, así que el value debe ser la misma referencia de objeto que vive en el array `values`. Añade `cursor-grab active:cursor-grabbing` para una affordance clara.
<Reorder.Item value={item} className="rounded-2xl border p-6 cursor-grab active:cursor-grabbing" > {/* card content */} </Reorder.Item>Añade física spring al arrastrar
Usa `whileDrag` para agrandar ligeramente la tarjeta y proyectar una sombra. El spring de `transition` en el item gobierna cómo encaja de nuevo en su posición al soltarla. Mantén una stiffness alta y un damping moderado para que el asentamiento se sienta instantáneo pero no brusco.
whileDrag={{ scale: 1.03, boxShadow: "0 10px 40px rgba(0,0,0,0.1)", }} transition={{ type: "spring", stiffness: 300, damping: 25 }}Resuelve los iconos Lucide por nombre
Importa todo el namespace de Lucide y escribe un pequeño helper que devuelva el componente de icono por nombre de cadena. Guarda el nombre del icono en tus objetos de datos para que el componente siga siendo puramente presentacional, sin imports fijos por cada icono.
import * as LucideIcons from "lucide-react"; function getIcon(name?: string) { if (!name) return null; return (LucideIcons as unknown as Record<string, React.ElementType>)[name] ?? null; }
Cuándo usarlo
Recurre a este patrón en dashboards de producto donde los usuarios personalizan una lista de características o un orden de prioridad, en checklists de onboarding donde la secuencia importa, o en landing pages donde quieres mostrar flexibilidad e interactividad sin una librería de drag-and-drop completa. Evítalo en páginas puramente informativas donde reordenar no tiene significado: la affordance crea la expectativa de que el orden se persiste o se utiliza. Asegúrate de guardar el nuevo orden en el estado (o en un backend) cuando la interacción importa.
Usado por
- Linear, El drag-to-reorder es clave en la gestión de prioridades de issues y proyectos en toda la app.
- Notion, Cada bloque en Notion es arrastrable con un tirador grip, la misma affordance GripVertical que se usa aquí.
- Trello, El drag-and-drop de tarjetas dentro y entre columnas es el modelo de interacción principal del producto.
- Framer, El panel de capas reordenable y las listas de componentes de la herramienta de diseño usan interacciones de arrastre impulsadas por spring.
FAQ
¿El Reorder de Framer Motion funciona en móvil/táctil?
Sí. Framer Motion usa pointer events por debajo, así que la entrada táctil y de stylus dispara el arrastre correctamente. Prueba en iOS Safari en particular, ya que el scroll por inercia puede entrar en conflicto con los gestos de arrastre vertical: puede que necesites prevenir el scroll por defecto en el contenedor de la lista.
¿Cómo persisto el nuevo orden después de que el usuario reordena?
El callback `onReorder` recibe el nuevo array de inmediato en cada actualización del arrastre. Para la persistencia, aplica debounce a una llamada de guardado dentro de un useEffect que observe el estado items, o lanza una petición POST en un evento de drag-end separado usando onDragEnd en cada Reorder.Item.
¿Puedo usar esto con un layout de cuadrícula en lugar de una lista vertical?
Reorder.Group admite `axis="x"` para listas horizontales y `axis="y"` (usado aquí) para las verticales. La reordenación real en cuadrícula 2D no está soportada de fábrica por el Reorder de Framer Motion; para eso, recurre a una librería como dnd-kit, que gestiona el arrastre multieje de forma nativa.
¿Es la interacción de arrastre accesible para usuarios de teclado?
El Reorder de Framer Motion no expone la reordenación por teclado de fábrica. Para una accesibilidad completa, añade botones visibles (subir / bajar) que ejecuten operaciones de splice sobre el estado items junto a la interfaz de arrastre.