Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Livewire Drag And Drop Laravel Package

artisanpack-ui/livewire-drag-and-drop

A Livewire drag-and-drop component from ArtisanPack UI for building sortable lists and moving items between lists without custom JavaScript. It handles reordering and drag events server-side through Livewire, making it easy to add interactive, sortable interfaces to Laravel apps.

View on GitHub
Deep Wiki
Context7

Getting Started

This package gives you drag-and-drop reordering for Livewire lists via two Alpine directives: x-drag-context on the list container and x-drag-item on each item, with a drag:end event carrying orderedIds / toOrderedIds that you persist server-side.

Minimal setup:

  1. Install the JS package (npm) — the PHP side is just Livewire; there is no service provider to publish.
  2. Register the Alpine plugin before Alpine boots, in resources/js/app.js:
import Alpine from 'alpinejs'
import LivewireDragAndDrop from 'livewire-drag-and-drop'

Alpine.plugin(LivewireDragAndDrop) // also auto-registers on alpine:init
Alpine.start()
  1. Mark up a list — the context element is where you listen for events:
<ul x-drag-context wire:key="tasks" @drag:end="reorder($event.detail)">
    @foreach ($tasks as $task)
        <li x-drag-item="{{ $task->id }}" wire:key="task-{{ $task->id }}">
            {{ $task->title }}
        </li>
    @endforeach
</ul>
  1. Persist the order from the Alpine handler into Livewire:
@drag:end="$wire.reorder($event.detail.toOrderedIds)"

Where to look first: docs/events.md (the full drag:end payload and lifecycle), docs/connected-containers.md if you need kanban-style moves, and docs/upgrading.md — 2.1 changes several integration points even though the 2.0 directive API is unchanged.

First real use case: a single sortable list with a position column. Store the ordered IDs, loop over them, and write position = $index. That path needs zero configuration and no new APIs.

Implementation Patterns

Single-list reorder (the 80% case). Keep the array-of-IDs as the source of truth on the model and reindex on save:

public function reorder(array $orderedIds): void
{
    foreach ($orderedIds as $position => $id) {
        Task::whereKey($id)->update(['position' => $position]);
    }
}

Use $event.detail.toOrderedIds rather than parsing the DOM.

Connected containers (kanban). Give sibling contexts the same group; they then accept each other's items, including when a container is empty:

<div x-drag-context.group.board wire:key="col-todo" @drag:end="move($event.detail)">
    <div data-drag-items>
        @foreach ($todo as $task) ... @endforeach
    </div>
</div>

The data-drag-items marker is what makes an empty container a valid drop zone — add it to every column, not just empty ones, so the markup doesn't change shape.

One handler, both directions. A drop payload contains fromContext, toContext, fromIndex, toIndex, fromOrderedIds, toOrderedIds, itemId, moveId. Use a single Livewire method that branches on fromContext !== toContext — this is also where you do authorization (can this user move this card to "done"?).

Optimistic moves with revert. Every move has a moveId. Persist optimistically, then roll back on server rejection:

@drag:end="
    $wire.move($event.detail).catch(() => LivewireDragAndDrop.revert($event.detail.moveId))
"

revertItem(itemId) and the $dragRevert(itemId) Alpine magic cover the cases where you only know the item, e.g. a broadcast telling the client its move was rejected.

Drop rules as the first line of validation. Use accepts for structural rules (a "done" column that refuses new work, a read-only board) and keep business rules in drag:drop:

<div x-drag-context.group.board :accepts="(detail) => detail.toContext !== 'locked'">
@drag:drop="if ($event.detail.toContext === 'archive' && ! $event.detail.itemId) $event.detail.reject('Cannot archive')"

drag:drop is cancelable before the DOM changes — reject there and the item animates back, which beats persisting and reverting.

Nested contexts (kanban with card lists). Nest x-drag-context elements and rely on 2.1's non-bubbling events: the inner list's drag:end will not fire your column handler. Attach each handler to its own context element.

