Enhancement

Sortable

Reorder a list by dragging its items, with a mouse, a finger or the keyboard

Import
@parallelogram-js/core/components/Sortable
Selector
[data-sortable]
Stylesheet
@parallelogram-js/core/styles/draggable.css

Playground

Drag an item, or tab to one and press Space, then the arrow keys.

  • Write the brief
  • Design
  • Review
  • Publish
<ul class="sortable-list" data-sortable>
  <li>Write the brief</li>
  <li>Design</li>
  <li>Review</li>
  <li>Publish</li>
</ul>

Two lists in one group, with a grip on each card to drag it by.

  • Brief
  • Budget
  • Kick-off
<div class="sortable-board">
  <ul class="sortable-list" data-sortable data-sortable-group="board" data-sortable-handle=".sortable-grip" aria-label="To do">
    <li><span class="sortable-grip" aria-hidden="true">⠿</span> Brief</li>
    <li><span class="sortable-grip" aria-hidden="true">⠿</span> Budget</li>
  </ul>
  <ul class="sortable-list" data-sortable data-sortable-group="board" data-sortable-handle=".sortable-grip" aria-label="Done">
    <li><span class="sortable-grip" aria-hidden="true">⠿</span> Kick-off</li>
  </ul>
</div>

Each item carries a hidden input, so submitting sends the order as it stands.

  1. Brief
  2. Design
  3. Review
<form method="post" action="/tasks/order">
  <ol class="sortable-list" data-sortable>
    <li>Brief <input type="hidden" name="order[]" value="12"></li>
    <li>Design <input type="hidden" name="order[]" value="13"></li>
    <li>Review <input type="hidden" name="order[]" value="14"></li>
  </ol>
  <button type="submit" class="btn">Save order</button>
</form>

About

The container's children are the items, or the elements data-sortable-items selects. A drag starts once a mouse has moved a few pixels, or after a short hold on touch, so a click on a link or button inside an item still works and a quick swipe still scrolls; data-sortable-handle limits it to a part of each item. A copy follows the pointer while a placeholder shows where the item will land, and the page or the list scrolls when the pointer nears its edge.

From the keyboard, Tab reaches each item, Space or Enter picks it up, the arrow keys move it and Space or Enter drops it; Escape puts it back where it started. Each step is announced to screen readers, and every announcement can be reworded or translated with an attribute or once on Sortable.defaults.

Containers with the same data-sortable-group share their items, for a board of columns. sortable:change fires on each list a drop changed once the item is in place, with the list's items in order — the event to save on. An <input type="hidden"> in each item puts the new order in a form without any script.

Usage

Register the component and the framework loads it the first time a page contains [data-sortable].

import { Parallelogram } from '@parallelogram-js/core';

const app = Parallelogram.create();
app.components.add('[data-sortable]', () => import('@parallelogram-js/core/components/Sortable'));
app.run();

The framework is optional. Sortable.enhanceAll() mounts the component on every matching element by itself, for a page that wants one enhancement rather than a framework:

import Sortable from '@parallelogram-js/core/components/Sortable';

Sortable.enhanceAll();

It takes a selector and the component's options, and returns the instance it mounted them with. What it leaves out is what the framework adds: loading a component only on the pages that use it, mounting again after a page swap, and ordering with dependsOn.

Its styles come from the package stylesheet:

@import '@parallelogram-js/core/styles/draggable.css';

Acknowledgements

Sortable is built on Shopify Draggable, used under the MIT licence. Copyright (c) 2018-present Shopify.

The drag engine, its Mirror, Scrollable and Focusable plugins, and the Sortable, Swappable and Droppable behaviour are ported from Draggable 1.2. A single Pointer Events sensor replaces its mouse, touch and force-touch sensors, the keyboard sensor is new, and state is written to data attributes rather than classes. Thank you to Shopify and Draggable's contributors for the work this builds on.

Attributes

