Marble UI
← Components

Sortable List (drag and drop)

Preview: Cards

Reorder by drag, or keyboard (focus the handle, Enter, arrows, Enter). Built on React Aria drag and drop.

Import

import { SortableList } from '@marbleui/ui'

Props

<SortableList>

PropTypeDefaultDescription
aria-label *requiredstring—
children *required(item: T) => ReactNode—
items *requiredT[]—
onReorder *required(items: T[]) => void—
classNamestring—
getText(item: T) => string(i) => String(i.id)
variant'divided' | 'cards''cards'

Accessibility

Keyboard interaction, focus management and ARIA semantics come from the underlying primitives: React Aria Button · React Aria DropIndicator · React Aria GridList · React Aria GridListItem. Follow the primitive's docs for the full keyboard map.

Usage notes

List that users can reorder by dragging or with the keyboard. For a static list use ListView; for moving cards between columns use Kanban.

<SortableList aria-label="Roadmap" items={items} onReorder={setItems} getText={(i) => i.title}>
  {(i) => <span>{i.title}</span>}
</SortableList>
  • Controlled: you own items and receive the new order in onReorder. Every item needs a unique id.
  • aria-label is required. Keyboard: focus the drag handle, press Enter, move with the arrow keys, press Enter to drop.
  • getText supplies the plain-text label for each item (defaults to its id); set it so drags and screen readers get a readable name.
  • variant is cards (default) or divided.
Example source · show
import { useState } from 'react'
import { Badge } from '../badge'
import { SortableList } from './sortable-list'

const initial = [
  { id: 'research', title: 'User research', tone: 'success' as const, tag: 'Done' },
  { id: 'wireframes', title: 'Wireframes', tone: 'accent' as const, tag: 'In progress' },
  { id: 'visual', title: 'Visual design', tone: 'neutral' as const, tag: 'Todo' },
  { id: 'build', title: 'Build components', tone: 'neutral' as const, tag: 'Todo' },
  { id: 'launch', title: 'Launch', tone: 'neutral' as const, tag: 'Todo' },
]

function Demo({ variant }: { variant: 'cards' | 'divided' }) {
  const [items, setItems] = useState(initial)
  return (
    <SortableList aria-label="Roadmap" items={items} onReorder={setItems} getText={(i) => i.title} variant={variant} className="w-80">
      {(i) => (
        <div className="flex items-center justify-between gap-2">
          <span>{i.title}</span>
          <Badge tone={i.tone}>{i.tag}</Badge>
        </div>
      )}
    </SortableList>
  )
}

export const styles = { Cards: () => <Demo variant="cards" />, Divided: () => <Demo variant="divided" /> }

Source: packages/ui/src/sortable-list/sortable-list.tsx

Catalog

Category
collections
Priority
Core
Primitive
react-aria
React Aria
useDraggableCollection + useDroppableCollection