A wider screen, please

This page features interactive demos and side-by-side code samples that need at least 340 pixels of horizontal space. Please widen your browser window, or rotate your device, to continue.

Morpheus v0.1.0
v0.1.0

Sortable#

Component <neo-sortable> is a drag-to-reorder container for its direct element children as a column, row, or grid. Pointer, touch, and a full keyboard contract drive the same list. A child marked data-neo-sortable-handle becomes the grip; absent that, the whole item is draggable. Give every item a stable id so neo-sortable-start / -move / -end events identify which item moved and its from / to position.

Default
Sortable list
Whole-item drag
Drag-free control
Horizontal row
Grid
Unbounded
Scrollable parent
Custom move easing
Custom placement indicator
Disabled
Deactivate Rename Duplicate
Delete
New state Toggle autoplay
Design
Build
Test
Ship
HTML CSS

The selected state's markup and its scoped CSS. The CSS is wrapped in <style>@scope { ... }</style> and applies to the preview only. Edits change the preview above, applied live while Sync is on, or when you click Patch or Replace. Patch morphs the preview; Replace swaps it outright.

Replace Patch

Signals

Datastar signals declared in the selected state's markup. Change a value to drive the preview, applied live while Sync is on, or when you click Patch.

Patch

Reference#

Attributes #

NameTypeDefaultDescription
orientation"vertical" | "horizontal" | "grid" observed"vertical"Constrains pointer travel and the reorder axis. vertical is a column, horizontal a row, grid a 2-D auto-fill grid.
disabledboolean observedfalseFreezes reordering and removes the keyboard grip (grips become tabindex=-1).
unboundedbooleanfalseBy default the floating dragged element is clamped to the host's bounding box so an item can't be dragged outside the list (the lower bound wins if an item is larger than the host). unbounded opts out, letting it follow the pointer anywhere on the page.
easingstringtheme easingTransition for the displaced (non-dragged) items as they glide to their new slot. Absent → the theme easing (--neo-easing) at --neo-sortable-move-duration. Empty string → no animation (instant). A bare duration (220ms) pairs with --neo-easing; a full pair (220ms ease-in-out) is used verbatim.
tolerate-reorderbooleanfalseControls how an in-flight drag survives an external morph (e.g. a Datastar fat-morph). By default a drag is kept alive only by a no-op morph: same item ids, same per-id content, same order of the other rows. tolerate-reorder loosens that to the same id set: an order-only morph is treated as a no-op and the gesture continues. Any added, removed, or swapped id still cancels the drag.

States #

NameDescription
:state(active)Set while a reorder is in flight, whether pointer- or keyboard-driven.
:state(pointer)Set while the in-flight reorder is pointer-driven; carries the grabbing cursor, which a keyboard reorder must not.

Slots #

NameDescription
defaultThe sortable items: the host's direct element children. Give each a stable id (or data-neo-sortable-id) so the reorder events can name the moved item. Mark a child data-neo-sortable-ignore to exclude it from the set.
[data-neo-sortable-item]Opts an item into the default styling (a bordered row with the handle leading). The Templ SortableItem wrapper adds it automatically; in raw HTML it is optional, drop it to style items yourself.
[data-neo-sortable-handle]Optional drag grip inside an item. With a handle, only that subtree starts a drag; without one the whole item is the grip.
[data-neo-sortable-nodrag]Marks a region inside a whole-item grip that must not start a drag, so pressing it interacts normally. Native controls (links, buttons, form fields, contenteditable) are already exempt; use this for anything else. Ignored when the item has an explicit data-neo-sortable-handle.
template[data-neo-sortable-placeholder]Optional <template> whose content replaces the default dashed placement indicator (the gap shown where the dragged item will land). The gap keeps the dragged item's size; the slotted markup renders inside it.

Events #

NameDetailBubblesDescription
neo-sortable-start{ id: string, index: number }yesFires when a drag (pointer or keyboard pick-up) begins. index is the picked-up item's position.
neo-sortable-move{ id: string, from: number, to: number }yesFires on every reorder during the gesture: item id moved from index from to index to.
neo-sortable-end{ id: string, from: number, to: number, changed: boolean, aborted?: boolean }yesFires when the drag ends (drop, cancel, or blur). from / to are the item's start and final positions; changed is false when they match or the gesture was cancelled. An external patch that aborts an in-flight drag sets aborted: true with to: -1.

CSS variables #