NameTypeDefaultDescription
data-sortablerequiredflagnoneMarks the container
data-sortable-groupstringnoneContainers with the same group share one drag, so items move between them; the first one mounted sets the options For every instance, Sortable.defaults.group.
data-sortable-itemsselectornoneThe elements that can be dragged; the container's children when not set For every instance, Sortable.defaults.items.
data-sortable-handleselectornoneThe part of an item that starts a drag, such as a grip; give it touch-action: none so a touch on it drags rather than scrolls For every instance, Sortable.defaults.handle.
data-sortable-distancenumber4Pixels a mouse or pen moves before a drag starts, so a click on a link or button inside an item still clicks it For every instance, Sortable.defaults.distance.
data-sortable-touch-delaynumber100Milliseconds a finger rests on an item before a drag starts; moving sooner scrolls the page instead For every instance, Sortable.defaults.touchDelay.
data-sortable-axisboth | x | ybothWhich way the copy under the pointer may move For every instance, Sortable.defaults.axis.
data-sortable-disabledbooleanfalseStops new drags from starting in this container while set
data-sortable-picked-upstringPicked up {item}.Announced when an item is picked up; {item} is its label For every instance, Sortable.defaults.pickedUp.
data-sortable-keyboard-hintstringArrow keys move it, Space drops it, Escape cancels.Announced after the pick-up message when the keyboard picked the item up For every instance, Sortable.defaults.keyboardHint.
data-sortable-movedstringPosition {position} of {total}.Announced after each move; {item}, {position} and {total} For every instance, Sortable.defaults.moved.
data-sortable-droppedstringDropped {item}.Announced when an item is dropped For every instance, Sortable.defaults.dropped.
data-sortable-cancelledstringCancelled. {item} is back where it started.Announced when a drag is cancelled with Escape For every instance, Sortable.defaults.cancelled.
data-sortable-stateset by the componentidle | active | overnoneactive while a drag in its group is in progress, over while the item is over this container
data-sortable-item-stateset by the componentplaceholder | mirror | placednoneOn the placeholder showing where the item will land, the copy following the pointer, and an item just dropped On item.

Methods

NameSignatureDescription
cancel(element: HTMLElement) => voidCancel a drag in progress in the element’s group, putting the item back

Events

NameDetailDescription
sortable:start{ element: HTMLElement; item: HTMLElement; placeholder: HTMLElement; reverted: boolean; pointerType: string | null }An item is being picked up; preventDefault() leaves it where it is DOM event and event bus Cancelable.
sortable:sort{ element: HTMLElement; item: HTMLElement; placeholder: HTMLElement; reverted: boolean; pointerType: string | null; over: HTMLElement | undefined }The item is about to move past another; preventDefault() stops it DOM event and event bus Cancelable.
sortable:sortedoldIndex: number; newIndex: number; oldContainer: HTMLElement; newContainer: HTMLElementThe placeholder has moved; the drop is still to come DOM event and event bus
sortable:stop{ element: HTMLElement; item: HTMLElement; placeholder: HTMLElement; reverted: boolean; pointerType: string | null; oldIndex: number; newIndex: number; oldContainer: HTMLElement; newContainer: HTMLElement; changed: boolean; items: HTMLElement[] }The drag has ended and the item is in place, dispatched on each container it changed; items are that container’s items in order DOM event and event bus
sortable:change{ element: HTMLElement; item: HTMLElement; placeholder: HTMLElement; reverted: boolean; pointerType: string | null; oldIndex: number; newIndex: number; oldContainer: HTMLElement; newContainer: HTMLElement; changed: boolean; items: HTMLElement[] }As sortable:stop, only when the item moved: the one to save on DOM event and event bus

CSS custom properties

NameDefaultDescription
--drag-over-bgvar(--color-hover)Background of a container the item is over
--drag-placeholder-opacity0.4Opacity of the placeholder where the item will land
--drag-placeholder-outline2px dashed var(--color-accent)Outline of the placeholder
--drag-mirror-shadowvar(--surface-dropdown-shadow)Shadow of the copy following the pointer
--drag-mirror-opacity0.95Opacity of the copy following the pointer
--drag-mirror-z-index2147483000Stacking of the copy following the pointer
--drag-placed-colorrgba(37, 99, 235, 0.15)Colour a dropped item fades from; none under prefers-reduced-motion
--drag-placed-duration0.6sHow long a dropped item takes to fade back