Skip to main content

Module sortable

Module sortable 

Source
Expand description

§Sortable API reference

The stable-ID sortable kernel composes with the shared drag core: SortableProvider, SortableGroup, SortableCollection, SortableItem, stable-ID ReorderEvent<K>, layout strategies, and variable-size layout projection. The original self-contained SortableList and SortableGrid remain as index-based compatibility components for concise single-list call sites.

Concept guide: docs/concepts/sortable-lists.md. SortableGrid, cell_of and index_of live in the sibling grid module; everything on this page is re-exported through the prelude except pointer_target (reach it as dioxus_dnd::sortable::pointer_target).

§Stable-ID sortable kernel

Use one SortableProvider<K> around one or more groups. K is the unique semantic item identity. The collection adapter requires Eq + Hash so it can reject duplicate IDs; direct group/item composition requires PartialEq and still requires the caller to keep IDs unique. Every item is both a core Draggable<SortablePayload<K>> and DropZone<SortablePayload<K>>, so sortables inherit core keyboard dragging, collision and release policy, monitors, effect negotiation, drag handles, world composition, and cross-group delivery.

let mut items = use_signal(|| vec![CardId(10), CardId(20), CardId(30)]);
rsx! {
    SortableProvider::<CardId> {
        SortableCollection::<CardId> {
            id: SortableGroupId::new(1),
            items: items(),
            item_key: move |id: CardId| id.0.to_string(),
            strategy: SortStrategy::LinearVertical,
            activation: ActivationPolicy::handle(
                ActivationConstraint::Distance(6.0)
            ),
            render: move |id| rsx! {
                SortableHandle { label: "Move card", "Move" }
                CardRow { id }
            },
            on_reorder: move |event: ReorderEvent<CardId>| {
                apply_reorder(&mut items.write(), &event);
            },
        }
    }
}

SortableCollection is the concise items + render adapter:

PropTypeDefaultWhat it does
itemsVec<K>requiredStable item ids in visual order.
item_keyCallback<K, String>requiredStable, unique Dioxus key for each item. Duplicate item ids or render keys are rejected with a clear panic.
renderCallback<K, Element>requiredRenders one item by id.
on_reorderEventHandler<ReorderEvent<K>>requiredReports identity, source and destination groups, and placement intent.
idOption<SortableGroupId>stable auto idStable group identity used for cross-group intent.
strategySortStrategyLinearVerticalLinearVertical, LinearHorizontal, GridInsert, or GridSwap.
activationOption<ActivationPolicy>core defaultActivation policy forwarded to every item’s core draggable.

ReorderEvent<K> carries active, over, from_group, to_group, and placement. over: Some(id) targets an item; over: None targets the group background and means append. Placement is Before, After, or On; GridSwap uses On for pointer and keyboard drops alike. Events describe intent without exposing mutable indexes. Every group registers a background target, so both input modes can append to populated groups and an initially empty group remains reachable by keyboard.

SortableGroupId::new(value) creates an explicit application ID below 2^32. Automatic IDs use a disjoint namespace at or above 2^32, so an omitted ID cannot collide with an explicit one.

apply_reorder(&mut Vec<K>, &ReorderEvent<K>) -> bool applies same-group insertions by stable identity and returns whether it changed the vector. Cross-group events are deliberately left to the owning model because source and target collections may live in different stores.

For custom layouts, SortableGroup<K> is the public provider and layout boundary. It takes the same id, strategy, activation, and on_reorder props plus children; render keyed SortableItem<K> children directly:

SortableGroup::<CardId> {
    on_reorder,
    class: "custom-grid",
    for (position, id) in items().into_iter().enumerate() {
        SortableItem::<CardId> {
            key: "{id.0}",
            id,
            position,
            CardRow { id }
        }
    }
}

position is the item’s current zero-based visual position. It makes keyboard intent deterministic without inspecting opaque Dioxus children: for insertion strategies, moving from a later item to an earlier one yields Before, the reverse yields After, and cross-group item targets use Before. A group background supplies explicit append intent. GridSwap always yields On. The group context remains private implementation detail; the components are the supported composition seam. SortableHandle is a semantic alias for the core DragHandle.

§Variable-size layout projection

