Accessible Drag and Drop with Multiple Items

Share this article

In this article, I’d like to show you how to extend the capabilities of HTML5 drag and drop — so it can handle multiple elements, and support keyboard interaction, for sighted and screen reader users.

I’m going to assume that you already have a passing familiarity with the drag and drop API, but if not, have a look at this introductory article (ignoring the references to Modernizr, which you won’t need).

The basic approach to dragging multiple elements is actually pretty trivial — we simply need to remember the drag data for more than one element. We could do that using the dataTransfer object, or we can just use a separate array (which is what we’re going to do). So why write a whole article about it?

Why — because although the data is simple, the interface is rather more complex.

We’ll need to do some extra work to implement a pre-selection mechanism, and we’ll need to make it work from the keyboard (since native drag and drop doesn’t support this).

Note: This article won’t cover touch events, nor provide a polyfill for older browsers. Furthermore, the approach we take will only work within a single page, it won’t support dragging between windows. Although the drag and drop API does support this, there’s no straightforward way of making it keyboard accessible.

Basic Drag and Drop

So let’s start with a functional example which defines the basic drag and drop events, allowing a single element to be dragged with the mouse between two containers:

See the Pen Basic Drag and Drop by SitePoint (@SitePoint) on CodePen.

There’s a few things in there that might differ from other demos you’ve seen.

The first is the way it maintains an item reference for the element being dragged, rather than passing the element’s ID through the dataTransfer object (although we do have to pass something or the whole operation will fail in Firefox; it could be anything, so an empty string will do):

var item = null;

document.addEventListener('dragstart', function(e)
{
  item = e.target;
	
  e.dataTransfer.setData('text', '');

}, false);

This simplifies the demo by avoiding the need for the elements to have IDs, and would make it much easier to extend into a server-side application (like a CMS), where element IDs may not be easily knowable. This approach will also be the basis of multiple selection, where the item reference will become an items array.

The next significant thing is the omission of the event properties, effectAllowed and dropEffect. These can take a value such as "copy" or "move", and are supposed to control which actions are allowed and which cursor the browser will show. However, browser implementation is inconsistent, so there’s not much point including them.

Finally, note how the underlying HTML does not include any draggable attributes:

<ol data-draggable="target">
  <li data-draggable="item">Item 0</li>
  <li data-draggable="item">Item 1</li>
  <li data-draggable="item">Item 2</li>
  <li data-draggable="item">Item 3</li>
</ol>

Although most examples use these attributes in static HTML, I think that contradicts the principle of separation — since it allows the element to be dragged, but you can’t actually drop it anywhere without the accompanying JavaScript. So instead, I’ve used static data attributes to identify draggable elements, and then used scripting to apply the draggable attribute to browsers which pass feature detection.

This approach also provides the opportunity to exclude any broken implementations, for the cases where feature detection fails. Opera 12 or earlier is excluded using the window.opera test, because its implementation is quite buggy, and it’s not worth spending time on anymore.

Accessible Drag and Drop

Accessibility is a fundamental design principle, and it’s always easier to it get right when considered in those terms. So before we go any further with this demo, we must have a clear idea of the requirements for keyboard and screen reader accessibility.

The ARIA Authoring Practices has a section on drag and drop, which outlines the attributes we’ll need, and has guidelines on how the interactions should work. To summarize:

  1. Draggable elements are identified with aria-grabbed="false", and must be navigable with the keyboard.
  2. Provide a mechanism for the user to select which elements they want to drag, and the recommended keystroke is Space. When an element is selected for dragging, its aria-grabbed attribute is set to "true".
  3. Provide a mechanism for the user to indicate that they’ve finished making selections, and the recommended keystroke is Control+M.
  4. Target elements are then identified with aria-dropeffect, with a value that indicates which actions are allowed, such as "move" or "copy". At this point in the process, target elements must also be navigable with the keyboard.
  5. When the user arrives at a target element, provide a mechanism for them to perform the drop action, and the recommended keystroke is also Control+M.
  6. Users can cancel the entire operation at any time, by pressing the Escape key.
  7. When the action is completed or aborted, clean-up the interface by setting all aria-dropeffect attributes to "none" (or removing them), and all aria-grabbed attributes to "false".

Now the two ARIA attributes, aria-grabbed and aria-dropeffect, must be used according to specification. However, the events and interactions are simply recommendations, and we don’t have to follow them slavishly. Nonetheless, we should (and will) follow these recommendations as closely as possible, because they’re the closest thing we have to a normative reference.

And for the most part, I think it makes sense. But I do take issue with both uses of the Control+M keystroke. I’m not convinced that it’s really necessary to have an end-of-selection keystroke at all, and I don’t the keystroke itself is very intuitive for sighted keyboard users.

So I think the best approach is to supplement those recommendations:

  • We will implement the end-of-selection keystroke, but it won’t be required nor prevent further selection, it will simply be a shortcut for moving focus to the first drop target.
  • We will use Control+M for both keystrokes, but we’ll also allow the drop action to be triggered with the Enter key.

The guidelines also talk about implementing multiple selection using modifiers. The recommendation is to use Shift+Space for contiguous selection (i.e. selecting all items between two end points), and Control+Space for non-contiguous selection (i.e. selecting arbitrary items). Those are clearly the best modifiers to use … however, the Mac equivalent of Control+Space would be Command+Space, but that’s already bound to a system action and can’t be suppressed by JavaScript. Maybe we could use Control, but Mac users won’t be expecting that, since the Control key is mostly only used for triggering a right-click with a one-button mouse. The only remaining choice is Shift, but that’s earmarked for contiguous selection!

So for the sake of simplicity, and to side-step this problem, we’re not going to implement contiguous selection. The simplest thing we can do for now is to support all three modifiers for non-contiguous selection — Command+Space, Control+Space or Shift+Space are all treated the same way on every platform — then whatever a particular user can trigger, and whatever they think makes sense, is available.

Multiple Selection

Now that we have a solid idea of what’s required for keyboard accessibility, we can start to implement selection events for both the mouse and the keyboard. Selecting an item will inevitably mean adding a class or attribute to indicate the selection, so let’s use aria-grabbed in both cases. This will give us a handy and semantically appropriate styling hook, and will make it possible to support cross-modality — i.e. the ability to support any mixture of mouse and keyboard interaction, rather than assuming that only one or the other will be used.

So let’s start by updating the initial code that applied the draggable attribute, so it also applies default values for aria-grabbed and tabindex:

for(var 
  items = document.querySelectorAll('[data-draggable="item"]'), 
  len = items.length, 
  i = 0; i < len; i ++)
{
  items[i].setAttribute('draggable', 'true');
  items[i].setAttribute('aria-grabbed', 'false');
  items[i].setAttribute('tabindex', '0');
}

Then we can implement the selection functionality, beginning with the mouse events.

Mouse Selection

In this demo, you can make single selections with the mouse, or multiple selections using a modifier (and that’s all you can do). But note how multiple selections can only be made within a single container, you can’t make selections across multiple containers:

See the Pen Mouse Selection by SitePoint (@SitePoint) on CodePen.

Limiting selections to a single ‘owner’ container is deliberate, and implemented in the scripting with the selections.owner reference. The purpose of that behavior is to simplify the interactions for users, so that it’s always clear where the selections came from, and where they can be moved to (i.e. anywhere else).

If you look at the JavaScript code, you’ll also see how we’ve added some selection functions — addSelection, removeSelection and clearSelections — which add and remove aria-grabbed to indicate item selection, and manage the selections object’s data.

The mouse interactions which call those functions use two mouse events. Single selection or global reset (without a modifier) is triggered by a mousedown event, but deselection or multiple selection (with a modifier) is deferred to the mouseup event.

Using two events is essential for creating intuitive interactions, which meet user expectations for how modified and unmodified clicks should behave. Most of it’s pretty straightforward, but there’s one potential contradiction:

  1. Modified click on an already-selected item should deselect it.
  2. Modified or unmodified drag on any already-selected item should move the whole selection without changing it.

It’s impossible to implement both of those using only mousedown and dragstart events, because they would directly contradict each other. But if we use the mouseup event for modified selection and deselection, then we can satisfy both expectations.

