"use client";
import * as Sortable from "@/components/ui/sortable";
import * as React from "react";
export function SortableDemo() {
const [tricks, setTricks] = React.useState([
{
id: "1",
title: "The 900",
description: "The 900 is a trick where you spin 900 degrees in the air.",
},
{
id: "2",
title: "Indy Backflip",
description:
"The Indy Backflip is a trick where you backflip in the air.",
},
{
id: "3",
title: "Pizza Guy",
description: "The Pizza Guy is a trick where you flip the pizza guy.",
},
{
id: "4",
title: "Rocket Air",
description: "The Rocket Air is a trick where you rocket air.",
},
{
id: "5",
title: "Kickflip Backflip",
description:
"The Kickflip Backflip is a trick where you kickflip backflip.",
},
{
id: "6",
title: "FS 540",
description: "The FS 540 is a trick where you fs 540.",
},
]);
return (
<Sortable.Root
value={tricks}
onValueChange={setTricks}
getItemValue={(item) => item.id}
orientation="mixed"
>
<Sortable.Content className="grid auto-rows-fr grid-cols-3 gap-2.5">
{tricks.map((trick) => (
<Sortable.Item key={trick.id} value={trick.id} asChild asHandle>
<div className="flex size-full flex-col gap-1 rounded-md border bg-zinc-100 p-4 text-foreground shadow-sm dark:bg-zinc-900">
<div className="font-medium text-sm leading-tight sm:text-base">
{trick.title}
</div>
<span className="line-clamp-2 hidden text-muted-foreground text-sm sm:inline-block">
{trick.description}
</span>
</div>
</Sortable.Item>
))}
</Sortable.Content>
<Sortable.Overlay>
<div className="size-full rounded-md bg-primary/10" />
</Sortable.Overlay>
</Sortable.Root>
);
}
Installation
CLI
npx shadcn@latest add "https://diceui.com/r/sortable"
pnpm dlx shadcn@latest add "https://diceui.com/r/sortable"
yarn dlx shadcn@latest add "https://diceui.com/r/sortable"
bun x shadcn@latest add "https://diceui.com/r/sortable"
Manual
Install the following dependencies:
npm install @dnd-kit/core @dnd-kit/modifiers @dnd-kit/sortable @dnd-kit/utilities @radix-ui/react-slot
pnpm add @dnd-kit/core @dnd-kit/modifiers @dnd-kit/sortable @dnd-kit/utilities @radix-ui/react-slot
yarn add @dnd-kit/core @dnd-kit/modifiers @dnd-kit/sortable @dnd-kit/utilities @radix-ui/react-slot
bun add @dnd-kit/core @dnd-kit/modifiers @dnd-kit/sortable @dnd-kit/utilities @radix-ui/react-slot
Copy the refs composition utilities into your lib/compose-refs.ts
file.
/**
* @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx
*/
import * as React from "react";
type PossibleRef<T> = React.Ref<T> | undefined;
/**
* Set a given ref to a given value
* This utility takes care of different types of refs: callback refs and RefObject(s)
*/
function setRef<T>(ref: PossibleRef<T>, value: T) {
if (typeof ref === "function") {
return ref(value);
}
if (ref !== null && ref !== undefined) {
ref.current = value;
}
}
/**
* A utility to compose multiple refs together
* Accepts callback refs and RefObject(s)
*/
function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
return (node) => {
let hasCleanup = false;
const cleanups = refs.map((ref) => {
const cleanup = setRef(ref, node);
if (!hasCleanup && typeof cleanup === "function") {
hasCleanup = true;
}
return cleanup;
});
// React <19 will log an error to the console if a callback ref returns a
// value. We don't use ref cleanups internally so this will only happen if a
// user's ref callback returns a value, which we only expect if they are
// using the cleanup functionality added in React 19.
if (hasCleanup) {
return () => {
for (let i = 0; i < cleanups.length; i++) {
const cleanup = cleanups[i];
if (typeof cleanup === "function") {
cleanup();
} else {
setRef(refs[i], null);
}
}
};
}
};
}
/**
* A custom hook that composes multiple refs
* Accepts callback refs and RefObject(s)
*/
function useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
// eslint-disable-next-line react-hooks/exhaustive-deps
return React.useCallback(composeRefs(...refs), refs);
}
export { composeRefs, useComposedRefs };
Copy and paste the following code into your project.
"use client";
import {
type Announcements,
DndContext,
type DndContextProps,
type DragEndEvent,
DragOverlay,
type DragStartEvent,
type DraggableAttributes,
type DraggableSyntheticListeners,
type DropAnimation,
KeyboardSensor,
MouseSensor,
type ScreenReaderInstructions,
TouchSensor,
type UniqueIdentifier,
closestCenter,
closestCorners,
defaultDropAnimationSideEffects,
useSensor,
useSensors,
} from "@dnd-kit/core";
import {
restrictToHorizontalAxis,
restrictToParentElement,
restrictToVerticalAxis,
} from "@dnd-kit/modifiers";
import {
SortableContext,
type SortableContextProps,
arrayMove,
horizontalListSortingStrategy,
sortableKeyboardCoordinates,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Slot } from "@radix-ui/react-slot";
import * as React from "react";
import { useComposedRefs } from "@/lib/compose-refs";
import { cn } from "@/lib/utils";
import * as ReactDOM from "react-dom";
const orientationConfig = {
vertical: {
modifiers: [restrictToVerticalAxis, restrictToParentElement],
strategy: verticalListSortingStrategy,
collisionDetection: closestCenter,
},
horizontal: {
modifiers: [restrictToHorizontalAxis, restrictToParentElement],
strategy: horizontalListSortingStrategy,
collisionDetection: closestCenter,
},
mixed: {
modifiers: [restrictToParentElement],
strategy: undefined,
collisionDetection: closestCorners,
},
};
const ROOT_NAME = "Sortable";
const CONTENT_NAME = "SortableContent";
const ITEM_NAME = "SortableItem";
const ITEM_HANDLE_NAME = "SortableItemHandle";
const OVERLAY_NAME = "SortableOverlay";
interface SortableRootContextValue<T> {
id: string;
items: UniqueIdentifier[];
modifiers: DndContextProps["modifiers"];
strategy: SortableContextProps["strategy"];
activeId: UniqueIdentifier | null;
setActiveId: (id: UniqueIdentifier | null) => void;
getItemValue: (item: T) => UniqueIdentifier;
flatCursor: boolean;
}
const SortableRootContext =
React.createContext<SortableRootContextValue<unknown> | null>(null);
SortableRootContext.displayName = ROOT_NAME;
function useSortableContext(consumerName: string) {
const context = React.useContext(SortableRootContext);
if (!context) {
throw new Error(`\`${consumerName}\` must be used within \`${ROOT_NAME}\``);
}
return context;
}
interface GetItemValue<T> {
/**
* Callback that returns a unique identifier for each sortable item. Required for array of objects.
* @example getItemValue={(item) => item.id}
*/
getItemValue: (item: T) => UniqueIdentifier;
}
type SortableRootProps<T> = DndContextProps & {
value: T[];
onValueChange?: (items: T[]) => void;
onMove?: (
event: DragEndEvent & { activeIndex: number; overIndex: number },
) => void;
strategy?: SortableContextProps["strategy"];
orientation?: "vertical" | "horizontal" | "mixed";
flatCursor?: boolean;
} & (T extends object ? GetItemValue<T> : Partial<GetItemValue<T>>);
function SortableRoot<T>(props: SortableRootProps<T>) {
const {
value,
onValueChange,
collisionDetection,
modifiers,
strategy,
onMove,
orientation = "vertical",
flatCursor = false,
getItemValue: getItemValueProp,
accessibility,
...sortableProps
} = props;
const id = React.useId();
const [activeId, setActiveId] = React.useState<UniqueIdentifier | null>(null);
const sensors = useSensors(
useSensor(MouseSensor),
useSensor(TouchSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
const config = React.useMemo(
() => orientationConfig[orientation],
[orientation],
);
const getItemValue = React.useCallback(
(item: T): UniqueIdentifier => {
if (typeof item === "object" && !getItemValueProp) {
throw new Error("getItemValue is required when using array of objects");
}
return getItemValueProp
? getItemValueProp(item)
: (item as UniqueIdentifier);
},
[getItemValueProp],
);
const items = React.useMemo(() => {
return value.map((item) => getItemValue(item));
}, [value, getItemValue]);
const onDragStart = React.useCallback(
(event: DragStartEvent) => {
sortableProps.onDragStart?.(event);
if (event.activatorEvent.defaultPrevented) return;
setActiveId(event.active.id);
},
[sortableProps.onDragStart],
);
const onDragEnd = React.useCallback(
(event: DragEndEvent) => {
sortableProps.onDragEnd?.(event);
if (event.activatorEvent.defaultPrevented) return;
const { active, over } = event;
if (over && active.id !== over?.id) {
const activeIndex = value.findIndex(
(item) => getItemValue(item) === active.id,
);
const overIndex = value.findIndex(
(item) => getItemValue(item) === over.id,
);
if (onMove) {
onMove({ ...event, activeIndex, overIndex });
} else {
onValueChange?.(arrayMove(value, activeIndex, overIndex));
}
}
setActiveId(null);
},
[value, onValueChange, onMove, getItemValue, sortableProps.onDragEnd],
);
const onDragCancel = React.useCallback(
(event: DragEndEvent) => {
sortableProps.onDragCancel?.(event);
if (event.activatorEvent.defaultPrevented) return;
setActiveId(null);
},
[sortableProps.onDragCancel],
);
const announcements: Announcements = React.useMemo(
() => ({
onDragStart({ active }) {
const activeValue = active.id.toString();
return `Grabbed sortable item "${activeValue}". Current position is ${active.data.current?.sortable.index + 1} of ${value.length}. Use arrow keys to move, space to drop.`;
},
onDragOver({ active, over }) {
if (over) {
const overIndex = over.data.current?.sortable.index ?? 0;
const activeIndex = active.data.current?.sortable.index ?? 0;
const moveDirection = overIndex > activeIndex ? "down" : "up";
const activeValue = active.id.toString();
return `Sortable item "${activeValue}" moved ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;
}
return "Sortable item is no longer over a droppable area. Press escape to cancel.";
},
onDragEnd({ active, over }) {
const activeValue = active.id.toString();
if (over) {
const overIndex = over.data.current?.sortable.index ?? 0;
return `Sortable item "${activeValue}" dropped at position ${overIndex + 1} of ${value.length}.`;
}
return `Sortable item "${activeValue}" dropped. No changes were made.`;
},
onDragCancel({ active }) {
const activeIndex = active.data.current?.sortable.index ?? 0;
const activeValue = active.id.toString();
return `Sorting cancelled. Sortable item "${activeValue}" returned to position ${activeIndex + 1} of ${value.length}.`;
},
onDragMove({ active, over }) {
if (over) {
const overIndex = over.data.current?.sortable.index ?? 0;
const activeIndex = active.data.current?.sortable.index ?? 0;
const moveDirection = overIndex > activeIndex ? "down" : "up";
const activeValue = active.id.toString();
return `Sortable item "${activeValue}" is moving ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;
}
return "Sortable item is no longer over a droppable area. Press escape to cancel.";
},
}),
[value],
);
const screenReaderInstructions: ScreenReaderInstructions = React.useMemo(
() => ({
draggable: `
To pick up a sortable item, press space or enter.
While dragging, use the ${orientation === "vertical" ? "up and down" : orientation === "horizontal" ? "left and right" : "arrow"} keys to move the item.
Press space or enter again to drop the item in its new position, or press escape to cancel.
`,
}),
[orientation],
);
const contextValue = React.useMemo(
() => ({
id,
items,
modifiers: modifiers ?? config.modifiers,
strategy: strategy ?? config.strategy,
activeId,
setActiveId,
getItemValue,
flatCursor,
}),
[
id,
items,
modifiers,
strategy,
config.modifiers,
config.strategy,
activeId,
getItemValue,
flatCursor,
],
);
return (
<SortableRootContext.Provider
value={contextValue as SortableRootContextValue<unknown>}
>
<DndContext
collisionDetection={collisionDetection ?? config.collisionDetection}
modifiers={modifiers ?? config.modifiers}
sensors={sensors}
{...sortableProps}
id={id}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
onDragCancel={onDragCancel}
accessibility={{
announcements,
screenReaderInstructions,
...accessibility,
}}
/>
</SortableRootContext.Provider>
);
}
const SortableContentContext = React.createContext<boolean>(false);
SortableContentContext.displayName = CONTENT_NAME;
interface SortableContentProps extends React.ComponentPropsWithoutRef<"div"> {
strategy?: SortableContextProps["strategy"];
children: React.ReactNode;
asChild?: boolean;
withoutSlot?: boolean;
}
const SortableContent = React.forwardRef<HTMLDivElement, SortableContentProps>(
(props, forwardedRef) => {
const {
strategy: strategyProp,
asChild,
withoutSlot,
children,
...contentProps
} = props;
const context = useSortableContext(CONTENT_NAME);
const ContentPrimitive = asChild ? Slot : "div";
return (
<SortableContentContext.Provider value={true}>
<SortableContext
items={context.items}
strategy={strategyProp ?? context.strategy}
>
{withoutSlot ? (
children
) : (
<ContentPrimitive
data-slot="sortable-content"
{...contentProps}
ref={forwardedRef}
>
{children}
</ContentPrimitive>
)}
</SortableContext>
</SortableContentContext.Provider>
);
},
);
SortableContent.displayName = CONTENT_NAME;
interface SortableItemContextValue {
id: string;
attributes: DraggableAttributes;
listeners: DraggableSyntheticListeners | undefined;
setActivatorNodeRef: (node: HTMLElement | null) => void;
isDragging?: boolean;
disabled?: boolean;
}
const SortableItemContext =
React.createContext<SortableItemContextValue | null>(null);
SortableItemContext.displayName = ITEM_NAME;
function useSortableItemContext(consumerName: string) {
const context = React.useContext(SortableItemContext);
if (!context) {
throw new Error(`\`${consumerName}\` must be used within \`${ITEM_NAME}\``);
}
return context;
}
interface SortableItemProps extends React.ComponentPropsWithoutRef<"div"> {
value: UniqueIdentifier;
asHandle?: boolean;
asChild?: boolean;
disabled?: boolean;
}
const SortableItem = React.forwardRef<HTMLDivElement, SortableItemProps>(
(props, forwardedRef) => {
const {
value,
style,
asHandle,
asChild,
disabled,
className,
...itemProps
} = props;
const inSortableContent = React.useContext(SortableContentContext);
const inSortableOverlay = React.useContext(SortableOverlayContext);
if (!inSortableContent && !inSortableOverlay) {
throw new Error(
`\`${ITEM_NAME}\` must be used within \`${CONTENT_NAME}\` or \`${OVERLAY_NAME}\``,
);
}
if (value === "") {
throw new Error(`\`${ITEM_NAME}\` value cannot be an empty string`);
}
const context = useSortableContext(ITEM_NAME);
const id = React.useId();
const {
attributes,
listeners,
setNodeRef,
setActivatorNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: value, disabled });
const composedRef = useComposedRefs(forwardedRef, (node) => {
if (disabled) return;
setNodeRef(node);
if (asHandle) setActivatorNodeRef(node);
});
const composedStyle = React.useMemo<React.CSSProperties>(() => {
return {
transform: CSS.Translate.toString(transform),
transition,
...style,
};
}, [transform, transition, style]);
const itemContext = React.useMemo<SortableItemContextValue>(
() => ({
id,
attributes,
listeners,
setActivatorNodeRef,
isDragging,
disabled,
}),
[id, attributes, listeners, setActivatorNodeRef, isDragging, disabled],
);
const ItemPrimitive = asChild ? Slot : "div";
return (
<SortableItemContext.Provider value={itemContext}>
<ItemPrimitive
id={id}
data-disabled={disabled}
data-dragging={isDragging ? "" : undefined}
data-slot="sortable-item"
{...itemProps}
{...(asHandle && !disabled ? attributes : {})}
{...(asHandle && !disabled ? listeners : {})}
ref={composedRef}
style={composedStyle}
className={cn(
"focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1",
{
"touch-none select-none": asHandle,
"cursor-default": context.flatCursor,
"data-dragging:cursor-grabbing": !context.flatCursor,
"cursor-grab": !isDragging && asHandle && !context.flatCursor,
"opacity-50": isDragging,
"pointer-events-none opacity-50": disabled,
},
className,
)}
/>
</SortableItemContext.Provider>
);
},
);
SortableItem.displayName = ITEM_NAME;
interface SortableItemHandleProps
extends React.ComponentPropsWithoutRef<"button"> {
asChild?: boolean;
}
const SortableItemHandle = React.forwardRef<
HTMLButtonElement,
SortableItemHandleProps
>((props, forwardedRef) => {
const { asChild, disabled, className, ...itemHandleProps } = props;
const context = useSortableContext(ITEM_HANDLE_NAME);
const itemContext = useSortableItemContext(ITEM_HANDLE_NAME);
const isDisabled = disabled ?? itemContext.disabled;
const composedRef = useComposedRefs(forwardedRef, (node) => {
if (!isDisabled) return;
itemContext.setActivatorNodeRef(node);
});
const HandlePrimitive = asChild ? Slot : "button";
return (
<HandlePrimitive
type="button"
aria-controls={itemContext.id}
data-disabled={isDisabled}
data-dragging={itemContext.isDragging ? "" : undefined}
data-slot="sortable-item-handle"
{...itemHandleProps}
{...(isDisabled ? {} : itemContext.attributes)}
{...(isDisabled ? {} : itemContext.listeners)}
ref={composedRef}
className={cn(
"select-none disabled:pointer-events-none disabled:opacity-50",
context.flatCursor
? "cursor-default"
: "cursor-grab data-dragging:cursor-grabbing",
className,
)}
disabled={isDisabled}
/>
);
});
SortableItemHandle.displayName = ITEM_HANDLE_NAME;
const SortableOverlayContext = React.createContext(false);
SortableOverlayContext.displayName = OVERLAY_NAME;
const dropAnimation: DropAnimation = {
sideEffects: defaultDropAnimationSideEffects({
styles: {
active: {
opacity: "0.4",
},
},
}),
};
interface SortableOverlayProps
extends Omit<React.ComponentPropsWithoutRef<typeof DragOverlay>, "children"> {
container?: Element | DocumentFragment | null;
children?:
| ((params: { value: UniqueIdentifier }) => React.ReactNode)
| React.ReactNode;
}
function SortableOverlay(props: SortableOverlayProps) {
const { container: containerProp, children, ...overlayProps } = props;
const context = useSortableContext(OVERLAY_NAME);
const [mounted, setMounted] = React.useState(false);
React.useLayoutEffect(() => setMounted(true), []);
const container =
containerProp ?? (mounted ? globalThis.document?.body : null);
if (!container) return null;
return ReactDOM.createPortal(
<DragOverlay
dropAnimation={dropAnimation}
modifiers={context.modifiers}
className={cn(!context.flatCursor && "cursor-grabbing")}
{...overlayProps}
>
<SortableOverlayContext.Provider value={true}>
{context.activeId
? typeof children === "function"
? children({ value: context.activeId })
: children
: null}
</SortableOverlayContext.Provider>
</DragOverlay>,
container,
);
}
export {
SortableRoot as Sortable,
SortableContent,
SortableItem,
SortableItemHandle,
SortableOverlay,
//
SortableRoot as Root,
SortableContent as Content,
SortableItem as Item,
SortableItemHandle as ItemHandle,
SortableOverlay as Overlay,
};
Layout
Import the parts, and compose them together.
import * as Sortable from "@/components/ui/sortable";
<Sortable.Root>
<Sortable.Content>
<Sortable.Item >
<Sortable.ItemHandle />
</Sortable.Item>
<Sortable.Item />
</Sortable.Content>
<Sortable.Overlay />
</Sortable.Root>
Examples
With Dynamic Overlay
Display a dynamic overlay when an item is being dragged.
"use client";
import * as Sortable from "@/components/ui/sortable";
import * as React from "react";
interface Trick {
id: string;
title: string;
description: string;
}
export function SortableDynamicOverlayDemo() {
const [tricks, setTricks] = React.useState<Trick[]>([
{
id: "1",
title: "The 900",
description: "The 900 is a trick where you spin 900 degrees in the air.",
},
{
id: "2",
title: "Indy Backflip",
description:
"The Indy Backflip is a trick where you backflip in the air.",
},
{
id: "3",
title: "Pizza Guy",
description: "The Pizza Guy is a trick where you flip the pizza guy.",
},
{
id: "4",
title: "Rocket Air",
description: "The Rocket Air is a trick where you rocket air.",
},
{
id: "5",
title: "Kickflip Backflip",
description:
"The Kickflip Backflip is a trick where you kickflip backflip.",
},
{
id: "6",
title: "FS 540",
description: "The FS 540 is a trick where you fs 540.",
},
]);
return (
<Sortable.Root
value={tricks}
onValueChange={setTricks}
getItemValue={(item) => item.id}
orientation="mixed"
>
<Sortable.Content className="grid auto-rows-fr grid-cols-3 gap-2.5">
{tricks.map((trick) => (
<TrickCard key={trick.id} trick={trick} asHandle />
))}
</Sortable.Content>
<Sortable.Overlay>
{(activeItem) => {
const trick = tricks.find((trick) => trick.id === activeItem.value);
if (!trick) return null;
return <TrickCard trick={trick} />;
}}
</Sortable.Overlay>
</Sortable.Root>
);
}
interface TrickCardProps
extends Omit<React.ComponentPropsWithoutRef<typeof Sortable.Item>, "value"> {
trick: Trick;
}
function TrickCard({ trick, ...props }: TrickCardProps) {
return (
<Sortable.Item value={trick.id} asChild {...props}>
<div className="flex size-full flex-col gap-1 rounded-md border bg-zinc-100 p-4 text-foreground shadow-sm dark:bg-zinc-900">
<div className="font-medium text-sm leading-tight sm:text-base">
{trick.title}
</div>
<span className="line-clamp-2 hidden text-muted-foreground text-sm sm:inline-block">
{trick.description}
</span>
</div>
</Sortable.Item>
);
}
With Handle
Use ItemHandle
as a drag handle for sortable items.
Trick | Difficulty | Points | |
---|---|---|---|
The 900 | Expert | 9000 | |
Indy Backflip | Advanced | 4000 | |
Pizza Guy | Intermediate | 1500 | |
360 Varial McTwist | Expert | 5000 |
"use client";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Sortable,
SortableContent,
SortableItem,
SortableItemHandle,
SortableOverlay,
} from "@/components/ui/sortable";
import { GripVertical } from "lucide-react";
import * as React from "react";
export function SortableHandleDemo() {
const [tricks, setTricks] = React.useState([
{ id: "1", title: "The 900", difficulty: "Expert", points: 9000 },
{ id: "2", title: "Indy Backflip", difficulty: "Advanced", points: 4000 },
{ id: "3", title: "Pizza Guy", difficulty: "Intermediate", points: 1500 },
{
id: "4",
title: "360 Varial McTwist",
difficulty: "Expert",
points: 5000,
},
]);
return (
<Sortable
value={tricks}
onValueChange={setTricks}
getItemValue={(item) => item.id}
>
<Table className="rounded-none border">
<TableHeader>
<TableRow className="bg-accent/50">
<TableHead className="w-[50px] bg-transparent" />
<TableHead className="bg-transparent">Trick</TableHead>
<TableHead className="bg-transparent">Difficulty</TableHead>
<TableHead className="bg-transparent text-right">Points</TableHead>
</TableRow>
</TableHeader>
<SortableContent asChild>
<TableBody>
{tricks.map((trick) => (
<SortableItem key={trick.id} value={trick.id} asChild>
<TableRow>
<TableCell className="w-[50px]">
<SortableItemHandle asChild>
<Button variant="ghost" size="icon" className="size-8">
<GripVertical className="h-4 w-4" />
</Button>
</SortableItemHandle>
</TableCell>
<TableCell className="font-medium">{trick.title}</TableCell>
<TableCell className="text-muted-foreground">
{trick.difficulty}
</TableCell>
<TableCell className="text-right text-muted-foreground">
{trick.points}
</TableCell>
</TableRow>
</SortableItem>
))}
</TableBody>
</SortableContent>
</Table>
<SortableOverlay>
<div className="size-full rounded-none bg-primary/10" />
</SortableOverlay>
</Sortable>
);
}
With Primitive Values
Use an array of primitives (string or number) instead of objects for sorting.
"use client";
import * as Sortable from "@/components/ui/sortable";
import * as React from "react";
export function SortablePrimitiveValuesDemo() {
const [tricks, setTricks] = React.useState([
"The 900",
"Indy Backflip",
"Pizza Guy",
"Rocket Air",
"Kickflip Backflip",
"FS 540",
]);
return (
<Sortable.Root value={tricks} onValueChange={setTricks} orientation="mixed">
<Sortable.Content className="grid grid-cols-3 gap-2.5">
{tricks.map((trick) => (
<Sortable.Item key={trick} value={trick} asChild asHandle>
<div className="flex size-full flex-col items-center justify-center rounded-md border border-zinc-200 bg-zinc-100 p-8 text-center shadow-xs dark:border-zinc-800 dark:bg-zinc-900">
<div className="font-medium text-sm leading-tight sm:text-base">
{trick}
</div>
</div>
</Sortable.Item>
))}
</Sortable.Content>
<Sortable.Overlay>
{(activeItem) => (
<Sortable.Item value={activeItem.value} asChild>
<div className="flex size-full flex-col items-center justify-center rounded-md border border-zinc-200 bg-zinc-100 p-8 text-center shadow-xs dark:border-zinc-800 dark:bg-zinc-900">
<div className="font-medium text-sm leading-tight sm:text-base">
{activeItem.value}
</div>
</div>
</Sortable.Item>
)}
</Sortable.Overlay>
</Sortable.Root>
);
}
API Reference
Root
The main container component for sortable functionality.
Prop | Type | Default |
---|---|---|
onDragPending? | ((event: DragPendingEvent) => void) | - |
onDragAbort? | ((event: DragAbortEvent) => void) | - |
flatCursor? | boolean | false |
onDragCancel? | ((event: DragCancelEvent) => void) | - |
onDragEnd? | ((event: DragEndEvent) => void) | - |
onDragOver? | ((event: DragOverEvent) => void) | - |
onDragMove? | ((event: DragMoveEvent) => void) | - |
onDragStart? | ((event: DragStartEvent) => void) | - |
measuring? | MeasuringConfiguration | - |
collisionDetection? | CollisionDetection | Based on orientation:
- vertical: closestCenter
- horizontal: closestCenter
- mixed: closestCorners |
children? | ReactNode | - |
cancelDrop? | CancelDrop | - |
autoScroll? | boolean | Options | false |
accessibility? | { announcements?: Announcements | undefined; container?: Element | undefined; restoreFocus?: boolean | undefined; screenReaderInstructions?: ScreenReaderInstructions | undefined; } | - |
id? | string | React.useId() |
orientation? | "vertical" | "horizontal" | "mixed" | "vertical" |
sensors? | SensorDescriptor<any>[] | [
useSensor(MouseSensor),
useSensor(TouchSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
] |
strategy? | SortingStrategy | Automatically selected based on orientation:
- vertical: verticalListSortingStrategy
- horizontal: horizontalListSortingStrategy
- mixed: undefined |
modifiers? | Modifiers | Automatically selected based on orientation:
- vertical: [restrictToVerticalAxis, restrictToParentElement]
- horizontal: [restrictToHorizontalAxis, restrictToParentElement]
- mixed: [restrictToParentElement] |
onMove? | ((event: DragEndEvent & { activeIndex: number; overIndex: number; }) => void) | - |
getItemValue? | ((item: TData) => UniqueIdentifier) | - |
onValueChange? | ((items: TData[]) => void) | - |
value | TData[] | - |
Content
Container for sortable items. Multiple SortableContent
components can be used within a Sortable
component.
Prop | Type | Default |
---|---|---|
asChild? | boolean | false |
withoutSlot? | boolean | false |
children | ReactNode | - |
strategy? | SortingStrategy | Automatically selected based on orientation:
- vertical: verticalListSortingStrategy
- horizontal: horizontalListSortingStrategy
- mixed: undefined |
Item
Individual sortable item component.
Prop | Type | Default |
---|---|---|
asChild? | boolean | false |
asHandle? | boolean | false |
disabled? | boolean | false |
value | UniqueIdentifier | - |
Data Attribute | Value |
---|---|
[data-disabled] | Present when the item is disabled. |
[data-dragging] | Present when the item is being dragged. |
ItemHandle
A button component that acts as a drag handle for sortable items.
Prop | Type | Default |
---|---|---|
asChild? | boolean | false |
Data Attribute | Value |
---|---|
[data-disabled] | Present when the item is disabled. |
[data-dragging] | Present when the parent sortable item is being dragged. |
The component extends the base Button
component and adds the following styles:
select-none
for pointer eventscursor-grab
when not dragging (unlessflatCursor
is true)cursor-grabbing
when dragging (unlessflatCursor
is true)cursor-default
whenflatCursor
is true
Overlay
The overlay component that appears when an item is being dragged.
Prop | Type | Default |
---|---|---|
zIndex? | number | - |
wrapperElement? | string | number | symbol | - |
modifiers? | Modifiers | - |
transition? | string | TransitionGetter | - |
adjustScale? | boolean | - |
children? | ReactNode | ((params: { value: UniqueIdentifier; }) => ReactNode) | - |
dropAnimation? | DropAnimation | { sideEffects: defaultDropAnimationSideEffects({ styles: { active: { opacity: "0.4" } } }), } |
container? | Element | DocumentFragment | null | document.body |
Accessibility
Keyboard Interactions
Key | Description |
---|---|
EnterSpace | Picks up the sortable item for reordering when released, and drops the item in its new position when pressed again. |
ArrowUp | Moves the sortable item up in vertical orientation. |
ArrowDown | Moves the sortable item down in vertical orientation. |
ArrowLeft | Moves the sortable item left in horizontal orientation. |
ArrowRight | Moves the sortable item right in horizontal orientation. |
Esc | Cancels the sort operation and returns the item to its original position. |