project_layout(items, rects, active, target) is the pure preview seam. It maps ids to the measured slots they would occupy after insertion and returns Vec<ItemTransform<K>> with per-id x and y translations. It reads real rect origins rather than multiplying by a uniform row pitch, so variable height rows, gaps, and virtualized measurements can share the same policy. DropPlacement<K> supplies over: Option<K> and Placement; None projects the active item into the append slot.

§Index-based compatibility components

The APIs below remain source-compatible. They own their gesture state and emit SortEvent { from, to }; use the stable-ID kernel when sortables need to share core policy or move between containers. Their optional index-key fallback exists only for 3.x compatibility and is safe only for stateless, position-identified rows. Stateful, focusable, or reordered domain items must supply item_key.

let mut items = use_signal(|| vec!["a".to_string(), "b".into(), "c".into()]);
rsx! {
    SortableList {
        len: items.read().len(),
        item_key: move |ix| items.read()[ix].clone(),
        render: move |ix: usize| rsx! { li { "{items.read()[ix]}" } },
        on_sort: move |ev: SortEvent| apply_sort(&mut items.write(), ev),
    }
}

§SortableList

A list whose items drag to reorder. Data-agnostic: it renders one wrapper div per index inside a root div (arbitrary attributes forward to the root), measures the wrappers, runs the pointer gesture, and emits a SortEvent on drop. Headless: the component ships behavior plus data-* styling hooks; you compose the looks. Rows slide to preview the drop by default; opt into a floating, caller-composed ghost with overlay.

PropTypeDefaultWhat it does
lenusizerequiredNumber of items.
renderCallback<usize, Element>requiredRenders the item at the given index.
on_sortEventHandler<SortEvent>requiredFired when the user drops an item at a new position.
axisAxisVerticalWhich axis rows are laid out (and shifted) along.
live_previewbooltrueOpen a live gap where the drop would land by translating the rows in between. false keeps rows still; style the hovered slot via data-drop-target.
transition_msu32160Duration of the row-slide transition during live preview, in milliseconds.
overlayOption<Callback<usize, Element>>NoneOpt-in floating ghost: renders overlay(index) pinned to the pointer, sized to the picked-up row’s measured rect, and hides the in-flow row so its slot reads as the drop gap. Keep it lightweight; it is your content, not a clone of the row.
touch_handleboolfalseConfine pointer drags to a leading grip instead of the whole row. The grip carries touch-action: none; the rest of the row keeps scrolling by finger. Style it via [data-sort-handle].
touchTouchSenseAutoHow a finger shares whole rows with native scrolling. Auto keeps vertical swipes scrolling and picks a row up on a short hold or a sideways pull; Immediate makes any 8px travel drag. Ignored under touch_handle, where the grip owns every touch.
handleOption<Callback<usize, Element>>NoneContent for the touch_handle grip, keyed by index. Defaults to a braille-dots glyph.
item_keyOption<Callback<usize, String>>index text (compatibility only)Stable render identity for the item currently at each index. Required in practice when rows contain hook state, focus, or mounted handles and the backing collection can reorder.

Data attributes, present while true and absent otherwise:

AttributeWherePresent while
data-draggingitem wrapperthis row is being dragged
data-drop-targetitem wrapperthis row is the hovered landing slot (never the source row)
data-dnd-motionitem wrapperalways; marks the sliding element for the crate’s prefers-reduced-motion override
data-sort-handlegrip spanalways, under touch_handle; the styling hook for the grip
data-sort-contentcontent divalways, under touch_handle; wraps your render output

