Accessibility pattern · Data display

Kanban board

Dragging is one way to move a card, never the only one: every card has a Move menu that a single click or tap can use, and a handle that the keyboard can pick up, carry with the arrow keys and drop. Each move is announced with the card, its column and its place in it, focus stays on the card wherever it goes, and the column counts follow.

WCAG criteria
7
Keyboard rules
7
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Release 2.4

Drag a card, or use its Move menu. With the keyboard, press Space on a card's handle, move it with the arrow keys and press Space again to drop it; Escape puts it back.

To do

  • Update Hindi strings

    Content Due 9 Oct
  • Audit color contrast

    Accessibility Due 12 Oct

In progress

  • Fix checkout focus order

    Bug Due 7 Oct
  • Design review

    Design Due 8 Oct
  • Write release notes

    Writing Due 10 Oct

Done

  • Set up error tracking

    Engineering Done 2 Oct
  • Ship dark theme

    Design Done 5 Oct

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
TabMoves through each card's handle and Move button, column by column.
Space or EnterOn a handle, picks the card up; pressed again, drops it where it is.
Arrow Up or Arrow DownWhile a card is picked up, moves it up or down its column.
Arrow Left or Arrow RightWhile a card is picked up, moves it to the previous or next column, keeping its place where it can.
EscapeWhile a card is picked up or dragged, puts it back where it started. In the Move menu, closes the menu.
Enter or Space or Arrow DownOn Move, opens the menu; Arrow Up and Arrow Down move through it, Home and End jump to its ends.
Enter or SpaceIn the menu, moves the card to that column, or up or down, and returns focus to the card's Move button.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Focus reaches a handlePick up Design review, toggle button, not pressed. Drag a card, or use its Move menu…
Space picks it upPressed. Picked up Design review. In progress, position 2 of 3.
Arrow Right carries itDesign review. Done, position 2 of 3.
Space drops itNot pressed. Dropped Design review. Done, position 2 of 3.
Escape insteadCancelled. Design review is back in In progress, position 2 of 3.
Done is chosen in the Move menuMoved Design review to Done, position 3 of 3. Move Design review, menu button

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-kanban" data-ap-kanban>
  <div class="ap-kanban__top">
    <h3 class="ap-kanban__board-name">Release 2.4</h3>
    <p class="ap-kanban__keys" id="kanban-keys">Drag a card, or use its Move menu. With the keyboard, press Space on a card's handle, move it with the arrow keys and press Space again to drop it; Escape puts it back.</p>
  </div>

  <div class="ap-kanban__board">
    <div class="ap-kanban__col" data-ap-col="todo">
      <div class="ap-kanban__head">
        <h4 class="ap-kanban__name" id="kanban-todo">To do</h4>
        <span class="ap-kanban__tally" aria-hidden="true" data-ap-tally>2</span>
      </div>
      <ul class="ap-kanban__cards" aria-labelledby="kanban-todo" data-ap-cards>
        <li class="ap-kanban__card" data-ap-task="hindi">
          <button type="button" class="ap-kanban__grip" aria-label="Pick up Update Hindi strings" aria-pressed="false" aria-describedby="kanban-keys">
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 6h.01M15 6h.01M9 12h.01M15 12h.01M9 18h.01M15 18h.01"/></svg>
          </button>
          <p class="ap-kanban__what">Update Hindi strings</p>
          <span class="ap-kanban__tag" data-tone="info">Content</span>
          <span class="ap-kanban__due">Due 9 Oct</span>
          <button type="button" class="ap-kanban__move" id="kanban-move-hindi" aria-label="Move Update Hindi strings" aria-haspopup="menu" aria-expanded="false" aria-controls="kanban-menu">
            Move
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg>
          </button>
        </li>
        <li class="ap-kanban__card" data-ap-task="contrast">
          <button type="button" class="ap-kanban__grip" aria-label="Pick up Audit color contrast" aria-pressed="false" aria-describedby="kanban-keys">
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 6h.01M15 6h.01M9 12h.01M15 12h.01M9 18h.01M15 18h.01"/></svg>
          </button>
          <p class="ap-kanban__what">Audit color contrast</p>
          <span class="ap-kanban__tag" data-tone="success">Accessibility</span>
          <span class="ap-kanban__due">Due 12 Oct</span>
          <button type="button" class="ap-kanban__move" id="kanban-move-contrast" aria-label="Move Audit color contrast" aria-haspopup="menu" aria-expanded="false" aria-controls="kanban-menu">
            Move
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg>
          </button>
        </li>
      </ul>
      <p class="ap-kanban__blank" data-ap-blank hidden>No cards here yet.</p>
    </div>
    <div class="ap-kanban__col" data-ap-col="doing">
      <div class="ap-kanban__head">
        <h4 class="ap-kanban__name" id="kanban-doing">In progress</h4>
        <span class="ap-kanban__tally" aria-hidden="true" data-ap-tally>3</span>
      </div>
      <ul class="ap-kanban__cards" aria-labelledby="kanban-doing" data-ap-cards>
        <li class="ap-kanban__card" data-ap-task="focus">
          <button type="button" class="ap-kanban__grip" aria-label="Pick up Fix checkout focus order" aria-pressed="false" aria-describedby="kanban-keys">
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 6h.01M15 6h.01M9 12h.01M15 12h.01M9 18h.01M15 18h.01"/></svg>
          </button>
          <p class="ap-kanban__what">Fix checkout focus order</p>
          <span class="ap-kanban__tag" data-tone="danger">Bug</span>
          <span class="ap-kanban__due">Due 7 Oct</span>
          <button type="button" class="ap-kanban__move" id="kanban-move-focus" aria-label="Move Fix checkout focus order" aria-haspopup="menu" aria-expanded="false" aria-controls="kanban-menu">
            Move
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg>
          </button>
        </li>
        <li class="ap-kanban__card" data-ap-task="review">
          <button type="button" class="ap-kanban__grip" aria-label="Pick up Design review" aria-pressed="false" aria-describedby="kanban-keys">
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 6h.01M15 6h.01M9 12h.01M15 12h.01M9 18h.01M15 18h.01"/></svg>
          </button>
          <p class="ap-kanban__what">Design review</p>
          <span class="ap-kanban__tag" data-tone="accent">Design</span>
          <span class="ap-kanban__due">Due 8 Oct</span>
          <button type="button" class="ap-kanban__move" id="kanban-move-review" aria-label="Move Design review" aria-haspopup="menu" aria-expanded="false" aria-controls="kanban-menu">
            Move
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg>
          </button>
        </li>
        <li class="ap-kanban__card" data-ap-task="notes">
          <button type="button" class="ap-kanban__grip" aria-label="Pick up Write release notes" aria-pressed="false" aria-describedby="kanban-keys">
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 6h.01M15 6h.01M9 12h.01M15 12h.01M9 18h.01M15 18h.01"/></svg>
          </button>
          <p class="ap-kanban__what">Write release notes</p>
          <span class="ap-kanban__tag" data-tone="info">Writing</span>
          <span class="ap-kanban__due">Due 10 Oct</span>
          <button type="button" class="ap-kanban__move" id="kanban-move-notes" aria-label="Move Write release notes" aria-haspopup="menu" aria-expanded="false" aria-controls="kanban-menu">
            Move
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg>
          </button>
        </li>
      </ul>
      <p class="ap-kanban__blank" data-ap-blank hidden>No cards here yet.</p>
    </div>
    <div class="ap-kanban__col" data-ap-col="done">
      <div class="ap-kanban__head">
        <h4 class="ap-kanban__name" id="kanban-done">Done</h4>
        <span class="ap-kanban__tally" aria-hidden="true" data-ap-tally>2</span>
      </div>
      <ul class="ap-kanban__cards" aria-labelledby="kanban-done" data-ap-cards>
        <li class="ap-kanban__card" data-ap-task="errors">
          <button type="button" class="ap-kanban__grip" aria-label="Pick up Set up error tracking" aria-pressed="false" aria-describedby="kanban-keys">
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 6h.01M15 6h.01M9 12h.01M15 12h.01M9 18h.01M15 18h.01"/></svg>
          </button>
          <p class="ap-kanban__what">Set up error tracking</p>
          <span class="ap-kanban__tag" data-tone="warning">Engineering</span>
          <span class="ap-kanban__due">Done 2 Oct</span>
          <button type="button" class="ap-kanban__move" id="kanban-move-errors" aria-label="Move Set up error tracking" aria-haspopup="menu" aria-expanded="false" aria-controls="kanban-menu">
            Move
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg>
          </button>
        </li>
        <li class="ap-kanban__card" data-ap-task="dark">
          <button type="button" class="ap-kanban__grip" aria-label="Pick up Ship dark theme" aria-pressed="false" aria-describedby="kanban-keys">
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 6h.01M15 6h.01M9 12h.01M15 12h.01M9 18h.01M15 18h.01"/></svg>
          </button>
          <p class="ap-kanban__what">Ship dark theme</p>
          <span class="ap-kanban__tag" data-tone="accent">Design</span>
          <span class="ap-kanban__due">Done 5 Oct</span>
          <button type="button" class="ap-kanban__move" id="kanban-move-dark" aria-label="Move Ship dark theme" aria-haspopup="menu" aria-expanded="false" aria-controls="kanban-menu">
            Move
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg>
          </button>
        </li>
      </ul>
      <p class="ap-kanban__blank" data-ap-blank hidden>No cards here yet.</p>
    </div>
  </div>

  <div class="ap-kanban__menu" role="menu" id="kanban-menu" aria-labelledby="kanban-move-hindi" hidden>
    <div role="group" aria-labelledby="kanban-menu-cap">
      <span class="ap-kanban__menu-cap" id="kanban-menu-cap" aria-hidden="true">Move to column</span>
      <button type="button" class="ap-kanban__item" role="menuitemradio" aria-checked="false" tabindex="-1" data-ap-to="todo"><svg class="ap-kanban__check" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12 5 5L20 7"/></svg>To do</button>
      <button type="button" class="ap-kanban__item" role="menuitemradio" aria-checked="false" tabindex="-1" data-ap-to="doing"><svg class="ap-kanban__check" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12 5 5L20 7"/></svg>In progress</button>
      <button type="button" class="ap-kanban__item" role="menuitemradio" aria-checked="false" tabindex="-1" data-ap-to="done"><svg class="ap-kanban__check" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12 5 5L20 7"/></svg>Done</button>
    </div>
    <div class="ap-kanban__sep" role="separator"></div>
    <button type="button" class="ap-kanban__item" role="menuitem" tabindex="-1" data-ap-shift="-1"><svg class="ap-kanban__arrow" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 19V5M6 11l6-6 6 6"/></svg>Move up</button>
    <button type="button" class="ap-kanban__item" role="menuitem" tabindex="-1" data-ap-shift="1"><svg class="ap-kanban__arrow" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 5v14M6 13l6 6 6-6"/></svg>Move down</button>
  </div>

  <p class="ap-kanban__sr" aria-live="polite" data-ap-live></p>
  <span class="ap-kanban__sr" data-ap-msg="lift" hidden>Picked up {card}. {column}, position {pos} of {total}.</span>
  <span class="ap-kanban__sr" data-ap-msg="move" hidden>{card}. {column}, position {pos} of {total}.</span>
  <span class="ap-kanban__sr" data-ap-msg="drop" hidden>Dropped {card}. {column}, position {pos} of {total}.</span>
  <span class="ap-kanban__sr" data-ap-msg="cancel" hidden>Cancelled. {card} is back in {column}, position {pos} of {total}.</span>
  <span class="ap-kanban__sr" data-ap-msg="moved" hidden>Moved {card} to {column}, position {pos} of {total}.</span>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    Each column is a list named by its heading, so screen readers say how many cards it holds, and the count updates as cards move.

  • 2.1.1 Keyboard Level A

    Everything a mouse drag does can be done from the keyboard, with the handle or with the Move menu.

  • 2.4.3 Focus Order Level A

    Focus moves with the card, to its handle or its Move button, so it is never left behind in the old column.

  • 2.4.7 Focus Visible Level AA

    Handles, Move buttons and menu items show a two-pixel ring, and a picked-up card also gets an outline and a lifted shadow.

  • 2.5.7 Dragging Movements Level AA

    The Move menu moves any card to any column, or up and down, with single clicks or taps, so dragging is never required.

  • 4.1.2 Name, Role, Value Level A

    The handle is a toggle button that is pressed while it holds the card, and the menu's columns are radio items with the current one checked.

  • 4.1.3 Status Messages Level AA

    Picking up, moving, dropping and cancelling are announced in a polite live region with the card, column and position.