There’s also a special case we have to handle, which is where the user attempts a modified drag on an item which isn’t selected. Since modified selection is deferred to mouseup, that action would initiate dragging on an item which won’t be included in the drop. But we can fix that by using the dragstart event to automatically select the target item.

Keyboard Selection

Keyboard selection is implemented using the model we looked at earlier — you can Tab to items, and then use Space to make selections, with a modifier for multiple selections:

See the Pen Keyboard Selection by SitePoint (@SitePoint) on CodePen.

The JavaScript for this example uses the same selection functions as for the mouse, which are called using a single keydown event differentiated by keyCode (note: the newest code is at the bottom of the JavaScript panel).

The keydown also handles the Escape key on any target element, to implement the global reset keystroke. For the mouse, you can reset the selections by clicking anywhere outside the containers, and so pressing Escape is the keyboard equivalent of that.

So now we have a complete set of selection events. And crucially, since we’ve used aria-grabbed for both sets of events, there’s no functional difference in the result of mouse or keyboard interaction — a user can select with the keyboard then drag with the mouse, or vice versa — and the state of the interface will be correct for any combination.

Dragging the Selection

Having made one or more selections, the user needs to know where they can drag the items to (i.e. which containers are valid drop targets). Once again, we can use the same attribute for both mouse and keyboard interaction — in this case aria-dropeffect, which we first apply to all the target containers by default:

for(var 
  targets = document.querySelectorAll('[data-draggable="target"]'), 
  len = targets.length, 
  i = 0; i < len; i ++)
{
  targets[i].setAttribute('aria-dropeffect', 'none');
}

When targets are available, their aria-dropeffect changes from "none" to "move". This lets screen readers know that the elements are now valid targets, and can also be used as a visual styling hook:

[data-draggable="target"]
{
  border-color:#888;

  background:#ddd;
  color:#555;
}
[data-draggable="target"][aria-dropeffect="move"]
{
  border-color:#68b;

  background:#fff;
}

(Note that, although the only visual differences are colors, the change in contrast is enough that it’s not relying on color alone to convey information.)

Keyboard Dragging

For keyboard interaction, the "move" state is applied as soon as any selections are made. Keyboard interaction doesn’t have a ‘dragging’ state as such — targets are chosen by users when they Tab to a target container, and therefore the target containers must be available whenever selections exist:

See the Pen Keyboard Dragging by SitePoint (@SitePoint) on CodePen.

You’ll see how the JavaScript code has two new functions — addDropeffect and clearDropeffect — which manage aria-dropeffect and tabindex on the target containers.

But when targets are available, we also remove tabindex from all the items inside them, to improve usability for keyboard users by reducing the amount of stuff in the tab order. Since items outside the owner container can’t be selected, they needn’t (and arguably shouldn’t) be in the tab order at all. Such items also have their aria-grabbed attribute temporarily removed, so that screen readers don’t continue to announce them as draggable items.

These functions are called by the same keydown events as we used for selection — adding dropeffect whenever any items are selected, and clearing it again when they’re all reset. That event also includes the end-of-selection keystroke (Control+M for PC or Command+M for Mac), which is simply a shortcut to set focus on the first available target container.

We also need extra focus management for the Escape keystroke, since the keystroke might be used while focus is on a target container, and has the effect of removing tabindex from that very container. That would cause the focus position to be reset back to the top of the page, so we have to manage the focus explicitly, to prevent that. The simplest solution is to set the focus back on the last-selected item.

You might notice how the calls to addDropeffect and clearDropeffect are somewhat over-egged — for example, every selection event calls addDropeffect, even if we already have selections. We need to do this to support cross-modality — i.e. we can’t assume that those previous selections were also made with the keyboard. But similarly, we can’t assume that a mouse selection follows another mouse selection, so we need to update the mouse events as well as the keyboard events, to restore the mode-applicable state whenever either are used.

Mouse Dragging

For mouse dragging, we don’t add aria-dropeffect until dragging has actually started, at which point all the available target containers become highlighted:

See the Pen Mouse Dragging by SitePoint (@SitePoint) on CodePen.

The complication with mouse interaction is the need to create a ‘hover’ state. For the keyboard, it was simple to add a :focus rule to the target styles, but we can’t use :hover for mouse interaction because the hover state never occurs during native dragging.

What we can do, however, is to use native drag events to monitor the mouse position, and then implement a scripted hover state. The events we need are called dragenter and dragleave, which are conceptually similar to mouseover and mouseout, but with one important difference — the drag events don’t have a relatedTarget property. So when you drag the mouse into a target element (for example), the dragenter event can’t tell you which element the mouse is moving away from.

But we can fix that by maintaining a manual reference — since a dragleave event is always preceded by a dragenter event, the target of that dragenter must logically be the same element as the related-target of the dragleave. So if we record that reference in the former, we can use it in the latter:

var related = null;

document.addEventListener('dragenter', function(e)
{
  related = e.target;

}, false);

document.addEventListener('dragleave', function(e)
{
  //the related var is this event's relatedTarget element

}, false);

To make that useful, we also need this getContainer function, which returns a target container reference from any inner element:

function getContainer(element)
{
  do
  {
    if(element.nodeType == 1 && element.getAttribute('aria-dropeffect'))
    {
      return element;
    }
  }
  while(element = element.parentNode);

  return null;
}

We need this because the dragleave events will bubble — just like mouseout, they’ll fire when the mouse moves over any element inside the same container. We need to filter those events so they don’t reset the hover state, so getContainer is used to normalize all references to their parent container, then we only respond to the ones where that reference has changed:

document.addEventListener('dragleave', function(e)
{
  var droptarget = getContainer(related);

  if(droptarget != selections.droptarget)
  {
    //... update target highlighting

    selections.droptarget = droptarget;
  }
}, false);

Dropping the Selection

The final interaction is to drop the items into a target container. At this point in the process, we’ll have an items array with references to all the items being dragged, so moving that selection is simply a case of appending the items to the drop target container:

for(var len = items.length, i = 0; i < len; i ++)
{
  droptarget.appendChild(items[i]);
}

We don’t need to reference the dataTransfer object at all, because we’re not using it.

Mouse Droppings

For mouse interaction, we’ll already have a droptarget reference created by the dragleave event (or it will be null if there is no current target). So we simply need a dragend event to handle what happens when the mouse is released:

See the Pen Mouse Droppings by SitePoint (@SitePoint) on CodePen.

What’s significant here, is that there’s no drop event.

The drop event nominally means that the element has been dropped into a valid target, while the dragend event simply means that the mouse has been released (i.e. whether or not the drop target was valid). But in practice, within the context of a single document, there’s actually no difference between those two events — both events fire every time, and a ‘valid drop target’ is nothing more than ‘whatever we say it is’!

However we do have to deal with a browser event bug — Mac/Webkit won’t fire the drop event if the Command key is still held down. Consequently, releasing the mouse would leave items still grabbed and targets still active, so to avoid that problem, we simply use the dragend event for everything.

Within that event, we have one condition to handle drop actions (when the target was valid), and one for handling reset (in either case). This split in conditional logic ensures that we don’t reset the grabbed state if there’s no valid drop target — so the user can try again without having to re-select the items.

Keyboard Dropping

For keyboard interaction, dropping the selection means navigating to a target container, and then pressing the drop keystroke (either Enter or Modifier+M). We don’t have an active droptarget reference, but then we don’t need one — the drop target is the element that receives the keystroke:

See the Pen Keyboard Dropping by SitePoint (@SitePoint) on CodePen.

So the drop action is simply a keydown event on the target containers, which checks the target element and keystroke, and responds accordingly — moving the selection, then cleaning up the interface. In this case we’re only dealing with valid drop actions — there’s no such thing as invalid drops with keyboard navigation, because only valid targets receive the keystroke in the first place (and we’ve already implemented user abort via the Escape key).

But we do have to manage the focus, in the same way as we did for abort — since the element that currently has the focus is about to lose its tabindex, we must move the focus somewhere explicit, to prevent it from being reset. Again, the most simply-intuitive solution is to focus the last-selected item.

And that’s it!

