Implementing a drag-and-drop interface in Drupal with JavaScript
Drag-and-drop interactions can make Drupal administration feel considerably faster when editors need to reorder cards, move navigation items, arrange dashboard panels, or organise content in a visual way. The browser supplies the basic pointer events, while Drupal provides the behaviour system, render arrays, permissions, form API, and AJAX tools needed to turn that interaction into a reliable feature.
A sound implementation separates the visual movement from the saved data. JavaScript can update the screen immediately, but the server must still check permissions, validate the submitted order, and save only records the current user is allowed to change. This distinction matters for a public-facing site as much as it does for an internal editorial dashboard.
For Australian teams, the practical setting can vary widely. A small agency in Brisbane may need a lightweight custom module, while a university in Melbourne or a council in Perth may require keyboard access, audit trails, and compatibility with established Drupal workflows. The examples below use Drupal behaviours and modern JavaScript without tying the solution to a particular theme.
Decide what should move
Start by defining the item being moved and the value that represents its position. Common examples include taxonomy terms, menu links, paragraphs inside a layout, media tiles, or rows in a custom administration view. Each draggable element should have a stable identifier, such as a node ID or UUID, rather than relying on its current position in the DOM.
A useful markup pattern might look like this:
<div class="js-sortable-list" data-save-url="/admin/content/cards/reorder">
<div class="js-sortable-item" data-item-id="42" tabindex="0">
<span class="card-title">Community grants</span>
<button type="button" class="js-move-up">Move up</button>
<button type="button" class="js-move-down">Move down</button>
</div>
</div>
The data-save-url attribute identifies the endpoint, while data-item-id gives the server enough information to understand the new order. Avoid placing sensitive information in data attributes. A data attribute is visible to anyone who can inspect the page and should be treated as presentation metadata, not as a security boundary.
Decide whether the interface needs free dragging, simple reordering, or moving items between groups. A single vertical list is easier to understand and test than a dashboard where cards can be dropped into several columns. If editors in Sydney are arranging a large catalogue or newsroom queue, filtering, search, and a visible save state may be more valuable than elaborate animation.
Add the behaviour through a Drupal library
Drupal JavaScript should normally be attached through a library rather than inserted directly into a Twig template. In a custom module, define the assets in example.libraries.yml:
sortable:
version: 1.x
js:
js/sortable.js: {}
dependencies:
- core/drupal
- core/once
- core/drupalSettings
css:
theme:
css/sortable.css: {}
Attach the library from a render array, a controller, a preprocess function, or a Twig template depending on where the interface is built. For an administrative route, a controller can return:
return [
'#theme' => 'sortable_cards',
'#cards' => $cards,
'#attached' => [
'library' => ['example/sortable'],
],
];
Drupal behaviours are important because markup can be replaced through AJAX after the first page load. The behaviour receives a context and should use once() so that event handlers are not attached repeatedly:
(function (Drupal, once) {
Drupal.behaviors.cardSorter = {
attach(context) {
once('card-sorter', '.js-sortable-list', context).forEach((list) => {
initialiseSorter(list);
});
}
};
})(Drupal, once);
This approach works with BigPipe, modal forms, AJAX-rendered views, and other Drupal features that alter the page after the initial response. It also keeps the code aligned with Drupal’s asset management and aggregation systems. Test the behaviour with caching enabled, because an interaction that works only when aggregation is disabled is not ready for production.
Implement the pointer interaction
Native HTML5 drag events can be enough for a simple desktop list, but they often feel inconsistent across touch devices. A lightweight pointer-event implementation gives you one event model for mouse, stylus, and touch input. The essential flow is to identify the item on pointerdown, move it while the pointer changes position, and commit the result on pointerup.
function initialiseSorter(list) {
let draggedItem = null;
list.addEventListener('pointerdown', (event) => {
const item = event.target.closest('.js-sortable-item');
if (!item || !list.contains(item)) {
return;
}
draggedItem = item;
draggedItem.classList.add('is-dragging');
draggedItem.setPointerCapture(event.pointerId);
});
list.addEventListener('pointermove', (event) => {
if (!draggedItem) {
return;
}
const target = document.elementFromPoint(event.clientX, event.clientY);
const targetItem = target && target.closest('.js-sortable-item');
if (targetItem && targetItem !== draggedItem && list.contains(targetItem)) {
const box = targetItem.getBoundingClientRect();
const insertBefore = event.clientY < box.top + box.height / 2;
list.insertBefore(
draggedItem,
insertBefore ? targetItem : targetItem.nextElementSibling
);
}
});
list.addEventListener('pointerup', () => {
if (!draggedItem) {
return;
}
draggedItem.classList.remove('is-dragging');
draggedItem = null;
saveOrder(list);
});
}
A production version should handle pointercancel, prevent accidental text selection, and avoid saving when the item has not changed position. CSS can provide a clear insertion marker, a reduced-opacity dragged item, and a cursor that indicates the available action. Keep transitions brief; editors moving dozens of items need feedback, not visual effects that slow the task.
If the project already uses a well-maintained sortable library, it may be preferable to writing all pointer logic yourself. The decision depends on bundle size, licensing, touch support, and the complexity of nested drop zones. A small custom behaviour is appropriate for one list, whereas several connected lists may justify a dedicated library loaded through the Drupal asset pipeline.
Save the order securely
Once the browser has a new sequence, collect the identifiers in their current DOM order and send them to Drupal. Use a POST request and include Drupal’s CSRF token. A token can be made available through drupalSettings, or the endpoint can use the standard Drupal mechanisms for obtaining and checking one.
function saveOrder(list) {
const order = [...list.querySelectorAll('.js-sortable-item')]
.map((item) => item.dataset.itemId);
list.classList.add('is-saving');
fetch(list.dataset.saveUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': drupalSettings.example.csrfToken
},
body: JSON.stringify({ order })
})
.then((response) => {
if (!response.ok) {
throw new Error('The order could not be saved.');
}
list.classList.add('is-saved');
})
.catch(() => {
list.classList.add('has-save-error');
})
.finally(() => {
list.classList.remove('is-saving');
});
}
On the Drupal side, define a route with an access requirement and a controller that reads the JSON body. The controller should verify that the submitted IDs belong to the expected entity type, remove duplicates, check the user’s access to each item, and reject missing or unknown records. Do not trust a hidden field, DOM order, or role name supplied by the browser.
The save operation should be transactional where several records are updated. If the database write fails halfway through, editors should not receive a success message for an incomplete reorder. Return a useful HTTP status and a small JSON response. If the site uses moderation, revisions, or editorial locks, decide whether reordering creates a revision and explain that behaviour in the interface.
For teams sharing content work between Adelaide and Canberra, concurrent edits are a realistic concern. Consider returning a revision or update timestamp and checking it before saving. When the server detects that someone else has changed the list, retain the editor’s current screen and display a message rather than silently overwriting the other person’s work.
Make the interaction accessible
A drag-only interface excludes users who cannot use a mouse or who prefer keyboard navigation. Provide explicit move controls as a parallel mechanism. The “Move up” and “Move down” buttons in the markup can use the same order-saving function as pointer dragging, so both paths update the same data model.
Keyboard support can also allow users to focus an item, press Space to pick it up, use the arrow keys to move it, and press Space again to drop it. If that pattern is implemented, communicate the state with an aria-live region and a visible message. Do not rely on colour or position alone to show where an item will land.
<div class="visually-hidden" aria-live="polite" data-sorter-status></div>
When an item moves, announce something such as “Community grants moved to position two.” Keep focus on the moved item so the user does not lose their place. The interface should remain usable at 200 percent zoom and on narrow screens, including mobile devices used by staff working away from a desktop office.
Australian government and education projects commonly have formal accessibility expectations aligned with WCAG. Even where the site is a commercial publication or a local business website, accessible reordering reduces support requests and gives editors a dependable alternative to a precise drag gesture. Test with keyboard-only navigation, VoiceOver or NVDA, touch input, and browser zoom before calling the feature complete.
Test, monitor, and deploy carefully
Test the full path rather than stopping when the item appears to move. Confirm that the order survives a page refresh, cache rebuild, language switch, failed request, permission change, and simultaneous edit. Check empty lists, long titles, filtered results, duplicate submissions, and records deleted while an editor’s page remains open.
The visual interaction and the stored order should be tested separately. A browser test can confirm that the correct identifiers are submitted, while a Drupal kernel or functional test can verify access checks, validation, database updates, and error responses. Logging rejected requests can help identify stale interfaces or attempted access to records outside the current user’s permissions.
A useful rollout pattern is to enable the feature for a small editorial role first. A content team in Melbourne might trial it on a staging copy before extending it to every publisher, while a national organisation may need a short editor guide and a fallback form. Keep documentation close to the feature; an operational reference such as workflow documentation can sit alongside the module notes when teams define publishing responsibilities.
Choose the implementation according to the content model and the editorial task. A simple list rarely needs a large dependency, while complex layouts benefit from established components and more extensive testing.
| Approach | Suitable for | Main benefit | Main limitation |
|---|---|---|---|
| Native pointer events | One custom list | Small, dependency-free implementation | More edge cases to maintain |
| HTML5 drag events | Basic desktop reordering | Familiar browser API | Uneven touch and keyboard experience |
| Sortable JavaScript library | Connected lists and complex layouts | Mature drop-zone behaviour | Extra asset weight and upgrade work |
| Drupal form widgets | Structured entity or field ordering | Strong server-side integration | Less flexible visual interaction |
| AJAX save after each move | Small editorial lists | Immediate persistence | More requests and conflict handling |
| Submit all changes together | Large or multi-step forms | Fewer requests, easier batching | Unsaved changes can be lost |
A dependable drag-and-drop interface in Drupal is therefore a combination of visual feedback, behaviour-safe JavaScript, secure persistence, and accessible controls. Treat the browser as a convenient editing surface rather than the authority on what may be changed. With that foundation, the same pattern can support menus, editorial queues, media collections, and dashboard layouts across Australian Drupal projects.