---
title: "Drag to reorder"
description: "Reorder a Firelight list by dragging, with the browser's drag and drop events typed by Ev.drag, repeat to move each item's element, and buttons that do the same from the keyboard."
---

# Drag to reorder

Build a list whose items can be dragged into a new order, with Move up and Move down buttons that do the same from the keyboard.

- [MDN: HTML Drag and Drop API](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API)

Drag Walk the dog to the top of the list. Or Tab to its ↑ button and press Enter a few times.

```fsharp
module Snippets.DragReorder

open Fable.Core
open Browser.Types
open Firelight
open type Firelight.Lit

type Step = { Id: int; Text: string }

/// `item` moved to `index` in the list without it.
let moveTo (index: int) (item: Step) (steps: Step list) =
    let rest = steps |> List.filter (fun s -> s.Id <> item.Id)
    List.insertAt (max 0 (min index rest.Length)) item rest

[<AttachMembers>]
type ReorderList() =
    inherit LitElement()

    static member properties =
        PropertyDeclarations.create [
            "steps", PropertyDeclaration<Step list>(state = true)
            "dragging", PropertyDeclaration<Step option>(state = true)
            "message", PropertyDeclaration<string>(state = true)
        ]

    static member styles =
        css
            $$"""
        :host { display: grid; gap: 0.5rem; justify-items: start; }
        ol { margin: 0; padding: 0; list-style: none; display: grid; gap: 0.35rem; width: 18rem; }
        li { display: flex; align-items: center; gap: 0.5rem; padding: 0.35rem 0.5rem; cursor: grab;
             border: 1px solid var(--border); border-radius: 0.5rem; background: var(--bg); }
        li.dragging { opacity: 0.5; }
        .text { flex: 1; }
        button { font: inherit; padding: 0 0.45rem; }
        p { margin: 0; }
        """

    member val steps =
        [ "Wake up"; "Make coffee"; "Read the news"; "Walk the dog"; "Start work" ]
        |> List.mapi (fun i text -> { Id = i; Text = text })
        with get, set

    member val dragging: Step option = None with get, set
    member val message = "" with get, set

    member this.Move(step: Step, index: int) =
        this.steps <- moveTo index step this.steps
        let position = 1 + (this.steps |> List.findIndex (fun s -> s.Id = step.Id))
        this.message <- $"{step.Text} moved to position {position} of {this.steps.Length}."

    /// From the keyboard: move, then put the focus back on the button, as moving the item's
    /// element takes the focus off it.
    member this.Nudge(step: Step, index: int, button: string) =
        this.Move(step, index)

        promise {
            let! _ = this.updateComplete
            this.query<HTMLElement> $"#{button}-{step.Id}" |> Option.iter _.focus()
        }
        |> Promise.start

    member this.StepView (step: Step) (index: int) =
        let dragStart =
            Ev.drag (fun e ->
                this.dragging <- Some step
                e.dataTransfer.effectAllowed <- "move"
                // Firefox only starts a drag that carries some data.
                e.dataTransfer.setData ("text/plain", step.Text) |> ignore
            )

        // Allowing a drop means cancelling dragover.
        let dragOver = Ev.drag (fun e -> if this.dragging.IsSome then e.preventDefault ())

        let drop =
            Ev.drag (fun e ->
                e.preventDefault ()
                this.dragging |> Option.iter (fun dragged -> this.Move(dragged, index))
            )

        html
            $"""
        <li draggable="true" class={if this.dragging = Some step then "dragging" else ""}
            @dragstart={dragStart} @dragover={dragOver} @drop={drop} @dragend={fun _ -> this.dragging <- None}>
            <span class="text">{step.Text}</span>
            <button id="up-{step.Id}" aria-label="Move {step.Text} up" aria-disabled={index = 0}
                @click={fun _ -> if index > 0 then this.Nudge(step, index - 1, "up")}>↑</button>
            <button id="down-{step.Id}" aria-label="Move {step.Text} down" aria-disabled={index = this.steps.Length - 1}
                @click={fun _ -> if index < this.steps.Length - 1 then this.Nudge(step, index + 1, "down")}>↓</button>
        </li>"""

    override this.render() =
        html
            $"""
        <ol aria-label="Morning routine">{repeat (this.steps, (fun s _ -> s.Id), this.StepView)}</ol>
        <p role="status">{if this.message <> "" then html $"{this.message}" else nothing}</p>"""

defineElement<ReorderList> "my-reorder-list"
```

```html
<my-reorder-list></my-reorder-list>
```

## How it works

The order is an F# list, and `moveTo` is a plain function that returns the list with one step
moved. Dragging and the buttons both end in `Move`, which calls it.

- **Dragging.** `draggable="true"` lets an item be picked up. Its `dragstart` records which step
  is moving, and `drop`, on the item it lands on, moves it to that item's place. `Ev.drag` types
  each handler's event as a `DragEvent`, so `e.dataTransfer` needs no cast.
- **Allowing the drop.** An element only accepts a drop if its `dragover` handler calls
  `preventDefault`, which is the browser's way of saying "drop here". Without it, `drop` never
  fires.
- **Firefox.** Firefox doesn't start a drag that carries no data, so `dragstart` puts the step's
  text in `dataTransfer`, though nothing reads it.
- **Keyed.** [`repeat`](https://roboz0r.github.io/Firelight/guides/templates/#keyed-lists-with-repeat) keys each item by its id, so
  each `<li>` moves with its step. With `List.map`, each element would stay put and show another
  step's text.
- **The keyboard.** Dragging needs a pointer, so each item also has ↑ and ↓ buttons, named "Move
  Walk the dog up" and so on for screen readers. Moving an element in the DOM takes the focus off
  the button inside it, so `Nudge` puts the focus back once the update has rendered. At the top or
  the bottom, a button is `aria-disabled` rather than disabled, so it keeps the focus.
- **Saying what happened.** The `role="status"` line announces the new position, for anyone who
  can't see the list move.

Touch screens don't send these drag events. For dragging on phones, listen for `pointerdown`,
`pointermove` and `pointerup` instead, or use a library built on them. The buttons work everywhere.

The [Kanban demo](https://roboz0r.github.io/Firelight/demos/kanban/) drags cards between columns the same way, with the drag state in
its Elmish model.

## Related

- [Events: Typed handlers with Ev](https://roboz0r.github.io/Firelight/guides/events/#typed-handlers-with-ev), for `Ev.drag` and the
  other event types.
- [Animating list changes](https://roboz0r.github.io/Firelight/cookbook/animate-list/), to slide the items to their new places.
