Lets users sort an array of sibling elements via drag and drop.

Functionality based on dndkit's useSortable, which is implemented as a basic use-case component, and allows for:

  • horizontal, vertical and grid sort
  • a11y controls

(!) This version of the component is a primitive and not to be used by itself directly in core. It is meant as a utility layer so as to not repeat code relevant to interacting with useSortable.

All implementations require:

  • the .Root level component to be passed an array of sortableIds on which to apply the sorting. Additionally, requires an onSortChange function to call so that the implementation receives the new order of items to manually render appropriately.
  • the .Item level component to be passed in a unique (to its siblings) id, the same id which coresponds to it from the sortableIds array.
  • the .Handle level component to be passed in the same targetId as the id of the .Item it controls.

The entire Sortable.Item can act as a draggable Item button itself by pasing the isDragHandle prop to it, however, it is usually prefered to control Sortable.Item via a nested Sortable.Handle button so as to allow for any content in the Item itself and avoid nested buttons which is invalid HTML.

Example usage

const sortableItems = [
  { text: 'A', id: 1, disabled: true, hasHandle: true },
  { text: 'B', id: 2, disabled: true },
  { text: 'C', id: 3, hasHandle: true },
  { text: 'D', id: 'd' }
]
const [sortableIdsCurrentOrder, setSortableIdsCurrentOrder] = React.useState(
  sortableItems.map(({ id }) => id)
)
<Sortable.Root
  sortableIds={sortableIdsCurrentOrder}
  onSortChange={({ order }) => {
    setSortableIdsCurrentOrder(order)
  }}
>
  {sortableIdsCurrentOrder.map((sortedId) => {
    const sortableItem = sortableItems.find(({ id }) => id === sortedId)
    if (!sortableItem) return null
    return (
      <Sortable.Item
        key={sortableItem.id}
        id={sortableItem.id}
        disabled={!sortableItem.hasHandle && sortableItem.disabled}
        isDragHandle={!sortableItem.hasHandle}
        asChild
      >
        <div className="flex">
          {sortableItem.hasHandle && (
            <Sortable.Handle
              targetId={sortableItem.id}
              disabled={sortableItem.hasHandle && sortableItem.disabled}
            />
          )}
          {sortableItem.text}
        </div>
      </Sortable.Item>
    )
  })}
</Sortable.Root>

API Reference

Sortable.Root
PropTypeDefaultRequired
onSortChange(onSortChangeData: { order: UniqueIdentifier[]; oldIndex: number; newIndex: number; }) => void-
sortableIds(string | number)[]-
Sortable.Item
PropTypeDefaultRequired
asChild
boolean
-
className
string
--
disabled
boolean
--
id
string | number
-
isDragHandle
boolean
-
style
Pick<HTMLAttributes<HTMLDivElement>, "style">
--
Sortable.Handle
PropTypeDefaultRequired
appearance
"simple" | "outline" | "solid" | Partial<Record<Breakpoint, "simple" | "outline" | "solid">>
--
asJSX.IntrinsicElements--
hasTooltip
boolean
--
href
string
--
isDragging
boolean
--
isRounded
boolean
--
label
string
drag handle-
onClick
MouseEventHandler<HTMLElement>
--
size
"xs" & Partial<Record<Breakpoint, "sm" | "md" | "lg">> | "sm" & Partial<Record<Breakpoint, "sm" | "md" | "lg">> | "md" & Partial<Record<Breakpoint, "sm" | "md" | "lg">> | "lg" & Partial<Record<Breakpoint, "sm" | "md" | "lg">> | Partial<Record<Breakpoint, "xs" | "sm" | "md" | "lg">> & "sm" | Partial<Record<Breakpoint, "xs" | "sm" | "md" | "lg">> & "md" | Partial<Record<Breakpoint, "xs" | "sm" | "md" | "lg">> & "lg" | Partial<Record<Breakpoint, "xs" | "sm" | "md" | "lg">> & Partial<Record<Breakpoint, "sm" | "md" | "lg">> | "sm" | "md" | "lg"
--
targetId
string | number
-
theme
"neutral" | "primary" | "primaryDark" | "success" | "warning" | "danger" | "white" | Partial<Record<Breakpoint, "neutral" | "primary" | "primaryDark" | "success" | "warning" | "danger" | "white">>
--
tooltipSide
"left" | "right" | "top" | "bottom"
--