<!-- LLM_VERSION_INFO
FORMAT: text/markdown
CONTENT_TYPE: article
ORIGINAL_URL: https://fly.io/phoenix-files/liveview-drag-and-drop
ALTERNATE_VERSION: phoenix-files/liveview-drag-and-drop/index.html (text/html)
EXTRACTION_DATE: 2026-04-18T22:12:23.421Z

This is the markdown version with text-only content (images converted to alt-text).
For rich formatting with images, request the HTML version at: phoenix-files/liveview-drag-and-drop/index.html
-->

# LiveView Drag and Drop with SortableJS

LiveView hooks provide a powerful way to seamlessly integrate JavaScript libraries into LiveView applications. In this post, we’ll integrate SortableJS to build a list component with draggable items.

## Defining a live_component

We begin by defining a `:live_component` called `ListComponent`, along with its two main callbacks, `render/1` and `update/2`:

```elixir
defmodule ComponentsExamplesWeb.ListComponent do
  use ComponentsExamplesWeb, :live_component

def render(assigns) do
    ~H"""
    <div class="bg-gray-100 py-4 rounded-lg">
      <div class="space-y-5 mx-auto max-w-7xl px-4 space-y-4">

</div>
    </div>
    """
  end

def update(assigns, socket) do
    {:ok, assign(socket, assigns)}
  end
end
```

Next, we add some elements to our component. The component has three main sections: 1) a header with the list title and an input for adding new elements, 2) the list of draggable elements, and 3) a button for clearing the list.

```elixir
def render(assigns) do
  ~H"""
  <div class="bg-gray-100 py-4 rounded-lg">
    <div class="space-y-5 mx-auto max-w-7xl px-4 space-y-4">
+      <.header>
+        <%= @list_name %>
+        <.simple_form
+          for={@form}
+          phx-change="validate"
+          phx-submit="save"
+          phx-target={@myself}
+        >
+          <.input field={@form[:name]} type="text" />
+          <:actions>
+            <.button class="align-middle ml-2">
+              <.icon name="hero-plus" />
+            </.button>
+          </:actions>
+        </.simple_form>
+     </.header>
    </div>
  </div>
  """
end
```

Now the part we’re interested in today: the items list!

```elixir
def render(assigns) do
  ~H"""
  <div class="bg-gray-100 py-4 rounded-lg">
    <div class="space-y-5 mx-auto max-w-7xl px-4 space-y-4">
      <.header>
        ...
      </.header>
+      <div id={"#{@id}-items"}>
+        <div
+          :for={item <- @list}
+          id={"#{@id}-#{item.id}"}
+          class="..."
+        >
+          <div class="flex">
+            <button type="button" class="w-10">
+              <.icon
+                name="hero-check-circle"
+                class={[
+                  "w-7 h-7",
+                  if(item.status == :completed, do: "bg-green-600", else: "bg-gray-300")
+                 ]}
+              />
+            </button>
+            <div class="flex-auto block text-sm leading-6 text-zinc-900">
+              <%= item.name %>
+            </div>
+            <button type="button" class="w-10 -mt-1 flex-none">
+             <.icon name="hero-x-mark" />
+           </button>
+         </div>
+       </div>
      </div>
    </div>
  </div>
  """
end
```

## Adding SortableJS to our LiveView app

There is an existing JavaScript library called SortableJS that provides drag-and-drop functionality for elements inside an HTML tag. Let’s add SortableJS to our LiveView application!

1. Go to the [SortableJS source repository](https://github.com/SortableJS/Sortable) and locate the `sortable.js` file.
2. Copy the `sortable.js` file to the `/assets/vendor/` directory in your Phoenix project.

Next, we need to import the Sortable library. You can do this by adding the following line at the top of the `app.js` file:

```javascript
  import {Socket} from "phoenix"
  import {LiveSocket} from "phoenix_live_view"
  import topbar from "../vendor/topbar"
+ import Sortable from "../vendor/sortable"
```

Once the Sortable library is imported, we can use it in our component by defining a Hook in the same file:

```javascript
let Hooks = {}

Hooks.Sortable = {
  mounted(){
    let sorter = new Sortable(this.el, {
      animation: 150,
      delay: 100,
      dragClass: "drag-item",
      ghostClass: "drag-ghost",
      forceFallback: true,
      onEnd: e => {
        let params = {old: e.oldIndex, new: e.newIndex, ...e.item.dataset}
        this.pushEventTo(this.el, "reposition", params)
      }
    })
  }
}

let liveSocket = new LiveSocket("/live",
                 Socket,
                 {params: {_csrf_token: csrfToken}, hooks: Hooks}
               )
```

## Using SortableJS in LiveView components

We add a couple of lines to our component:

```elixir
def render(assigns) do
  ~H"""
  <div class="bg-gray-100 py-4 rounded-lg">
    <div class="space-y-5 mx-auto max-w-7xl px-4 space-y-4">
      <.header>
        ...
      </.header>
-     <div id={"#{@id}-items"}>
+     <div id={"#{@id}-items"} phx-hook="Sortable" data-list_id={@id}>
        <div
          :for={item <- @list}
          id={"#{@id}-#{item.id}"}
          class="..."
+         data-id={item.id}
        >
          ...
        </div>
      </div>
    </div>
  </div>
  """
end
```

## Formatting draggable list items

Fixing this detail is simple. We’ve already specified the CSS classes to format the drop placeholder and the dragged article. However, instead of defining new CSS classes, we can leverage the ones already defined by Tailwind.

### Bonus: multiple lists

What if we want to drag items between different lists? It is an option that Sortable already has, we just have to configure it:

```javascript
Hooks.Sortable = {
+ let group = this.el.dataset.group
  mounted(){
    let sorter = new Sortable(this.el, {
+     group: group ? group : undefined,
      animation: 150,
      delay: 100,
      dragClass: "drag-item",
      ghostClass: "drag-ghost",
      forceFallback: true,
      onEnd: e => {
        let params = {old: e.oldIndex, new: e.newIndex, ...e.item.dataset}
        this.pushEventTo(this.el, "reposition", params)
      }
    })
  }
}
```

## Discussion

In this post, we’ve learned how to set up SortableJS in our LiveView app and use it in our live components. But here’s the thing - we’re not actually doing anything with the data we send from the client to the server yet. We still need to figure out how to persist our elements and the position changes.