Behavior:

  • Live preview. Rows between the source and the hovered target shift by one slot pitch (the measured distance between consecutive row origins, so CSS margins and gaps count) to close the source’s slot, and the source row translates to the target slot, so every slot stays filled by exactly one box. A row is adopted as the target only once the pointer crosses its midpoint in the travel direction; hovering the source row or leaving the list keeps the previous target.
  • Measurement. Rows are measured at mount and re-measured at every drag start, so hit-testing runs against current slots. Mid-drag scrolls that ping the rect-refresh channel (an AutoScroll above, or refresh_all() from use_rect_refresh()) shift the cached slots by the wrapper’s measured movement instead of re-measuring rows whose transforms are mid-transition.
  • Commit and cancel. A release outside the bounding box of the measured rows cancels: no event fires. Inside it, the drop emits SortEvent { from, to } only when from != to. All internal drag state clears before on_sort runs, so the re-render your handler triggers never sees a stale preview.
  • Overlay. The ghost renders only when the source row’s rect was measured and both press and pointer positions are known; otherwise the row simply slides, so nothing ever disappears. While the ghost shows, the in-flow row is opacity: 0 but keeps translating; its invisible box is the gap.
  • touch_handle layout. The row wrapper becomes a flex row: the grip span, then a data-sort-content div (flex: 1 1 auto) around your content.
  • Robustness. While a drag is in flight without native pointer capture (capture engages with the web feature), an invisible full-viewport layer keeps move events flowing to the list; a mouse released off-list is recovered when it returns with no button held. Android’s long-press context menu is suppressed mid-gesture only.

§SortableGrid

A grid of tiles reordered (or swapped) by dragging: dashboards, tile galleries, icon views. A grid is a flat Vec displayed in cols columns; dropping a tile onto another either inserts (everything reflows, like a photo gallery) or swaps (tiles trade places, like a dashboard) depending on which apply function you pair with ReorderMode. It reuses the sortable vocabulary: drops emit SortEvents you apply with apply_sort or apply_swap.

PropTypeDefaultWhat it does
lenusizerequiredNumber of tiles.
colsusizerequiredNumber of columns.
renderCallback<usize, Element>requiredRenders the tile at the given index.
on_sortEventHandler<SortEvent>requiredFired when the user drops a tile on another.
modeReorderModeInsertInsert-and-reflow (gallery) or swap (dashboard). Changes no behavior; it renders as data-mode so the two feels can style differently. Pair Insert with apply_sort, Swap with apply_swap.
item_classOption<String>NoneClasses for each tile’s wrapper div, the element that carries data-dragging / data-drop-target.
item_keyOption<Callback<usize, String>>index text (compatibility only)Stable render identity for the item currently at each index. Supply it for stateful or focusable tiles whose backing collection can reorder.

The root renders display: grid; grid-template-columns: repeat(cols, 1fr) and forwards arbitrary attributes. A forwarded style merges after that default, so per-property overrides win (custom tracks via style: "grid-template-columns: 2fr 1fr 1fr;") while display: grid stays; spacing needs no override at all (class: "gap-2").

AttributeWherePresent while
data-moderootalways; valued "insert" or "swap"
data-draggingtile wrapperthis tile is being dragged
data-drop-targettile wrapperthis tile is hovered as the target (never the source)

Behavior:

  • The hovered tile is simply the one whose rect contains the pointer - no midpoint hysteresis, since tiles do not shift while you hover in swap/insert grids. A move over a tile element itself also falls back to that tile’s index when the pointer misses every measured rect.
  • Tile wrappers carry touch-action: none; grids rarely need to scroll by dragging across their own tiles. Mouse, touch and pen use the same gesture machine as Draggable, so the browser never creates a native drag image and any 8px travel drags.
  • Tiles never transform mid-drag, so a rect-refresh ping triggers a plain re-measure (lists re-anchor instead).
  • A release outside the tiles’ bounding box cancels, and drag state clears before on_sort runs. The same capture-substitute layer, lost-release recovery and context-menu suppression as SortableList apply.
  • The grid anchors the reduced-motion stylesheet once for its whole subtree, so animated tiles (FlipItem and friends) inherit the override without wiring of their own.

§SortEvent

“Move the item at from so it ends up at index to.”

FieldTypeMeaning
fromusizeThe index the item was picked up from.
tousizeThe index the item ends up at in the post-move collection.

Non-exhaustive, so reorder context can be added without a major release. Synthesize your own events (reorder buttons, tests) with SortEvent::new(from, to).

§ReorderMode

What a completed reorder gesture means. Insert (the default) removes the item and inserts it at the target index, the list reorder; Swap exchanges the two items’ positions, the grid or tile swap. On SortableGrid it only sets data-mode; the apply function you call carries the semantics.

§Axis

Layout direction of a SortableList: Vertical (the default) or Horizontal. Decides whether the midpoint test and the preview slide use the Y axis or the X axis.

