Skip to Content
Components
Sortable ALPHA

Sortable

Basic building blocks for sortable interfaces.

Documentation

Usage

Item A
Item B
Item C
"use client"; import { Sortable, SortableItem } from "@optiaxiom/react/unstable"; import { useState } from "react"; export function App() { const [items, setItems] = useState(["A", "B", "C"]); return ( <Sortable alignItems="center" fontFamily="mono" fontSize="md" fontWeight="600" items={items} onItemsChange={setItems} > {(items) => items.map((item, index) => ( <SortableItem bg="bg.avatar.neutral" index={index} item={item} key={item} p="12" rounded="sm" textAlign="center" w="224" > Item {item} </SortableItem> )) } </Sortable> ); }

Anatomy

import { Sortable, SortableGroup, SortableHandle, SortableItem, } from "@optiaxiom/react/unstable"; export default () => ( <Sortable> <SortableItem> <SortableHandle /> </SortableItem> <SortableGroup> <SortableItem /> </SortableGroup> </Sortable> );

Basic

Sortable works with a list of items provided via the items prop. Each item must be a unique string identifying the sortable item within the list.

Next we provide a render prop in children that has the same items as an argument but representing the live state as they are being sorted.

Finally we use SortableItem to render each sortable element inside the render function.

0. Item A
1. Item B
2. Item C
"use client"; import { Sortable, SortableItem } from "@optiaxiom/react/unstable"; export function App() { return ( <Sortable items={["A", "B", "C"]}> {(items) => items.map((item, index) => ( <SortableItem border="1" index={index} item={item} key={item}> {index}. Item {item} </SortableItem> )) } </Sortable> ); }

Sorting

Notice in the previous demo items are draggable but they always revert back to their original position.

We need to use either the onItemsChange or onChange prop to store the new sorted state.

Using onValueChange is the simplest way to handle sorting for smaller pieces of data where we can update the sorting rank for all items in one call.

Item A
Item B
Item C
"use client"; import { Sortable, SortableItem } from "@optiaxiom/react/unstable"; import { useState } from "react"; export function App() { const [items, setItems] = useState(["A", "B", "C"]); return ( <Sortable items={items} onItemsChange={setItems}> {(items) => items.map((item, index) => ( <SortableItem border="1" index={index} item={item} key={item}> Item {item} </SortableItem> )) } </Sortable> ); }

Using onChange is necessary in cases where we’re sorting a large number of items and we only want to mutate the item that is being dragged.

Item A
Item B
Item C
"use client"; import { Sortable, SortableItem } from "@optiaxiom/react/unstable"; import { useState } from "react"; import { calculateRank } from "./calculateRank"; export function App() { const [data, setData] = useState([ { id: "A", rank: 10_000, }, { id: "B", rank: 20_000, }, { id: "C", rank: 30_000, }, ]); return ( <Sortable items={data.toSorted((a, b) => a.rank - b.rank).map((item) => item.id)} onChange={({ items, source }) => { setData( data.map((item) => item.id === source ? { ...item, rank: calculateRank(data, source, items), } : item, ), ); }} > {(items) => items.map((item, index) => ( <SortableItem border="1" index={index} item={item} key={item}> Item {item} </SortableItem> )) } </Sortable> ); }

Drag handle

By default the whole item acts as the drag handle, but we can use the SortableHandle component to render separate drag handles within the items.

Item A
Item B
Item C
"use client"; import { Sortable, SortableHandle, SortableItem, } from "@optiaxiom/react/unstable"; import { IconGripVertical } from "@tabler/icons-react"; import { useState } from "react"; export function App() { const [items, setItems] = useState(["A", "B", "C"]); return ( <Sortable items={items} onItemsChange={setItems}> {(items) => items.map((item, index) => ( <SortableItem alignItems="center" border="1" display="flex" index={index} item={item} key={item} > <SortableHandle> <IconGripVertical size={20} /> </SortableHandle> Item {item} </SortableItem> )) } </Sortable> ); }

Multiple lists

Use the SortableGroup component to swap items between lists in addition to sorting with each other.

We also need to change the items prop to an object where the key is the ID of the group and the value is the ID of items within that list.

