WCAG 2.5.7: Dragging Movements

WCAG 2.5.7: Dragging Movements (Level AA)

The WCAG 2.1 Success Criterion 2.5.7, “Dragging Movements,” requires that if a user interface component or functionality relies on a dragging movement for its operation, an alternative method must be provided to achieve the same outcome without dragging. This criterion aims to ensure that users who cannot perform precise dragging gestures due to motor impairments or other factors can still interact with the content effectively.

Why This Criterion Matters for Accessibility

Dragging movements, while intuitive for many users with a mouse or touchscreen, can pose significant barriers for others. This criterion addresses a range of accessibility challenges:

  • Motor Impairments: Users with conditions like tremors, Parkinson’s disease, or those using assistive technologies such as mouth sticks, head pointers, or switch control may find it difficult or impossible to perform the precise, sustained movements required for dragging. Holding down a pointer while simultaneously moving it to a specific target can be an incredibly challenging task.
  • Input Device Limitations: Users relying on trackpads, touchscreens with low accuracy, or specialized input devices may struggle with the precision needed for dragging.
  • Cognitive Load: For some users with cognitive impairments, multi-step operations like dragging can be confusing or overwhelming compared to simpler, discrete actions (e.g., a single click or tap).
  • Temporary Disabilities: A user with a temporary injury, such as a sprained wrist, might find dragging painful or difficult.
  • Situational Limitations: Users in unstable environments (e.g., on a moving train) or those who are multitasking (e.g., holding a baby) may have reduced ability to perform precise dragging.

By providing non-dragging alternatives, we create a more inclusive and usable experience for a much wider audience, enhancing the overall flexibility and robustness of the user interface.

Understanding Success Criterion 2.5.7 Requirements

The full text of Success Criterion 2.5.7 states:

“Dragging Movements: All functionality that uses a dragging movement for operation can be achieved by a single pointer without dragging, unless dragging is essential or the function is determined by the user agent and not modified by the author.”

Let’s break down the key terms:

  • Dragging movement: This refers to an action where a pointer (e.g., mouse cursor, finger on a touchscreen) is held down and moved across the screen before being released. It involves a ‘pointer down’ event, movement, and a ‘pointer up’ event, where the start and end points are distinct.
  • Single pointer without dragging: This means an alternative method that can be accomplished with a single, discrete action, such as a click, tap, or pressing a button. This alternative must offer the same functionality as the dragging method.
  • Unless dragging is essential: This exception applies when the very nature of the function is defined by the dragging action. Examples include drawing applications where the act of dragging creates lines, or a virtual musical instrument where sliding a finger across keys is the intended input. Reordering items in a list, however, is generally NOT considered essential dragging, as the goal is to change order, not the dragging action itself.
  • Function is determined by the user agent and not modified by the author: This exception covers situations where the dragging functionality is provided natively by the browser or operating system and the author has not altered its behavior. Common examples include native scrollbars or resizing a browser window.

Practical Guidelines for Compliance

To comply with WCAG 2.5.7, focus on providing robust and equivalent alternatives:

  1. Identify Dragging Interactions: Review all components and functionalities on your site or application that require a dragging movement.
  2. Provide an Equivalent Alternative: For each identified dragging interaction, design and implement an an alternative method that achieves the exact same result.
  3. Keyboard Operability: Ensure the alternative method is fully operable via keyboard. This often means providing buttons or menu items that can be focused and activated using the tab key and enter/space bar.
  4. Clear Affordances: Make sure the alternative methods are clearly visible and understandable to all users. Don’t hide them in obscure menus.
  5. Maintain Functionality: The alternative method must offer the same level of functionality and control as the dragging method. For example, if dragging allows moving multiple items, the alternative should also allow this.
  6. Test Thoroughly: Test your implementations with various input methods (mouse, keyboard, touch) and, ideally, with users who have motor impairments or use assistive technologies.

Examples of Correct and Incorrect Implementations

Scenario: Reordering Items in a List

A common use case for dragging is reordering elements in a list or grid.

Incorrect Implementation (Dragging Only)

This example only allows reordering by dragging, making it inaccessible for many users.

<ul id="draggable-list">
  <li draggable="true">Item 1</li>
  <li draggable="true">Item 2</li>
  <li draggable="true">Item 3</li>
</ul>

<style class="language-css">
  #draggable-list li {
    padding: 10px;
    border: 1px solid #ccc;
    margin-bottom: 5px;
    background-color: #f9f9f9;
    cursor: grab;
  }
  #draggable-list li.dragging {
    opacity: 0.5;
  }
</style>