Usage

When to use it

Use it

  • Work that moves through stages, like tasks, hiring stages or support tickets.
  • A small team's shared view where seeing the whole flow at a glance matters.

Use something else

  • Ordering a single list: a sortable list with up and down actions is simpler.
  • Hundreds of items: give people a table with a status column and filters.
  • Status that changes in one step: a select on each item is quicker than a board.

Common failures

How it usually goes wrong

  • Drag and drop only

    People who use a keyboard, a switch, voice control or one finger with a tremor cannot drag. Each card has a Move menu that works with single clicks, and a handle that works from the keyboard.

  • Silent moves

    A card that jumps columns without a word leaves screen reader users guessing where it went. Each step says the card, the column and the position.

  • Focus left behind

    Moving the DOM node of a focused element can drop focus to the top of the page. Focus is put back on the card's handle or Move button after every move.

  • No way to undo a drag in progress

    Once picked up, a card must be able to go back. Escape returns it to where it started, for the keyboard and for a mouse drag.

  • Columns that hide their counts

    A number drawn in a badge is invisible to assistive technology. Each column is a list, so its size is announced, and the badge is kept in step.

  • Tiny drag handles

    A handle a few pixels wide is hard to hit. The handle is 32 pixels square, a mouse can grab the whole card, and the Move button is a full-size target.

Notes

Building it

  • The handle uses click events with detail 0 for picking up and dropping, which is what Enter, Space and screen reader activation produce; a mouse uses the drag instead.
  • A mouse can drag from anywhere on a card; touch and pen drag from the handle only, so swiping across the board still scrolls the page.
  • Announcements are templates in the markup, filled in with the card, column and position, so a translation can put the words in its own order.
  • One Move menu is shared by every card and moved into the card that opens it, so it reads in place and the markup stays short.
  • Cards glide to their new place in 180 milliseconds, and jump straight there when the system asks for reduced motion.

Sources: WCAG 2.2: Understanding Dragging Movements · WAI-ARIA Authoring Practices: Menu Button

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction