AdminKitDocs
Plugins

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.

Pro

This plugin is only available in AdminKit PRO. Learn more.

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 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:

<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:

<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:

<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 the Tasks page sets:

OptionValueWhat it does
group"tasks"Lists with the same group exchange items
animation150Slides the other items aside, in milliseconds
delay150Waits 150ms after the press before the drag starts
delayOnTouchOnlytrueApplies 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
onSortupdateCountsRuns 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.

Last updated on

On this page