<script class="language-javascript">
  const list = document.getElementById('draggable-list');
  let draggedItem = null;

  list.addEventListener('dragstart', (e) => {
    draggedItem = e.target;
    setTimeout(() => {
      e.target.classList.add('dragging');
    }, 0);
  });

  list.addEventListener('dragend', (e) => {
    e.target.classList.remove('dragging');
    draggedItem = null;
  });

  list.addEventListener('dragover', (e) => {
    e.preventDefault();
    const afterElement = getDragAfterElement(list, e.clientY);
    const currentItem = document.querySelector('.dragging');
    if (currentItem) {
      if (afterElement == null) {
        list.appendChild(currentItem);
      } else {
        list.insertBefore(currentItem, afterElement);
      }
    }
  });

  function getDragAfterElement(container, y) {
    const draggableElements = [...container.querySelectorAll('li:not(.dragging)')];

    return draggableElements.reduce((closest, child) => {
      const box = child.getBoundingClientRect();
      const offset = y - box.top - box.height / 2;
      if (offset < 0 && offset > closest.offset) {
        return { offset: offset, element: child };
      } else {
        return closest;
      }
    }, { offset: -Number.MAX_VALUE }).element;
  }
</script>

Correct Implementation (Dragging with Alternative Controls)

This example provides visible ‘Move Up’ and ‘Move Down’ buttons for each item, allowing reordering without dragging. The dragging functionality is still available as an enhancement.

<ul id="draggable-list-alt">
  <li id="item-1" draggable="true">Item 1 <button class="move-up" aria-label="Move Item 1 up">⬆️</button> <button class="move-down" aria-label="Move Item 1 down">⬇️</button></li>
  <li id="item-2" draggable="true">Item 2 <button class="move-up" aria-label="Move Item 2 up">⬆️</button> <button class="move-down" aria-label="Move Item 2 down">⬇️</button></li>
  <li id="item-3" draggable="true">Item 3 <button class="move-up" aria-label="Move Item 3 up">⬆️</button> <button class="move-down" aria-label="Move Item 3 down">⬇️</button></li>
</ul>

<style class="language-css">
  #draggable-list-alt li {
    padding: 10px;
    border: 1px solid #ccc;
    margin-bottom: 5px;
    background-color: #f9f9f9;
    cursor: grab;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  #draggable-list-alt li.dragging {
    opacity: 0.5;
  }
  #draggable-list-alt button {
    margin-left: 5px;
    padding: 5px 8px;
    cursor: pointer;
  }
</style>

<script class="language-javascript">
  const listAlt = document.getElementById('draggable-list-alt');
  let draggedItemAlt = null;

  // --- Dragging Logic --- //
  listAlt.addEventListener('dragstart', (e) => {
    draggedItemAlt = e.target;
    setTimeout(() => {
      e.target.classList.add('dragging');
    }, 0);
  });

  listAlt.addEventListener('dragend', (e) => {
    e.target.classList.remove('dragging');
    draggedItemAlt = null;
    updateAriaLabels(); // Update labels after reordering via drag
  });

  listAlt.addEventListener('dragover', (e) => {
    e.preventDefault();
    const afterElement = getDragAfterElement(listAlt, e.clientY);
    const currentItem = document.querySelector('.dragging');
    if (currentItem) {
      if (afterElement == null) {
        listAlt.appendChild(currentItem);
      } else {
        listAlt.insertBefore(currentItem, afterElement);
      }
    }
  });

  function getDragAfterElement(container, y) {
    const draggableElements = [...container.querySelectorAll('li:not(.dragging)')];

    return draggableElements.reduce((closest, child) => {
      const box = child.getBoundingClientRect();
      const offset = y - box.top - box.height / 2;
      if (offset < 0 && offset > closest.offset) {
        return { offset: offset, element: child };
      } else {
        return closest;
      }
    }, { offset: -Number.MAX_VALUE }).element;
  }

  // --- Alternative Control Logic (Buttons) --- //
  listAlt.addEventListener('click', (e) => {
    const listItem = e.target.closest('li');
    if (!listItem) return;

    if (e.target.classList.contains('move-up')) {
      const prevItem = listItem.previousElementSibling;
      if (prevItem) {
        listAlt.insertBefore(listItem, prevItem);
        updateAriaLabels();
        listItem.focus(); // Keep focus on moved item
      }
    } else if (e.target.classList.contains('move-down')) {
      const nextItem = listItem.nextElementSibling;
      if (nextItem) {
        listAlt.insertBefore(nextItem, listItem);
        updateAriaLabels();
        listItem.focus(); // Keep focus on moved item
      }
    }
  });

  // Update aria-labels dynamically if item order changes visually.
  // This ensures screen readers announce the correct item associations.
  function updateAriaLabels() {
    const items = listAlt.querySelectorAll('li');
    items.forEach((item, index) => {
      const textContent = item.textContent.replace(/s*⬆️s*⬇️s*$/, '').trim(); // Remove button text for label
      const upButton = item.querySelector('.move-up');
      const downButton = item.querySelector('.move-down');
      // Disable buttons at the ends of the list for better UX
      if (upButton) {
        upButton.disabled = (index === 0);
        upButton.setAttribute('aria-label', `Move ${textContent} up, currently position ${index + 1} of ${items.length}`);
      }
      if (downButton) {
        downButton.disabled = (index === items.length - 1);
        downButton.setAttribute('aria-label', `Move ${textContent} down, currently position ${index + 1} of ${items.length}`);
      }
    });
  }
  // Call updateAriaLabels initially and after any reordering operation.
  updateAriaLabels();