§Applying events

  • apply_sort(&mut Vec<T>, SortEvent) removes the item at from and inserts it at to: the standard list reorder. No-op when from == to or either index is out of range.
  • apply_swap(&mut [T], SortEvent) exchanges the two positions instead, for fixed-slot layouts. Same out-of-range guards; works on any mutable slice.

§Preview and hit-testing primitives

Both pure, public for custom surfaces and tests.

  • displacement(ix, from, over, step) -> f64 is the live-preview offset in CSS px along the list axis for the row at ix while row from is dragged over row over, with step the slot pitch. Rows between the two indices shift by step to close the source slot, and the source row itself translates to the target slot; the offsets sum to zero, so every slot stays filled. Assumes uniform row sizes for the source’s travel distance.
  • pointer_target(rects, from, current, at, axis) -> Option<usize> resolves which row should be the drop target while a drag from from hovers at at, given per-row rects measured at drag start (the stable, pre-displacement layout). A row is adopted only once the pointer crosses its center in the travel direction; over the source row or outside every rect it returns current unchanged. Not in the prelude; call dioxus_dnd::sortable::pointer_target.

§Grid coordinates

Provided for custom layouts and keyboard grid navigation:

  • cell_of(index, cols) -> (usize, usize) gives the (row, col) of a flat index. cols is clamped to at least 1.
  • index_of(row, col, cols, len) -> Option<usize> gives the flat index of (row, col), or None when col >= cols or the index reaches past len.

§Where the rest lives

ReorderButtons, the no-gesture reorder fallback emitting the same SortEvent: docs/api/accessibility.md. TouchSense details: docs/concepts/touch-and-input.md. AutoScroll and the rect-refresh channel: docs/api/autoscroll.md. Cross-container moves: docs/api/boards.md.

Re-exports§

pub use crate::sortable_kernel::SortableCollection;
pub use crate::sortable_kernel::SortableCollection;
pub use crate::sortable_kernel::SortableGroup;
pub use crate::sortable_kernel::SortableGroup;
pub use crate::sortable_kernel::SortableHandle;
pub use crate::sortable_kernel::SortableHandle;
pub use crate::sortable_kernel::SortableItem;
pub use crate::sortable_kernel::SortableItem;
pub use crate::sortable_kernel::SortableProvider;
pub use crate::sortable_kernel::SortableProvider;
pub use SortableList_completions::Component::SortableList;

Structs§

DropPlacement
ItemTransform
ReorderEvent
SortEvent
“Move the item at from so it ends up at index to.”
SortableGroupId
SortableListProps
Properties for the SortableList component.
SortablePayload

Enums§

Axis
Layout direction of the list - decides whether the midpoint test uses the Y axis (vertical lists) or the X axis (horizontal ones).
Placement
ReorderMode
What a completed reorder gesture means.
SortStrategy

Functions§

SortableCollection
Render a complete stable-ID collection with one item wrapper per id. Use SortableGroup and SortableItem directly for custom layouts.
SortableGroup
Provide one stable-ID sortable group and a layout boundary for its SortableItem children.
SortableHandle
Semantic alias for the general core drag handle in sortable UIs. An accessible button that activates the nearest handle-only [super::Draggable].
SortableItem
One stable-ID sortable item. Use directly inside a custom group layout, or let SortableGroup create it for every item.
SortableList
A list whose items can be dragged to reorder.
SortableProvider
Shared provider for one or more sortable groups. Place sibling groups under the same provider to enable cross-group moves.
apply_reorder
Apply a same-list stable-ID reorder.
apply_sort
Apply a SortEvent to a Vec in place.
apply_swap
Apply a SortEvent as a swap: the two items exchange positions.
displacement
The live-preview offset (CSS px along the list axis) for the row at ix while row from is dragged over row over - the mid-drag preview dnd-kit and react-beautiful-dnd made the baseline expectation.
pointer_target
Which row should be the drop target while a pointer drag from row from hovers at at, given per-row rects measured at drag start (so the test runs against the stable, pre-displacement layout). A row is adopted only once the pointer crosses its center in the travel direction, and while the pointer is over the source row or outside every rect, the previous target is kept. Pure, for testability.
project_layout
Project a stable-ID insertion by mapping each item to the measured slot it would occupy after the move. Because slots come from real rects, this does not assume a uniform row pitch.