Gleam Drag and Drop Library
A drag-and-drop library for Gleam applications built with the Lustre web framework. This library is a direct port of the Elm dnd-list package by Anna Bansaghi, bringing the same functionality to the Gleam ecosystem.
Original Elm package: https://annaghi.github.io/dnd-list/
This library provides both basic drag-and-drop functionality and advanced group-based operations, allowing you to create rich interactive interfaces with precise control over item movement and organization.
Features
While dragging and dropping a list item, the mouse events, the ghost element’s positioning and the list sorting are handled internally by this library. Here are the key capabilities:
- Complete drag-and-drop system: Handle mouse events, ghost element positioning, and list sorting automatically
- Touch support: Two-tap selection pattern for mobile devices (tap to select, tap drop zone to move)
- Flexible configuration: Choose from multiple operations (insert, rotate, swap) and listen modes (on drag vs on drop)
- Movement constraints: Support for free, horizontal, or vertical movement
- Visual feedback: Ghost element follows cursor with customizable styling
- Detailed state information: Access drag source, drop target, positions, and DOM elements during operations
- Type-safe API: Full Gleam type safety with phantom types for your data models
- Groups support: Advanced functionality for transferring items between logical groups with different operations for same-group vs cross-group movements
Architecture
The library is split into two main modules:
dnd.gleam: Core drag-and-drop functionality for single listsgroups.gleam: Extended functionality for group-based operations
Both modules follow The Elm Architecture (TEA) pattern with:
- Model: Internal drag state management
- Update: Message handling and state transitions
- View: Event bindings and styling helpers
Installation
gleam add dnd
Basic Usage
Here’s a simple example of drag-and-drop with list reordering:
import dnd
import lustre
import lustre/element/html
import lustre/attribute
type Model {
Model(system: dnd.System(String, Msg), items: List(String))
}
type Msg {
DndMsg(dnd.DndMsg)
}
fn init(_flags) -> Model {
let config = dnd.Config(
before_update: fn(_, _, list) { list },
listen: dnd.OnDrag,
operation: dnd.Rotate,
)
let system = dnd.create(config, DndMsg)
Model(system: system, items: ["A", "B", "C", "D"])
}
fn update(model: Model, msg: Msg) -> Model {
case msg {
DndMsg(dnd_msg) -> {
let #(new_dnd, new_items) =
model.system.update(dnd_msg, model.system.model, model.items)
let updated_system = dnd.System(..model.system, model: new_dnd)
Model(system: updated_system, items: new_items)
}
}
}
fn view(model: Model) -> element.Element(Msg) {
html.div(
[
// Global mouse events when dragging
case model.system.info(model.system.model) {
Some(_) -> event.on("mousemove", decode_drag_event)
None -> attribute.none()
}
],
list.index_map(model.items, fn(item, index) {
html.div(
[
attribute.id("item-" <> int.to_string(index)),
..model.system.drag_events(index, "item-" <> int.to_string(index))
],
[html.text(item)]
)
})
)
}
Groups Usage
For more complex scenarios with multiple groups:
import dnd/groups
type Group {
Left
Right
}
type Item {
Item(group: Group, value: String)
}
fn init(_flags) -> Model {
let config = groups.Config(
before_update: fn(_, _, list) { list },
listen: groups.OnDrag,
operation: groups.Rotate, // Same-group operation
groups: groups.GroupsConfig(
listen: groups.OnDrag,
operation: groups.InsertBefore, // Cross-group operation
comparator: fn(a, b) { a.group == b.group },
setter: fn(target_item, drag_item) {
Item(..drag_item, group: target_item.group)
},
),
)
let system = groups.create(config, DndMsg)
Model(system: system, items: [
Item(Left, "A"), Item(Left, "B"),
Item(Right, "1"), Item(Right, "2")
])
}
Operations
Basic Operations
InsertAfter: Move dragged item after the drop targetInsertBefore: Move dragged item before the drop targetRotate: Circular shift of items between drag and drop positionsSwap: Exchange positions of dragged and drop target itemsUnaltered: No list modification (useful for custom logic)
Listen Modes
OnDrag: Updates happen continuously while draggingOnDrop: Updates happen only when item is dropped
Input Modes
MouseOnly: Traditional mouse-based drag-and-drop (default)TouchOnly: Touch-based two-tap selection pattern onlyAuto: Automatically detect input type per-interaction
Touch Support
The library supports touch devices using a hold-to-select pattern that doesn’t interfere with scrolling:
- Hold an item for 200ms to select it (item “pops” with animation, drop zones appear)
- Release your finger - the item stays selected
- Scroll freely to find the drop zone (scroll gestures are detected and ignored)
- Tap a drop zone to move the selected item there
- Selection auto-cancels after a configurable timeout (default 5 seconds)
Touch UX Features
- Hold duration: Prevents accidental selection when quickly tapping (configurable, default 200ms)
- Pop animation: Visual feedback when selection activates (requires CSS keyframe)
- Drop cooldown: After the “pop”, drops are ignored for 500ms to prevent accidental drops when the layout shifts
- Scroll detection: Both items and drop zones distinguish taps from scroll gestures
Enabling Touch Support
import lustre/effect.{type Effect}
fn init(_flags) -> #(Model, Effect(Msg)) {
let config = dnd.Config(
before_update: fn(_, _, list) { list },
movement: dnd.Vertical,
listen: dnd.OnDrag,
operation: dnd.Rotate,
// Enable Auto mode for both mouse and touch
mode: dnd.Auto,
// Auto-cancel after 5 seconds (0 to disable)
touch_timeout_ms: 5000,
// Distance threshold to distinguish tap from scroll (default 10px)
touch_scroll_threshold: 10,
// Hold duration before selection activates (default 200ms, 0 for immediate)
touch_hold_duration_ms: 200,
// Cooldown after pop before drops are accepted (default 500ms)
touch_drop_cooldown_ms: 500,
)
let system = dnd.create(config, DndMsg)
#(Model(system: system, items: [...]), effect.none())
}
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
case msg {
DndMsg(dnd_msg) -> {
// Use update_with_effect for timeout support
let #(new_dnd, new_items, dnd_effect) =
model.system.update_with_effect(dnd_msg, model.system.model, model.items)
let updated_system = dnd.System(..model.system, model: new_dnd)
#(Model(system: updated_system, items: new_items), dnd_effect)
}
}
}
Pop Animation CSS
Add this keyframe to your CSS for the pop animation:
@keyframes dnd-pop {
50% { transform: scale(1.05); }
}
Touch View Integration
fn view(model: Model) -> Element(Msg) {
let system = model.system
let is_selecting = system.is_touch_selecting(system.model)
html.div([], [
// Render items with drop zones when selecting
..list.index_map(model.items, fn(item, i) {
let item_el = item_view(item, i, system)
case is_selecting {
True -> [drop_zone_view(i, system), item_el]
False -> [item_el]
}
}) |> list.flatten,
])
}
fn item_view(item, index, system) -> Element(Msg) {
let item_id = "item-" <> int.to_string(index)
html.div(
list.flatten([
[attribute.id(item_id)],
// Mouse drag events
system.drag_events(index, item_id),
// Touch grip events (hold to select)
system.touch_grip_events(index, item_id),
// Pop animation styles for selected item
system.touch_selected_styles(system.model, index),
]),
[html.text(item)]
)
}
fn drop_zone_view(index, system) -> Element(Msg) {
html.div(
list.flatten([
[attribute.class("drop-zone")],
system.touch_drop_zone_events(index),
]),
[html.text("Drop here")]
)
}
Examples
This repository includes two complete examples:
Basic Example (example/src/example.gleam)
A simple sortable list demonstrating:
- Basic drag and drop with visual feedback
- Touch support with Auto mode
- Multiple operation types (InsertBefore, Rotate, Swap)
- Ghost element styling
- Cross-browser compatibility
Run with:
cd example
gleam run -m lustre/dev start
Groups Example (groupsexample/src/example.gleam)
An advanced two-column interface showing:
- Items organized in Left and Right groups
- Touch support for cross-group transfers
- Cross-group item transfers
- Same-group reordering
- Footer drop zones for group transfers
- Conditional drop logic based on group membership
Run with:
cd groupsexample
gleam run -m lustre/dev start
Technical Details
Mouse Event Handling
The library uses precise mouse coordinate tracking:
clientXandclientYfor accurate positioning- Custom event decoders for Lustre integration
- Automatic text selection prevention during drag
Ghost Element
- Positioned using
position: fixedfor viewport-relative positioning
Cross-Browser Support
- Tested on Chrome and Safari (desktop and mobile)
- Touch support tested on iOS Safari
Development
gleam format # Format code
gleam check # Type check
For development with live reload:
cd groupsexample && gleam run -m lustre/dev start # Groups example
API Reference
Core Types
System(a, msg): Main system containing model, update, and view functionsModel: Internal drag state (opaque type)Config(a): Configuration for drag operationsInfo: Current drag information (indices, positions, elements)
Groups Types
GroupsConfig(a): Extended configuration for group operationsOperation: Available list operationsListen: When to trigger operations (OnDrag vs OnDrop)
The library provides a clean, type-safe API that integrates seamlessly with Lustre applications while offering the flexibility to handle complex drag-and-drop scenarios.
Development
File bug reports or feature requests at the issue tracker. To send patches configure git sendemail and send your patches.
git config sendemail.to "~tpjg/dnd-dev@lists.sr.ht"
git commit -m "..."
git send-email HEAD^
Or to send just once:
git commit
git send-email --to "~tpjg/dnd-dev@lists.sr.ht" HEAD^