JavaScript
Article

Accessible Drag and Drop with Multiple Items

By James Edwards

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:

Free Guide:

7 Habits of Successful CTOs

"What makes a great CTO?" Engineering skills? Business savvy? An innate tendency to channel a mythical creature (ahem, unicorn)? All of the above? Discover the top traits of the most successful CTOs in this free guide.

Comments
MatsSvensson

Do people really use things like this with only keyboard?

I haven't read trough all the text.
But by playing around with the examples, i have no idea how to drag&drop with my keyboard.

Is there some standard for what keys do what in situations like this?

I myself never use the keyboards for anything but to type in text, everything else is 100% mouse.
Mostly because i can never memorize any programs secret hidden handshakes,

Anyway, there seems to be no obvious way to figure it out, and i see no real visual indicators.

brothercake

Look at the very last demo -- that's the final one, which has complete keyboard interaction. You might find it easier to use on a page of its own: http://jspro.brothercake.com/multidrag/demo4b.html

You Tab to items, press Space to select (or Modifier + Space for multiple selections), then Tab to a target container, then press Enter to drop.

Those are the standard keystrokes, described by the ARIA Authoring Guidelines. (Except for drop -- the guidelines say to use Ctrl+M, and this script does also implement that -- but I didn't think that was very intuitive, so I added Enter as well).

As to whether people really use things like this with the keyboard -- well in most cases, they don't, because the interface doesn't support keyboard navigation! If you build it, they will come smile

But it's not about providing for user preferences -- it's really not for the benefit of power users or people who prefer to use the keyboard (although it does have that benefit) -- it's for people who can ONLY use the keyboard. For that group of users, these keystrokes are the most intuitive and obvious choices, because they're the keystrokes that are typically used for selecting and actuating things.

MatsSvensson

Aha, like for browsers for blind people etc?
I would not have guessed those keys in a meelion years, without some kind of manual.

brothercake

Yeah exactly -- for screenreader users, and for sighted keyboard users (eg. for people who have a hand tremor and can't keep a mouse steady)

If you don't typically use the keyboard for stuff, then it makes sense that you wouldn't have an immediate sense of what keystrokes are obvious for certain things.

But frequent keyboard users are more likely to. For example, checkboxes are activated with Space, Enter is used to press buttons (and sometimes Space is used for that as well), so these are the logical choices.

Having said that, there is a case for saying that this kind of interaction could have instructions. Perhaps a popup tooltip that appears the first time you Tab to an item, that explains what the keystrokes are.

sime_vidas

The keyboard functionality is awesome! Please implement touch next.

brothercake

Touch dragging is complicated, because dragging is the same physical action as swiping and scrolling, so it would need some kind of pre-drag initiation -- probably a longpress.

But yeah, that's the plan smile

Klara

First of all, thank you! This is amazing!

I'm including it into a simple web page builder. That being said, I have very limited knowledge of javascript, and intend to pass it off to wiser javascript developers than I to get the whole page building experience going - I'm just building the static template and trying to include as much accessibility in as possible, in advance.

But in testing my rudimentary version, which totally works from a strictly keyboard/mouse perspective, I could only get VoiceOver to indicate when something was selected. Neither NVDA or JAWS seemed to pick up that I had clicked something. Do you have any advice on whether I need to use aria-selected somewhere to get verbal affirmation and would that even work? I admit, I'm fresh-off-the-boat on learning the many aria roles, but I'm trying! Also, do you know of any aria role that I can attach to the drop-zone as a verbal indicator that someone could drop something there?

brothercake

Yeah screenreader support is rather patchy. The script is using the correct attributes -- aria-grabbed for items and aria-dropeffect for drop target containers -- but there's not much communication of those states with different screenreaders.

For example, I tested Jaws 16 + Firefox and it would announce the items as draggable, but wouldn't tell you that they were grabbed, or that a focused container was a drop target.

NVDA + Firefox was slightly better, announcing that an item has been grabbed (although ironically, not telling you that it's a draggable item in the first place!), and also announcing when the item has been dropped (but not telling you where you can drop it!)

It's hard to know how to improve things. On the one hand, the "correct" approach would be to stick to the current implementation and just wait for the screenreaders to support it properly.

But the useful approach would be to do whatever it takes to create a coherent interface. Maybe aria-selected could be helpful there -- perhaps using aria-grabbed and aria-selected on each of the items would be a good approach. Maybe structural labels in the drop containers would provide useful information to help users identify them.

I'll be doing a follow-up article to this in the next few weeks, so I'll investigate some of these possibilities and take a view then smile

meditator

Thanks James for the wonderful contribution.
Could you please help me out with a couple of things. I would like to integrate this with php using ajax to update some fields in the database. Could you guide me where to put the ajax script and how to get the item/element ID and the ID of the container to pass using ajax?
Thanks a lot for your help.

raja

Thank you! This is amazing!
I am new to javascript i need to use this in my web site and my html markup is different i have "p" and "A" tags in side each LI, and it is not dragging at all. Please tell where and what changes i have to make in code to work. here is my sample html markup.

    <ol data-draggable="target">
      <li data-draggable="item"><p>Item 0 <a href="#">my link</a></p></li>          
      <li data-draggable="item"><p>Item 1</p></li>
      <li data-draggable="item"><p>Item 2 <a href="#">my link</a></p></li>
      <li data-draggable="item"><p>Item 3</p></li>
   </ol>
  <ol data-draggable="target">
    <li data-draggable="item"><p>Item 4</p></li>
     <li data-draggable="item"><p>Item 5 <a href="#">my link</a></p></li>
  </ol>
  <ol data-draggable="target"> 
      <li data-draggable="item"><p>Item 6 <a href="#">my link</a></p></li>
      <li data-draggable="item"><p>Item 7</p></li>
  </ol>
   <ol data-draggable="target">
     <li data-draggable="item"><p>Item 8</p></li>
  </ol>
   <script src="demo4a.js" type="text/javascript"></script>
Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in JavaScript, once a week, for free.