Drag and Drop HTML
Ziehen können, nicht müssen
Karte, Griff, Zielspalte: jeder Zug braucht eine Alternative per Schaltfläche.
Ein Zug, zwei Wege
Die Karte darf springen
Ziehen bleibt möglich. Klick, Touch und Tastatur erreichen dasselbe Ergebnis.
Zu erledigen
2 Karten- Vertrag gegenlesen
- Ausgewählte KarteAngebot prüfenNach obenNach untenNach Erledigt
- Greifen
- Gedrückt halten
- Bewegen
- Loslassen
- Schaltfläche erreichen
- „Nach Erledigt“ aktivieren
KlickTouchEnterLeertaste
Erledigt
1 Karte- Angebot prüfenPlatz 1 von 1
role="status"WCAG 2.1.1 verlangt Tastaturbedienung. WCAG 2.5.7 verlangt zusätzlich eine klick- oder tippbare Alternative ohne Ziehen. Dieselben Schaltflächen können beides leisten.
Was nach jedem Zug synchron bleiben muss
Die Karte wird in die echte Liste verschoben, nicht nur optisch versetzt.
Nach einer Schaltflächenaktion bleibt der Fokus auf der mitbewegten Aktion.
Kartenname, Position, Anzahl und Zielspalte stehen in der Live-Region.
Vollständiges Grundmuster
HTML: Karte, Alternativen und Live-Region
Der Griff ist visuell. Die benannten Schaltflächen tragen die Alternative.
<div class="brett">
<section aria-labelledby="offen-title">
<h2 id="offen-title">Zu erledigen</h2>
<ul class="spalte" data-name="Zu erledigen">
<li class="karte" draggable="true">
<span class="griff" aria-hidden="true">⠿</span>
<span class="titel">Vertrag gegenlesen</span>
<a href="/aufgaben/1">Details</a>
<div class="aktionen">
<button type="button" data-move="up"
aria-label="Vertrag gegenlesen nach oben">Nach oben</button>
<button type="button" data-move="down"
aria-label="Vertrag gegenlesen nach unten">Nach unten</button>
<button type="button" data-move="lane"
aria-label="Vertrag gegenlesen nach Erledigt">Nach Erledigt</button>
</div>
</li>
<li class="karte" draggable="true">
<span class="griff" aria-hidden="true">⠿</span>
<span class="titel">Angebot prüfen</span>
<a href="/aufgaben/2">Details</a>
<div class="aktionen">
<button type="button" data-move="up"
aria-label="Angebot prüfen nach oben">Nach oben</button>
<button type="button" data-move="down"
aria-label="Angebot prüfen nach unten">Nach unten</button>
<button type="button" data-move="lane"
aria-label="Angebot prüfen nach Erledigt">Nach Erledigt</button>
</div>
</li>
</ul>
</section>
<section aria-labelledby="fertig-title">
<h2 id="fertig-title">Erledigt</h2>
<ul class="spalte" data-name="Erledigt"></ul>
</section>
</div>
<p id="move-status" role="status" aria-atomic="true"></p>JavaScript: beide Wege ändern dieselben Listen
const board = document.querySelector('.brett');
const lanes = [...board.querySelectorAll('.spalte')];
const status = document.querySelector('#move-status');
let dragged = null;
function cards(lane) {
return [...lane.querySelectorAll(':scope > .karte')];
}
function announce(card) {
const lane = card.parentElement;
const rows = cards(lane);
const title = card.querySelector('.titel').textContent;
status.textContent = title + ': Platz ' + (rows.indexOf(card) + 1) +
' von ' + rows.length + ' in ' + lane.dataset.name + '.';
}
function syncControls() {
lanes.forEach((lane, laneIndex) => {
const rows = cards(lane);
rows.forEach((card, index) => {
const title = card.querySelector('.titel').textContent;
const up = card.querySelector('[data-move="up"]');
const down = card.querySelector('[data-move="down"]');
up.disabled = index === 0;
down.disabled = index === rows.length - 1;
up.setAttribute('aria-label', title + ' nach oben');
down.setAttribute('aria-label', title + ' nach unten');
const laneButton = card.querySelector('[data-move="lane"]');
laneButton.textContent = laneIndex === 0
? 'Nach Erledigt' : 'Nach Zu erledigen';
laneButton.setAttribute('aria-label', title + ' ' + laneButton.textContent);
});
});
}
board.addEventListener('click', (event) => {
const button = event.target.closest('button[data-move]');
if (!button) return;
const card = button.closest('.karte');
const lane = card.parentElement;
if (button.dataset.move === 'up') {
lane.insertBefore(card, card.previousElementSibling);
} else if (button.dataset.move === 'down') {
lane.insertBefore(card.nextElementSibling, card);
} else {
const target = lanes.find((item) => item !== lane);
target.append(card);
}
syncControls();
button.focus();
announce(card);
});
board.addEventListener('dragstart', (event) => {
dragged = event.target.closest('.karte');
if (!dragged) return;
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', '');
});
board.addEventListener('dragover', (event) => {
if (!dragged || !event.target.closest('.spalte')) return;
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
});
board.addEventListener('drop', (event) => {
const lane = event.target.closest('.spalte');
if (!dragged || !lane) return;
event.preventDefault();
const before = event.target.closest('.karte');
before && before !== dragged
? lane.insertBefore(dragged, before)
: lane.append(dragged);
syncControls();
announce(dragged);
dragged = null;
});
board.addEventListener('dragend', () => {
dragged = null;
});
syncControls();So prüfen Sie das Ergebnis
Nur TastaturVerschieben Sie die zweite Karte nach oben und in die andere Spalte. Der Fokus bleibt erhalten.
Nur Klick oder TouchErreichen Sie dieselben Positionen ohne gedrückte Zeigerbewegung.
VorleseprogrammNach jedem Zug sind Kartenname, Platz, Anzahl und Spalte als Status verfügbar.
ZeigerZiehen Sie eine Karte vor eine andere. Danach stimmt die DOM-Reihenfolge mit der Ansicht überein.
Aus dem Prüfbericht
Drei Regeln an einer Karte
Kritisch Der Griff an der Karte Regel button-name
Das Punktesymbol nennt keine Aktion. aria-label="Karte verschieben" macht aus dem Griff eine benannte Schaltfläche.
Kritisch Der Griff ist zu klein zum Treffen Regel target-size
18 mal 18 Pixel ist leicht zu verfehlen und kann ohne ausreichenden Abstand gegen WCAG 2.5.8 verstoßen. Das Beispiel nutzt 44 mal 44 Pixel.
Kritisch Ein Link in der ziehbaren Karte Regel nested-interactive
Sitzt ein Link im Griff, bleibt unklar, was der Tastendruck auslöst. Stellen Sie Link und Schaltfläche nebeneinander.
Zum Mitnehmen
Die ziehbare Karte, korrigiert
Ein Name, ein Mindestmaß, eine Alternative zum Ziehen.
Korrektur in 3 Schritten ansehen
Dem Griff Namen geben und Link trennen
Der Griff bekommt einen Namen, der Link wandert daneben. So entstehen zwei getrennte Bedienelemente mit zwei klaren Tabstopps.
KorrekturvergleichSchritt 1 · HTML Vorher
<button class="griff" draggable="true"> <span aria-hidden="true">⠿</span> <a href="/karte/12">Details</a> </button>Nachher
<button class="griff" draggable="true" aria-label="Karte verschieben"> <span aria-hidden="true">⠿</span> </button> <a href="/karte/12">Details</a>Markierte Zeilen wurden geändert
So prüfen Sie es Tabben Sie über die Karte. Sie halten zweimal: am Griff und am Link.
Den Griff auf 44 Pixel bringen
Die aktive Fläche zählt, nicht das Symbol. WCAG 2.5.8 nennt im Regelfall 24 mal 24 CSS-Pixel oder ausreichenden Abstand. Das Beispiel nutzt 44 mal 44 Pixel.
KorrekturvergleichSchritt 2 · CSS Vorher
.griff { width: 18px; height: 18px; }Nachher
.griff { display: inline-grid; place-items: center; min-width: 44px; min-height: 44px; }Markierte Zeilen wurden geändert
So prüfen Sie es Messen Sie den Griff in den Entwicklerwerkzeugen. Die Fläche ist 44 mal 44 Pixel.
Verschieben ohne Ziehen anbieten
Ziehen ist eine Bewegung, die nicht jede Hand ausführt. Zwei Schaltflächen erreichen dasselbe Ziel mit einem Klick oder der Leertaste.
KorrekturvergleichSchritt 3 · HTML Vorher
<li class="karte" draggable="true"> <span>Angebot prüfen</span> </li>Nachher
<li class="karte" draggable="true"> <span>Angebot prüfen</span> <button type="button" data-move="up">Nach oben</button> <button type="button" data-move="down">Nach unten</button> </li>Markierte Zeilen wurden geändert
So prüfen Sie es Verschieben Sie eine Karte nur mit der Tastatur. Sie landet in derselben Reihenfolge.