A
Item A1
Item A2
Item A3
B
Item B1
Item B2
C
"use client"; import { Box, Flex } from "@optiaxiom/react"; import { Sortable, SortableGroup, SortableItem, } from "@optiaxiom/react/unstable"; import { useState } from "react"; export function App() { const [items, setItems] = useState({ A: ["A1", "A2", "A3"], B: ["B1", "B2"], C: [], }); return ( <Sortable alignItems="stretch" display="grid" flexDirection="row" fontFamily="mono" fontSize="md" fontWeight="600" gridTemplateColumns="3" h="384" items={items} onItemsChange={setItems} > {(items) => Object.entries(items).map(([column, items], index) => ( <SortableGroup asChild gap="0" group={column} index={index} key={column} overflow="hidden" rounded="sm" > {(isDropTarget) => ( <Box bg={isDropTarget ? "bg.warning.subtle" : "bg.secondary"} transition="colors" > <Box bg={isDropTarget ? "bg.warning.light" : "bg.avatar.neutral"} p="12" transition="colors" > {column} </Box> <Flex flex="1" justifyContent="flex-start" p="12"> {items.map((item, index) => ( <SortableItem bg="bg.default" index={index} item={item} key={item} p="24" rounded="sm" textAlign="center" w="224" > Item {item} </SortableItem> ))} </Flex> </Box> )} </SortableGroup> )) } </Sortable> ); }

Card example

We can combine Sortable with the Card component to build a sortable list of cards.

1

Launch Scooter Beta Sign Up

A/B Test - United Kingdom or Canada or Germany

2

Age Experiment

A/B Test - Everyone: 14, 16, 18

3

Multi-Armed Bandit for Images

Multi-armed bandit - Everyone: Off, Image 1
"use client"; import { Button, EllipsisMenuButton, Flex, Text } from "@optiaxiom/react"; import { Card, CardHeader, CardLink, Menu, MenuContent, MenuTrigger, Sortable, SortableHandle, SortableItem, } from "@optiaxiom/react/unstable"; import { IconGripVertical } from "@tabler/icons-react"; import { useState } from "react"; import { data } from "./data"; export function App() { const [items, setItems] = useState(() => Object.keys(data)); return ( <Sortable items={items} onItemsChange={setItems}> {(items) => items.map((item, index) => ( <Flex flexDirection="row" key={item}> <Text color="fg.secondary" fontSize="md" w="20"> {index + 1} </Text> <SortableItem asChild index={index} item={item}> <Card flex="1"> <CardHeader addonAfter={ <Menu options={[{ label: "Edit" }]}> <MenuTrigger asChild> <EllipsisMenuButton appearance="subtle" aria-label="actions" ml="auto" /> </MenuTrigger> <MenuContent /> </Menu> } addonBefore={ <SortableHandle asChild color="fg.tertiary" transition="colors" > <Button appearance="subtle" icon={<IconGripVertical />} /> </SortableHandle> } description={data[item].description} > <CardLink href="data:,">{data[item].title}</CardLink> </CardHeader> </Card> </SortableItem> </Flex> )) } </Sortable> ); }

Props

Sortable

Supports all Box props in addition to its own. Renders a <div> element.

Prop

asChild

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

false | true

className

string

items*

An array of item IDs in controlled mode.

string[] | Record<string, string[]>

onChange

Event handler that is called when items are re-sorted with full information about the event.

(data: { items: T; source: string; }) => void

onItemsChange

Handler that is called when the item IDs are re-sorted.

(value: T) => void

SortableGroup

Supports all Box props in addition to its own. Renders a <div> element.

Prop

asChild

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

false | true

className

string

group*

ID of the group that is being rendered.

string

index*

Array index of the group that is being rendered.

number

SortableItem

Supports all Box props in addition to its own. Renders a <div> element.

Prop

asChild

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

false | true

className

string

index*

Array index of the item that is being rendered.

number

item*

ID of the item that is being rendered.

string

SortableHandle

Supports all Box props in addition to its own. Renders a <div> element.

Prop

asChild

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

false | true

className

string

Changelog

1.5.0

  • Added component
Last updated on