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:

Architecture

The library is split into two main modules:

Both modules follow The Elm Architecture (TEA) pattern with:

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

Listen Modes

Input Modes

Touch Support

The library supports touch devices using a hold-to-select pattern that doesn’t interfere with scrolling:

  1. Hold an item for 200ms to select it (item “pops” with animation, drop zones appear)
  2. Release your finger - the item stays selected
  3. Scroll freely to find the drop zone (scroll gestures are detected and ignored)
  4. Tap a drop zone to move the selected item there
  5. Selection auto-cancels after a configurable timeout (default 5 seconds)

Touch UX Features

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:

Run with:

cd example
gleam run -m lustre/dev start

Groups Example (groupsexample/src/example.gleam)

An advanced two-column interface showing:

Run with:

cd groupsexample
gleam run -m lustre/dev start

Technical Details

Mouse Event Handling

The library uses precise mouse coordinate tracking:

Ghost Element

Cross-Browser Support

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

Groups Types

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^
Search Document