There are, of course, many possibilities for additional enhancements, among them:

  • Adding support for touch and/or pointer events.
  • Creating a polyfill for older browsers.
  • Sorting the selection when it’s dropped.
  • The choice of “copy” or “move” using Modifier+Drop.
  • Contiguous selection using Shift+Select and/or keyboard-range selection using Shift+Arrow.
  • Using a custom drag-ghost to indicate how many items are being dragged.
  • Visually dimming the selection when items are dragged by mouse, or when the end-of-selection keystroke is used.

I’ll talk about some of these possibilities in a future article.

In the meantime, you can grab a copy of the files from our GitHub repo:

Frequently Asked Questions (FAQs) about Accessible Drag and Drop

What is the concept of Accessible Drag and Drop?

Accessible Drag and Drop is a feature that allows users to interact with a website or application by clicking and dragging elements on the screen. This feature is particularly useful for tasks such as rearranging items, moving objects from one location to another, or selecting multiple items. It’s designed to be accessible, meaning it can be used by everyone, including those with disabilities. It’s built with HTML5 and JavaScript, which are widely supported by modern web browsers.

How can I make my drag and drop feature more accessible?

Making your drag and drop feature more accessible involves considering users who may not be able to use a mouse or touch screen. This can be achieved by ensuring keyboard accessibility, providing alternative text for screen readers, and implementing ARIA (Accessible Rich Internet Applications) roles and properties. These measures will ensure that your drag and drop feature can be used by everyone, regardless of their physical abilities or the devices they use.

How can I implement multiple draggable elements?

Implementing multiple draggable elements involves assigning the draggable attribute to each element you want to be draggable. This can be done in the HTML code by adding the attribute draggable=”true” to each element. In JavaScript, you can use the setAttribute method to assign the draggable attribute to multiple elements at once. Remember to add event listeners for the dragstart and dragend events to control what happens when the user starts and ends a drag operation.

How can I create multiple droppable areas?

Creating multiple droppable areas involves defining areas in your HTML where draggable elements can be dropped. This can be done by adding event listeners for the dragover and drop events to these areas. The dragover event is used to specify where the draggable elements can be dropped, while the drop event is used to define what happens when a draggable element is dropped.

How can I control the order of draggable elements?

Controlling the order of draggable elements can be achieved by using the HTML5 Drag and Drop API’s dataTransfer object. This object allows you to store data about the draggable element, such as its ID or index, which can be used to determine its position when it’s dropped. You can then use this information to rearrange the other elements accordingly.

How can I drag and drop multiple elements at once?

Dragging and dropping multiple elements at once is a more complex task that requires additional JavaScript code. You’ll need to keep track of all the selected elements, for example by storing their IDs in an array, and then move all these elements when a drag operation is started. When the elements are dropped, you’ll need to update their positions based on the drop location.

How can I prevent certain elements from being draggable?

Preventing certain elements from being draggable can be done by not assigning the draggable attribute to these elements, or by setting it to “false”. In JavaScript, you can use the removeAttribute method to remove the draggable attribute from an element.

How can I customize the appearance of draggable elements?

Customizing the appearance of draggable elements can be done with CSS. You can change the cursor style during a drag operation, add a drag effect, or change the element’s appearance when it’s being dragged. This can be done by adding and removing CSS classes in response to the dragstart and dragend events.

How can I handle errors during a drag and drop operation?

Handling errors during a drag and drop operation involves adding error handling code to your JavaScript. This can include checking for invalid drop locations, ensuring that the dataTransfer object contains valid data, and providing feedback to the user when an error occurs.

How can I test the accessibility of my drag and drop feature?

Testing the accessibility of your drag and drop feature can be done by using accessibility testing tools, such as the WAVE Evaluation Tool or the Accessibility Insights for Web extension. These tools can identify potential accessibility issues, such as missing alternative text or keyboard accessibility problems. Additionally, you should test your feature with a variety of devices and assistive technologies to ensure it works for all users.

James EdwardsJames Edwards
View Author

James is a freelance web developer based in the UK, specialising in JavaScript application development and building accessible websites. With more than a decade's professional experience, he is a published author, a frequent blogger and speaker, and an outspoken advocate of standards-based development.

drag and drophtml5 apisjameshkeyboard accessibility
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week