Swap Grid

A grid of slots: drop an item on another and the two trade places, drop it on an empty slot and it moves there.

Examples

Dashboard widgets

Drag a widget onto another one. Both glide to their new slots from where they were.

Revenue€48,210+6.2% this month
Active members1,284+38 this week
Open tickets279 waiting on you
Uptime99.94%Last 30 days
First response2 h 10 minMedian
Renewals due12Before 1 December

Press Enter or Space to pick up an item, the arrow keys to swap it with its neighbour, Enter to drop it and Escape to cancel.

Illustrative figures

Shortcuts with empty slots

defaultSlots sets the starting layout, with null for an empty slot. Empty slots show a dashed outline.

Inbox
Calendar
Docs
Search
People
Settings

Press Enter or Space to pick up an item, the arrow keys to swap it with its neighbour, Enter to drop it and Escape to cancel.

Moodboard

onSlotsChange reports the layout after each move: here the first slot is the cover image.

Flat illustration of warm hills under a coral sun
Dawn
Flat illustration of olive hills under a yellow sun
Meadow
Flat illustration of grey blue hills under a pale sun
Mist
Flat illustration of dark hills under the moon at night
Night

Press Enter or Space to pick up an item, the arrow keys to swap it with its neighbour, Enter to drop it and Escape to cancel.

Cover image: Dawn

Installation

npx shadcn@latest add https://ui.ndekocode.dev/r/motion-swap-grid.json

Component API

SwapGrid

PropTypeDefaultDescription
itemsSwapGridItem[]The items that can be placed in the grid.
columnsnumber3The number of columns. Rows follow from the number of slots.
defaultSlots(string | null)[]the item idsThe starting layout: one entry per slot, an item id or null when empty.
onSlotsChange(slots: (string | null)[]) => voidCalled with the new layout after each swap or move.
classNamestringClasses for the grid, such as the row height (auto-rows-[104px]).
itemClassNamestringClasses for the surface of each item.
aria-labelstringThe name of the grid.

SwapGridItem

FieldTypeDescription
idstringA stable id, used in defaultSlots and onSlotsChange.
labelstringThe name read to screen readers and in the announcements.
contentReact.ReactNodeWhat the item shows. Keep controls out: the item is a button.

Accessibility and motion

  • Every item is a button. Press Enter or Space to pick it up, the arrow keys to swap it with its neighbour, Enter to drop it and Escape to put everything back. Each step is announced.
  • Tab follows the visual order of the grid, which changes as items move.
  • A drag starts after the pointer moves 4px, follows the pointer captured by the item, and ignores a second finger. Dropping between slots sends the item home.
  • Items glide to their new places on a spring with no bounce, keeping the speed of the throw. With reduced motion they change place at once and the picked-up item does not grow.
  • Images inside items need draggable={false} so the browser does not start its own image drag.

Credits

Inspired by the Swapy demos of UI Layouts, rebuilt with motion and without the swapy package.