NameDefaultDescription
--neo-sortable-gap0.5remGap between items.
--neo-sortable-grid-min7remMinimum column width in grid orientation.
--neo-sortable-move-duration180msDefault glide duration for displaced items when the easing attribute is absent.
--neo-sortable-drag-shadowvar(--neo-card-shadow)Box shadow on the lifted item.
--neo-sortable-drag-ringinset 0 0 0 2px var(--accent, #6366f1)Ring on the lifted item, kept legible on dark backgrounds.
--neo-sortable-handle-colorvar(--muted, currentColor)Handle color at rest.
--neo-sortable-handle-hover-colorvar(--page-fg, currentColor)Handle color on hover or focus.
--neo-sortable-placeholder-colorcolor-mix(in srgb, var(--accent, #6366f1) 55%, transparent)Color of the dashed placement indicator.

Keyboard#

Every reorder works without a pointer. Tab to a drag grip (each is a tab stop), press Space or Enter to pick the item up, then move it:

  • Space / Enter: pick up the focused item, then drop it once it is in place.
  • Arrow keys: move the held item one slot along the orientation; in a grid, ArrowUp / ArrowDown jump a whole row.
  • Home / End: send the held item to the first or last position.
  • Tab / blur: drop and commit the new order. Escape cancels and restores the original order.

The first move, every subsequent move, the final position, and cancellation are announced through an aria-live region; the moving item carries aria-grabbed and grips expose aria-keyshortcuts. The disabled attribute drops the grips from the tab order.

Examples#

Sortable list#

A vertical list with a leading drag handle on each row. Only the handle is the grip, so text inside the row stays selectable and any buttons keep working.

Laptop
Phone
Tablet

Whole-item drag#

With no data-neo-sortable-handle child, the entire item is the grip: press anywhere on a row and drag.

Red
Green
Blue

Drag-free control#

The whole row is the grip, yet each <neo-switch> still toggles: its wrapper is marked data-neo-sortable-nodrag, so pressing it never starts a drag. Native controls are exempt automatically; custom elements need the marker. Toggle a switch, then drag a row by its label.

Email digest
Push notifications
SMS alerts

Horizontal row#

orientation="horizontal" lays the items out in a row and constrains the drag to the horizontal axis.

One
Two
Three
Four

Grid#

orientation="grid" reorders in two dimensions: the dragged tile drops into the nearest slot, and arrow keys step a row at a time.

A
B
C
D
E
F
G
H
I
J
K
L

Unbounded#

By default the dragged item is clamped to the list's own box, so a drag toward an edge stops at the boundary. unbounded opts out, letting the item follow the pointer freely over the rest of the page.

One
Two
Three
Four

Scrollable parent#

Twenty rows in an overflow: auto box. The lifted item is clamped to the scroll viewport, and the grip keeps touch-action: none so dragging a handle reorders while dragging the row body still scrolls the list.

Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8
Item 9
Item 10
Item 11
Item 12
Item 13
Item 14
Item 15
Item 16
Item 17
Item 18
Item 19
Item 20

Custom move easing#

The easing attribute tunes how the displaced items glide. Here a slow easing="420ms ease-in-out" makes the reflow obvious; an empty easing="" would turn it off and snap instantly.

First
Second
Third
Fourth

Custom placement indicator#

A <template data-neo-sortable-placeholder> child replaces the default dashed gap. The gap keeps the dragged item's size; the slotted markup renders inside it, here a tinted "Drop here" bar.

Alpha
Bravo
Charlie
Delta

Disabled#

disabled freezes reordering: pointer drags do nothing, the grips drop out of the tab order, and the handles dim to read as inert.

Laptop
Phone
Tablet

Examples: Datastar#

Morphing during interaction#

A Datastar app can fat-morph the page mid-drag, so the server may reorder or replace the items under a <neo-sortable> while the user holds a row. Pick up an item (drag it, or press Space) while autoplay runs to see each outcome:

  • Same items: the morph re-emits the same ids, content, and order. Nothing changes, so the dragged row re-links to its morphed node and the drag is untouched.
  • Different order: same ids, reordered. With tolerate-reorder the kit reconciles the new order in place and the drag survives; the held row keeps its grip.
  • Different items: the morph swaps an id in or out. The kit can't map the in-flight drag onto the new set, so it cancels the drag and lets the server's order stand.

Tolerating an order-only morph needs tolerate-reorder. In the strict default, reordering the other rows already counts as a change and cancels the drag.

Initial
Same items
Different order
Different items
Deactivate Rename Duplicate
Delete
New state Toggle autoplay
Design
Build
Test
HTML CSS

The selected state's markup and its scoped CSS. The CSS is wrapped in <style>@scope { ... }</style> and applies to the preview only. Edits change the preview above, applied live while Sync is on, or when you click Patch or Replace. Patch morphs the preview; Replace swaps it outright.

Replace Patch

Signals

Datastar signals declared in the selected state's markup. Change a value to drive the preview, applied live while Sync is on, or when you click Patch.

Patch

Settings

Theme

Default Sunshine Ocean Matrix Rose

Accessibility

Playgrounds

Server Simulator