</script>

Scenario: Resizing a Window/Panel

When providing resizable panels or windows within a web application.

Incorrect Implementation (Resizing Only by Dragging)

A panel that can only be resized by dragging its border.

<div class="resizable-panel">
  <p>Content of the panel.</p>
  <div class="resize-handle"></div>
</div>

<style class="language-css">
  .resizable-panel {
    width: 300px; /* Initial width */
    height: 200px; /* Initial height */
    border: 1px solid blue;
    position: relative;
    overflow: auto;
    resize: none; /* Explicitly prevent default browser resize behavior */
  }
  .resize-handle {
    width: 10px;
    height: 10px;
    background: blue;
    position: absolute;
    bottom: 0;
    right: 0;
    cursor: se-resize;
  }
</style>

<script class="language-javascript">
  const panel = document.querySelector('.resizable-panel');
  const handle = document.querySelector('.resize-handle');
  let isResizing = false;

  handle.addEventListener('mousedown', (e) => {
    isResizing = true;
    document.body.style.cursor = 'se-resize';
    e.preventDefault(); // Prevent text selection during drag
  });

  document.addEventListener('mousemove', (e) => {
    if (!isResizing) return;
    // Ensure panel stays within reasonable bounds, e.g., min-width/height
    const newWidth = Math.max(50, e.clientX - panel.getBoundingClientRect().left);
    const newHeight = Math.max(50, e.clientY - panel.getBoundingClientRect().top);
    panel.style.width = newWidth + 'px';
    panel.style.height = newHeight + 'px';
  });

  document.addEventListener('mouseup', () => {
    isResizing = false;
    document.body.style.cursor = '';
  });
</script>

Correct Implementation (Resizing with Input Fields)

The panel can be resized by dragging OR by entering specific pixel values into input fields. Both methods are synchronized, and the resize handle is keyboard operable.

<div class="resizable-panel-alt">
  <p>Content of the panel.</p>
  <div class="resize-controls">
    <label for="panel-width">Width:</label> <input type="number" id="panel-width" value="300" min="50" aria-controls="resizable-panel-alt">px
    <label for="panel-height">Height:</label> <input type="number" id="panel-height" value="200" min="50" aria-controls="resizable-panel-alt">px
  </div>
  <div class="resize-handle-alt" role="separator" aria-orientation="vertical" aria-label="Resize panel" tabindex="0"></div>
</div>

<style class="language-css">
  .resizable-panel-alt {
    width: 300px; /* Initial width */
    height: 200px; /* Initial height */
    border: 1px solid green;
    position: relative;
    overflow: auto;
    margin-top: 20px;
    resize: none; /* Explicitly prevent default browser resize behavior */
  }
  .resize-handle-alt {
    width: 10px;
    height: 10px;
    background: green;
    position: absolute;
    bottom: 0;
    right: 0;
    cursor: se-resize;
    touch-action: none; /* Prevents default touch behavior for better drag experience */
  }
  .resize-controls {
    margin-top: 10px;
    padding: 5px;
    background-color: #e0ffe0;
    border-top: 1px solid #c0e0c0;
  }
  .resize-handle-alt:focus {
    outline: 2px solid blue; /* Focus style for keyboard users */
  }
</style>