Keyboard and accessibility. Nothing to wire up — items get role="button", tabindex="0", and usage instructions via aria-describedby; Left/Right move between containers in a group, Up/Down/Home/End reorder within. Add human labels with data-drag-label on contexts and items (they feed aria-label and the {container} / {item} message placeholders). Translate everything once:

LivewireDragAndDrop.configure({
    messages: { moveUp: 'Mover {item} hacia arriba' },
    touchDelay: 300,
})

Styling with state attributes. data-dragging (item), data-drag-over, data-drag-active / data-drag-disallowed (contexts), body[data-drag-in-progress], and --drag-indicator-color let you do the whole visual layer in CSS without JS hooks. body[data-drag-in-progress] is the right selector for cursor: grabbing and scroll locking.

Touch and pen. On touch, drags begin after a press-and-hold (touchDelay, 200 ms). For dense mobile lists raise it to ~300 ms to avoid hijacking scroll; for pen/mouse the dragThreshold (5 px) prevents accidental drags on click.

Gotchas and Tips

Events no longer bubble — this is the one that will break your 2.0 code. A #[On('drag:end')] Livewire listener, a document listener, or a handler on an ancestor div will silently never fire. Listen on the x-drag-context element itself via Alpine (@drag:end="...") and call $wire. This is also why nested contexts work: an inner list's events can't reach the column.

Don't write native HTML5 drag code. Items no longer get draggable="true", and dragstart / dragover / drop no longer fire from this package. If you had custom CSS keyed on [draggable="true"] or a helper library that simulated HTML5 drags, migrate to the package's events and state attributes. Browser test helpers built around HTML5 DnD won't drive it either — assert on drag:end payloads and DOM order instead.

Upgrade if you're pinned to 2.0.x with Livewire. The 2.0 message.processed hook never fired, so moved items stayed flagged __livewire_ignore and server re-renders stopped applying to them. 2.1 uses the morphed hook (Livewire 3 and 4). If you saw "the item doesn't update after a re-render", this is the fix — but it means items need stable wire:keys to morph correctly.

Empty connected containers need data-drag-items. Without the marker the context has no drop position, so the column looks connected but swallows nothing. This is the most common "connected containers don't work" report.

configure() timing matters. Call it on the plugin function before Alpine.start(), or inside alpine:init after auto-registration. Calling it after drags have started changes nothing retroactively, and calling Alpine.plugin() twice can double-bind listeners.

revert() needs the IDs you were given. moveId is per-move and itemId must match the value in x-drag-item — don't invent client-side IDs, and don't reuse them across pages/pagination. After a revert, the DOM is restored locally, so don't also re-render the whole list or you'll race the revert.

Keyboard edge cases are handled, but test them. Escape or moving focus away during a keyboard drag restores the item to its original container and position; after a keyboard drop focus stays on the moved item even through a Livewire morph. Arrow Left/Right only cross containers for grouped contexts — ungrouped lists reorder within only, which surprises people who add a group to one side of a pair.

Author attributes win. The package sets role, tabindex, and aria-describedby but never overwrites what you provide. If you hand-roll aria-label, the {item} / {container} placeholders in translated messages won't resolve unless you also set data-drag-label.

Debug with state attributes, not console.log in directives. data-drag-active, data-drag-over, and data-drag-disallowed tell you immediately whether the pointer engine recognised a context as a valid target — data-drag-disallowed means accepts / data-drag-disabled refused it, which is a rules problem, not an event problem.

UMD global shape changed. With a plain <script> tag, window.LivewireDragAndDrop is now the plugin function itself, so window.LivewireDragAndDrop.configure(...) works directly — code written against the 2.0 namespace object (LivewireDragAndDrop.default) breaks.

The 2.0.1 named exports still exist. registerDirectives and registerLivewireHooks are kept and registerDirectives is now the same function as the default export, so bundler setups importing them keep working — just don't rely on them being distinct.

Announcements are templates, not strings. Every screen-reader message is a {placeholder} template; set them globally with configure({ messages }) or per context with data-drag-messages. Mixing the two is fine — per-context wins, and partial overrides keep the untouched keys from the global config.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin