Sortable
Basic building blocks for sortable interfaces.
#
Documentation
#
#
Usage
#
"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.
"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.
Only use either onItemsChange
or onChange
. Do not use both props on the
same component.
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.
"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.
App.tsx
"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.
"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.
"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
2
Age Experiment
3
Multi-Armed Bandit for Images
App.tsx
"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.
|
className
|
items* An array of item IDs in controlled mode.
|
onChange Event handler that is called when items are re-sorted with full information about the event.
|
onItemsChange Handler that is called when the item IDs are re-sorted.
|
#
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.
|
className
|
group* ID of the group that is being rendered.
|
index* Array index of the group that is being rendered.
|
#
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.
|
className
|
index* Array index of the item that is being rendered.
|
item* ID of the item that is being rendered.
|
#
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.
|
className
|
#
Changelog
#
#
1.5.0
#
- Added component