How to build a draggable reorderable card list in React
A draggable reorderable card list in React is built with Framer Motion's Reorder.Group and Reorder.Item components. The group manages the order state, each item gets spring physics on drag (scale + shadow), and releasing snaps the card into its new position automatically.
- Stack: React + Framer Motion (Reorder API) + Lucide React + Tailwind v4, ~100 lines.
- Core API: Reorder.Group, Reorder.Item, whileDrag, useState for the ordered array.
- Spring config: stiffness 300, damping 25, snappy settle without overshoot.
- Accessible: cards carry semantic h3 headings; keyboard drag is not natively supported by Reorder, so screen-reader users need an alternative reorder mechanism.
- Touch-friendly: Framer Motion's Reorder works on touch devices via pointer events.
This bento section turns a static feature list into a drag-to-reorder experience. Each card lifts with a spring scale and shadow when grabbed, then settles into its new slot the moment you release. It's the kind of micro-interaction that signals craft on a product landing page or dashboard onboarding flow.
Anatomy
The section has three layers. At the top, an optional header block (badge, h2, subtitle) fades in from below on viewport entry. Below that, a Reorder.Group wraps a vertical flex column. Each Reorder.Item is a rounded card containing a GripVertical drag handle on the left, an accent-colored Lucide icon, a bold title, and a muted description paragraph. The grid stays single-column and caps at max-w-3xl for readable line lengths.
How it works
Framer Motion's Reorder.Group takes a `values` prop (the current order array) and an `onReorder` callback that replaces it with the new sorted array. Each Reorder.Item receives the `value` it represents, an object reference, not an index. When a card is dragged, Framer Motion computes intersections with other items and fires onReorder in real time, producing a live-sorted list without manual index juggling. The `whileDrag` prop adds scale:1.03 and a box-shadow during the drag gesture; `transition` with spring stiffness 300 / damping 25 governs how the card settles after release.
How to build it in React
Set up the ordered state
Store your items array in a useState hook. Pass the state value to Reorder.Group's `values` prop and the setter to `onReorder`. The group handles the rest, no index tracking needed on your side.
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>Wrap each card in Reorder.Item
Pass the item object itself as the `value` prop. Framer Motion uses reference equality to identify which item moved, so the value must be the same object reference that lives in the `values` array. Add `cursor-grab active:cursor-grabbing` for clear affordance.
<Reorder.Item value={item} className="rounded-2xl border p-6 cursor-grab active:cursor-grabbing" > {/* card content */} </Reorder.Item>Add spring physics on drag
Use `whileDrag` to scale the card up slightly and cast a shadow. The `transition` spring config on the item governs how it snaps back into position when dropped. Keep stiffness high and damping moderate so the settle feels instant but not jarring.
whileDrag={{ scale: 1.03, boxShadow: "0 10px 40px rgba(0,0,0,0.1)", }} transition={{ type: "spring", stiffness: 300, damping: 25 }}Resolve Lucide icons by name
Import the entire Lucide namespace and write a small helper that returns the icon component by string name. Store the icon name in your data objects so the component stays purely presentational with no hard-coded imports per icon.
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; }
When to use it
Reach for this pattern on product dashboards where users personalize a feature list or priority order, onboarding checklists where sequence matters, or landing pages where you want to show flexibility and interactivity without a full drag-and-drop library. Skip it on purely informational pages where reordering carries no meaning, the affordance creates an expectation that the order is persisted or acted on. Make sure you save the new order to state (or a backend) when the interaction matters.
Used by
- Linear, Drag-to-reorder is core to issue and project priority management across the app.
- Notion, Every block in Notion is draggable with a grip handle, the same GripVertical affordance used here.
- Trello, Card drag-and-drop within and between columns is the product's primary interaction model.
- Framer, Reorderable layers panel and component lists throughout the design tool use spring-driven drag interactions.
FAQ
Does Framer Motion's Reorder work on mobile/touch?
Yes. Framer Motion uses pointer events under the hood, so touch and stylus input triggers drag correctly. Test on iOS Safari specifically, as momentum scrolling can conflict with vertical drag gestures, you may need to prevent default scroll on the list container.
How do I persist the new order after the user reorders?
The `onReorder` callback receives the new array immediately on every drag update. For persistence, debounce a save call inside a useEffect that watches the items state, or fire a POST request on a separate drag-end event using onDragEnd on each Reorder.Item.
Can I use this with a grid layout instead of a vertical list?
Reorder.Group supports `axis="x"` for horizontal lists and `axis="y"` (used here) for vertical ones. True 2D grid reordering is not supported by Framer Motion's Reorder out of the box; for that, reach for a library like dnd-kit which handles multi-axis drag natively.
Is the drag interaction accessible to keyboard users?
Framer Motion's Reorder does not expose keyboard reordering out of the box. For full accessibility, add visible buttons (move up / move down) that call array splice operations on the items state alongside the drag interface.