# Bootstrap 5 Drag and Drop

Bootstrap 5 drag and drop with SortableJS in AdminKit PRO: lists people reorder by dragging, and a task board whose cards move between columns.

> This plugin is only available in **AdminKit PRO**. [Learn more](/pricing/#bootstrap).

Drag and drop lets people put things in order by moving them: items in a list, tasks in a column, cards on a board. Use it where the order itself means something, such as priorities or the stage a task is in.

AdminKit PRO uses [SortableJS](https://sortablejs.github.io/Sortable/) 1.15.7. It works with the mouse, a finger or a pen. The Tasks page uses it for a board where cards move within a column and from one column to another.

SortableJS moves the elements on the page and leaves the styling to you. AdminKit adds two classes for it: a grab cursor for the items, and a faded placeholder for the item being dragged. Both work the same in light and dark mode.

## Sortable list

The simplest case is a list people can reorder. Drag an item up or down:

```html
<div class="list-group" id="sortable-list">
  <div class="list-group-item cursor-grab">Write release notes</div>
  <div class="list-group-item cursor-grab">Review pull request</div>
  <div class="list-group-item cursor-grab">Update dependencies</div>
  <div class="list-group-item cursor-grab">Plan the next sprint</div>
</div>

<script>
new Sortable(document.getElementById("sortable-list"), {
  animation: 150,
  ghostClass: "sortable-ghost"
});
</script>
```

`cursor-grab` shows the grab cursor over the items. `ghostClass: "sortable-ghost"` gives the placeholder 40% opacity while you drag. `animation: 150` slides the other items out of the way in 150ms.

## Task board

A board is a row of lists that share a `group`. SortableJS then lets an item move from one list to another. This is the board of the Tasks page, with fewer cards:

```html
<div class="task-board">
  <section class="task-column" aria-labelledby="board-todo">
    <div class="task-column-header">
      <h2 class="h5 fw-bold mb-0" id="board-todo">To do</h2>
      <span class="badge badge-secondary-light rounded-pill"><span data-task-count>2</span><span class="visually-hidden"> tasks</span></span>
    </div>
    <div class="task-list">
      <article class="card task-card cursor-grab">
        <div class="card-body">
          <div class="task-card-top"><span class="task-card-key">AK-241</span></div>
          <h3 class="task-card-title">Add CSV export to the orders table</h3>
          <div class="task-card-labels"><span class="badge badge-info-light">Frontend</span></div>
        </div>
      </article>
      <article class="card task-card cursor-grab">
        <div class="card-body">
          <div class="task-card-top"><span class="task-card-key">AK-238</span></div>
          <h3 class="task-card-title">Write onboarding emails for trial accounts</h3>
          <div class="task-card-labels"><span class="badge badge-secondary-light">Content</span></div>
        </div>
      </article>
    </div>
  </section>

  <section class="task-column" aria-labelledby="board-progress">
    <div class="task-column-header">
      <h2 class="h5 fw-bold mb-0" id="board-progress">In progress</h2>
      <span class="badge badge-secondary-light rounded-pill"><span data-task-count>1</span><span class="visually-hidden"> tasks</span></span>
    </div>
    <div class="task-list">
      <article class="card task-card cursor-grab">
        <div class="card-body">
          <div class="task-card-top"><span class="task-card-key">AK-236</span></div>
          <h3 class="task-card-title">Fix the date filter on the invoices page</h3>
          <div class="task-card-labels"><span class="badge badge-danger-light">Bug</span></div>
        </div>
      </article>
    </div>
  </section>

  <section class="task-column" aria-labelledby="board-done">
    <div class="task-column-header">
      <h2 class="h5 fw-bold mb-0" id="board-done">Done</h2>
      <span class="badge badge-secondary-light rounded-pill"><span data-task-count>1</span><span class="visually-hidden"> tasks</span></span>
    </div>
    <div class="task-list">
      <article class="card task-card cursor-grab">
        <div class="card-body">
          <div class="task-card-top"><span class="task-card-key">AK-230</span></div>
          <h3 class="task-card-title">Set up the staging environment</h3>
          <div class="task-card-labels"><span class="badge badge-success-light">DevOps</span></div>
        </div>
      </article>
    </div>
  </section>
</div>

<script>
var board = document.querySelector(".task-board");

function updateCounts() {
  board.querySelectorAll(".task-column").forEach(function (column) {
    column.querySelector("[data-task-count]").textContent = column.querySelectorAll(".task-card").length;
  });
}

board.querySelectorAll(".task-list").forEach(function (list) {
  new Sortable(list, {
    group: "tasks",
    animation: 150,
    delay: 150,
    delayOnTouchOnly: true,
    ghostClass: "sortable-ghost",
    onSort: updateCounts
  });
});
</script>
```

The columns sit side by side. When they do not fit, the board scrolls sideways and stops at the start of a column. On screens narrower than 576px, each column takes 85% of the width, so the next one peeks in.

Each list has a minimum height of 4rem. That leaves room to drop a card in an empty column.

## Use it in your page

`js/app.js` loads SortableJS and makes it available as `window.Sortable`. There is nothing else to install.

`app.js` is a module script, so it runs after the page has been parsed. The Tasks page therefore sets up the board in a `DOMContentLoaded` listener, which runs after `app.js`:

```html
<script>
  document.addEventListener("DOMContentLoaded", function () {
    var board = document.querySelector(".task-board");

    function updateCounts() {
      board.querySelectorAll(".task-column").forEach(function (column) {
        column.querySelector("[data-task-count]").textContent = column.querySelectorAll(".task-card").length;
      });
    }

    // On touch screens a short press starts the drag, so a swipe still scrolls the board
    board.querySelectorAll(".task-list").forEach(function (list) {
      new Sortable(list, {
        group: "tasks",
        animation: 150,
        delay: 150,
        delayOnTouchOnly: true,
        ghostClass: "sortable-ghost",
        onSort: updateCounts
      });
    });
  });
</script>
```

Cards added to a list later, such as those from the quick-add form of the Tasks page, can be dragged right away. SortableJS works on the list, not on each card.

## Options

These are the [SortableJS options](https://github.com/SortableJS/Sortable#options) the Tasks page sets:

| Option | Value | What it does |
| --- | --- | --- |
| `group` | `"tasks"` | Lists with the same group exchange items |
| `animation` | `150` | Slides the other items aside, in milliseconds |
| `delay` | `150` | Waits 150ms after the press before the drag starts |
| `delayOnTouchOnly` | `true` | Applies `delay` to touch only, so a mouse drags at once and a swipe still scrolls the board |
| `ghostClass` | `"sortable-ghost"` | The class of the placeholder while dragging |
| `onSort` | `updateCounts` | Runs when a list changed, here to update the counts in the column headers |

SortableJS only changes the order on the page. The Tasks page does not save it. To keep the new order, read the cards of each list in `onSort` and send them to your server.

## Styles

- `.cursor-grab` is a utility class that shows the grab cursor. It is in `src/scss/4-utilities/_cursors.scss`.
- `.sortable-ghost` sets the placeholder to 40% opacity. It is in `src/scss/5-vendor/_sortable.scss`.
- The board classes, `.task-board`, `.task-column`, `.task-list` and `.task-card` with its parts, are in `src/scss/3-components/_tasks.scss`. The column background is a tint of the text color, so it works on the light and the dark page background.

## Accessibility

Drag and drop with SortableJS only works with a pointer: a mouse, a finger or a pen.

- SortableJS 1.15 has no keyboard support. People who use the keyboard cannot move an item with it.
- It does not announce moves to screen readers either.
- The cards of the Tasks page are `<article>` elements and are not focusable.

So give every drag and drop action another way to do the same thing. For example, add a "Move to" menu or a select with the columns to each card, and move the card with JavaScript when it changes. Then tell screen readers where the card went, for example in an `aria-live` region.

The column headings, the counts with their hidden " tasks" text, and the `aria-labelledby` on each column already help screen reader users understand the board.

## Related

- [React Drag and Drop with dnd kit](/react/docs/plugins/drag-and-drop/): the task board in AdminKit for React, with keyboard support
- [Next.js Drag and Drop with dnd kit](/nextjs/docs/plugins/drag-and-drop/): the same board in AdminKit for Next.js
- [Card](/bootstrap/docs/components/card/): the cards of the task board
- [List group](/bootstrap/docs/components/list-group/): the list in the first example
- [Badge](/bootstrap/docs/components/badge/): the labels and counts
- [Tasks demo](https://demo.adminkit.io/pages-tasks.html)

---

Canonical HTML page: https://adminkit.io/bootstrap/docs/plugins/drag-and-drop/

Site overview for agents: https://adminkit.io/llms.txt (expanded: https://adminkit.io/llms-full.txt) · Documentation: https://adminkit.io/bootstrap/docs/ · Questions: support@adminkit.io
