Swapy
A framework-agnostic tool that converts any layout into a drag-to-swap one with just a few lines of code.
A framework-agnostic tool that converts any layout into a drag-to-swap one with just a few lines of code.
Project Views
last year
New Users
+10%
We Build Future of
Design Industry
Daily New clients
SK-Modernist
Smart Digital
Agency For Your
Business
500+ Users
Don't Take Our Words For It...
npm install swapy
1"use client"23import type React from "react"45import { useEffect, useRef } from "react"6import { createSwapy, type SlotItemMapArray } from "swapy"7import { cn } from "@/lib/utils"89type AnimationType = "dynamic" | "spring" | "none"10type SwapMode = "hover" | "drop"1112type Config = {13animation: AnimationType14continuousMode: boolean15manualSwap: boolean16swapMode: SwapMode17autoScrollOnDrag: boolean18}1920type SwapyLayoutProps = {21id: string22enable?: boolean23onSwap?: (event: { newSlotItemMap: { asArray: SlotItemMapArray } }) => void24config?: Partial<Config>25className?: string26children: React.ReactNode27}2829export const SwapyLayout = ({ id, onSwap, config = {}, className, children }: SwapyLayoutProps) => {30const containerRef = useRef<HTMLDivElement | null>(null)31const swapyRef = useRef<ReturnType<typeof createSwapy> | null>(null)3233useEffect(() => {34const container = containerRef.current35if (!container) return3637swapyRef.current = createSwapy(container, config)3839if (onSwap) {40swapyRef.current.onSwap(onSwap)41}4243return () => {44swapyRef.current?.destroy()45}46}, [config, onSwap])4748return (49<div id={id} ref={containerRef} className={className}>50{children}51</div>52)53}5455export const DragHandle = ({className}:{className?:string}) => {56return (57<div58data-swapy-handle59className={cn("absolute top-2 left-2 cursor-grab text-gray-500 rounded-md active:cursor-grabbing dark:border-gray-700 dark:bg-gray-800",className)}60>61<svg62xmlns="http://www.w3.org/2000/svg"63width="24"64height="24"65viewBox="0 0 24 24"66fill="none"67stroke="currentColor"68stroke-width="2"69stroke-linecap="round"70stroke-linejoin="round"71className="lucide lucide-grip-vertical-icon lucide-grip-vertical opacity-80"72>73<circle cx="9" cy="12" r="1" />74<circle cx="9" cy="5" r="1" />75<circle cx="9" cy="19" r="1" />76<circle cx="15" cy="12" r="1" />77<circle cx="15" cy="5" r="1" />78<circle cx="15" cy="19" r="1" />79</svg>80</div>81);82};8384export const SwapySlot = ({85id,86className,87children,88}: {89id: string90className?: string91children: React.ReactNode92}) => {93return (94<div className={cn("data-[swapy-highlighted]:bg-neutral-200 data-[swapy-highlighted]:dark:bg-neutral-800", className)} data-swapy-slot={id}>95{children}96</div>97)98}99100const dragOpacityClassMap: Record<number, string> = {10110: "data-[swapy-dragging]:opacity-10",10220: "data-[swapy-dragging]:opacity-20",10330: "data-[swapy-dragging]:opacity-30",10440: "data-[swapy-dragging]:opacity-40",10550: "data-[swapy-dragging]:opacity-50",10660: "data-[swapy-dragging]:opacity-60",10770: "data-[swapy-dragging]:opacity-70",10880: "data-[swapy-dragging]:opacity-80",10990: "data-[swapy-dragging]:opacity-90",110100: "data-[swapy-dragging]:opacity-100",111};112113export const SwapyItem = ({114id,115className,116children,117dragItemOpacity = 100, // default to 100118}: {119id: string;120className?: string;121children: React.ReactNode;122dragItemOpacity?: number;123}) => {124const opacityClass = dragOpacityClassMap[dragItemOpacity] ?? "data-[swapy-dragging]:opacity-50";125return (126<div127className={cn(128opacityClass,129className130)}131data-swapy-item={id}132>133{children}134</div>135);136};
1<SwapyLayout id="swapy" >2<SwapySlot id="slot-1">3<SwapyItem id="item-1">4<DragHandle />5Item 16</SwapyItem>7<SwapyItem id="item-2">8<DragHandle />9Item 210</SwapyItem>11<SwapyItem id="item-3">12<DragHandle />13Item 314</SwapyItem>15</SwapySlot>16</SwapyLayout>
SwapyLayout
Props
Props | Type | Default | Description |
---|---|---|---|
id | string | — | A unique identifier for the layout container. |
enable | boolean | — | Whether to enable drag-and-drop (currently unused in logic). |
onSwap | (event: { newSlotItemMap: { asArray: SlotItemMapArray } }) => void | — | Callback fired when an item is swapped. |
config | Partial<{ animation: "dynamic" | "spring" | "none"; continuousMode: boolean; manualSwap: boolean; swapMode: "hover" | "drop"; autoScrollOnDrag: boolean; }> | {} | Configuration object for swapy behavior. |
className | string | — | Additional class names to apply to the layout container. |
children | React.ReactNode | — | Child components (usually SwapySlot s containing SwapyItem s). |
SwapyItem
Props
Prop | Type | Default | Description |
---|---|---|---|
id | string | — | Unique identifier for the draggable item. |
className | string | — | Additional class names. |
children | React.ReactNode | — | Content to render inside the draggable item. |
dragItemOpacity | number (10–100) | 100 | Tailwind opacity value applied when item is being dragged (e.g., 50 = 50%). |
SwapySlot
Props
Prop | Type | Default | Description |
---|---|---|---|
id | string | — | Unique identifier for the slot (drop target). |
className | string | — | Additional class names. |
children | React.ReactNode | — | Content to render inside the slot. |
DragHandle
Props
Prop | Type | Default | Description |
---|---|---|---|
className | string | — | Optional class names for the handle. |