<script class="language-javascript">
  const panelAlt = document.querySelector('.resizable-panel-alt');
  const handleAlt = document.querySelector('.resize-handle-alt');
  const widthInput = document.getElementById('panel-width');
  const heightInput = document.getElementById('panel-height');
  let isResizingAlt = false;
  const MIN_SIZE = 50;

  function updatePanelSize(width, height) {
    panelAlt.style.width = width + 'px';
    panelAlt.style.height = height + 'px';
    widthInput.value = width;
    heightInput.value = height;
  }

  // Dragging logic for resize-handle-alt (using Pointer Events for broader compatibility)
  handleAlt.addEventListener('pointerdown', (e) => {
    isResizingAlt = true;
    document.body.style.cursor = 'se-resize';
    handleAlt.setPointerCapture(e.pointerId); // Capture pointer events to ensure move events fire consistently
    e.preventDefault(); // Prevent text selection during drag
  });

  document.addEventListener('pointermove', (e) => {
    if (!isResizingAlt) return;
    const rect = panelAlt.getBoundingClientRect();
    const newWidth = Math.max(MIN_SIZE, e.clientX - rect.left);
    const newHeight = Math.max(MIN_SIZE, e.clientY - rect.top);
    updatePanelSize(newWidth, newHeight);
  });

  document.addEventListener('pointerup', (e) => {
    if (isResizingAlt) {
      isResizingAlt = false;
      document.body.style.cursor = '';
      handleAlt.releasePointerCapture(e.pointerId); // Release pointer capture
    }
  });

  // Input field alternative
  widthInput.addEventListener('change', () => {
    const newWidth = Math.max(MIN_SIZE, parseInt(widthInput.value, 10) || MIN_SIZE);
    updatePanelSize(newWidth, panelAlt.offsetHeight);
  });

  heightInput.addEventListener('change', () => {
    const newHeight = Math.max(MIN_SIZE, parseInt(heightInput.value, 10) || MIN_SIZE);
    updatePanelSize(panelAlt.offsetWidth, newHeight);
  });

  // Keyboard control for resize handle
  handleAlt.addEventListener('keydown', (e) => {
    let currentWidth = panelAlt.offsetWidth;
    let currentHeight = panelAlt.offsetHeight;
    let changed = false;

    switch (e.key) {
      case 'ArrowLeft':
        currentWidth = Math.max(MIN_SIZE, currentWidth - 10);
        changed = true;
        break;
      case 'ArrowRight':
        currentWidth += 10;
        changed = true;
        break;
      case 'ArrowUp':
        currentHeight = Math.max(MIN_SIZE, currentHeight - 10);
        changed = true;
        break;
      case 'ArrowDown':
        currentHeight += 10;
        changed = true;
        break;
    }
    if (changed) {
      updatePanelSize(currentWidth, currentHeight);
      e.preventDefault(); // Prevent scrolling the page when using arrow keys on the handle
    }
  });

  // Initialize input values from current panel size
  updatePanelSize(panelAlt.offsetWidth, panelAlt.offsetHeight);
</script>

Best Practices and Common Pitfalls

Best Practices

  • Prioritize Non-Dragging: When designing new features, consider if a non-dragging interaction can be the primary method, with dragging as an enhancement.
  • Clear and Consistent Alternatives: Ensure alternative methods are easy to find, understand, and use. Use standard UI patterns (e.g., arrow buttons for reordering, context menus).
  • Keyboard Accessibility is Key: Always confirm that any alternative method is fully navigable and operable using only the keyboard. For resizable elements, consider using role="separator" with keyboard event listeners for arrow keys.
  • Provide Feedback: Both dragging and alternative methods should provide clear visual and programmatic feedback (e.g., ARIA live regions for screen readers) about the state and outcome of the action.
  • Document Your Choices: If you determine a dragging interaction is “essential,” clearly document the reasoning based on the WCAG definition.
  • Use Pointer Events: For drag interactions, consider using Pointer Events API (pointerdown, pointermove, pointerup) as they unify mouse, touch, and pen inputs and offer features like setPointerCapture() for better reliability.

Common Pitfalls

  • Forgetting the Alternative: The most common mistake is simply providing dragging functionality without any alternative.
  • Incomplete Alternatives: An alternative method might exist but doesn’t offer the full range or precision of the dragging method (e.g., only moving one item at a time when dragging allows multiple selections).
  • Hidden Alternatives: Burying the alternative functionality deep within menus or making it hard to discover.
  • Assuming “Essential”: Incorrectly classifying an interaction as “essential” dragging when it’s not (e.g., assuming a slider must be dragged, when precise input fields could also work).
  • Lack of Testing: Not testing with actual users who rely on alternative input methods or assistive technologies.

Conclusion

Adhering to WCAG 2.5.7 “Dragging Movements” is crucial for creating truly inclusive web experiences. By offering flexible interaction methods that go beyond single-mode dragging, we empower a broader range of users, including those with motor impairments, to fully participate in and benefit from digital content. This criterion not only improves accessibility for specific groups but often enhances usability for all, making interfaces more robust and adaptable to various user preferences and situations.

Privacy Overview

This website uses cookies so that we can provide you with the best user experience possible. Cookie information is stored in your browser and performs functions such as recognising you when you return to our website and helping our team to understand which sections of the website you find most interesting and useful.