# カードを移動しても何も読み上げないタスクボード

Source: https://easeweb.dev/learn/move-announcements
Topics: ドラッグ&ドロップ > 別のリストや列へ移したカードの、新しい位置が読み上げられる (WCAG 4.1.3)
The fix: ボードの外に、ステータス領域を最初から1つ置き、移動のたびに、どのカードがどの列へ移ったかを伝える文を1つ書き込みます。その文は、ボードの下に1行として見せても、視覚的に隠してボードの見た目をそのままにしてもかまいません。ボード自体をライブリージョンにしないでください。
Test with: screen reader, keyboard, mouse
References: https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F103 https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA22 https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/status_role https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-live

よくある問題を再現した学習用のサンプルです。実在のクライアントの診断結果ではありません。コードは出発点となる実装例のため、実際の製品でも確認してください。ここでは支援技術でのテスト結果は報告していません。

## 場面

チームのタスクボードに「未着手」「作業中」「完了」の3列があり、各見出しの横にカードの枚数が出ています。ボードはドラッグしなくても使えるように作られていて、各カードには移動先の列ごとにボタンがあります。ドラッグで動かしたい人向けに、ドラッグもそのまま使えます。「リリースノートを書く」の「完了へ移動」を押すと、カードは「完了」へ移り、「未着手」の件数は2から1に、「完了」は0から1になり、フォーカスは移動先のカードの最初のボタンに移ります。画面を見ている人には、カードが移ったことがすぐ分かります。スクリーンリーダーの利用者に聞こえるのは「未着手へ移動、ボタン、リリースノートを書く」で、フォーカスが移ったボタンの名前です。カードが今「完了」にあることも、移動できたことも伝わりません。手がかりは、カードのボタンが別の列を示すようになったことだけで、そこから自分で推測するしかありません。確かめるには、カードを離れ、「完了」の見出しを探して列を読み、元の場所へ戻ります。ボードは作業の状態を変える場所なので、これが1日に何度も起きます。

## 確認できる問題

「『リリースノートを書く』を完了へ移動しました」は、ステータスメッセージです。利用者が行った操作の結果を伝え、フォーカスを受け取りません。画面では、その結果が2か所に出ています。新しい列にあるカードと、見出しの件数です。達成基準4.1.3では、こうしたメッセージを、ロールやプロパティによって支援技術に伝えられるようにし、フォーカスを移さなくても読み上げられることを求めています。ここでは件数がライブリージョンになく、カードの移動先を伝えるものもほかにないため、失敗例F103にあたります。フォーカスをカードに移しても、その代わりにはなりません。フォーカスが移るボタンの名前は「未着手へ移動」のままで、カードの題名はその説明として読まれるため、スクリーンリーダーが伝えるのは、起きたことではなく、ボタンを押せば次に何が起きるかです。ブラウザーから伝わる情報も、ほかにはありません。DOMの中で要素を移動しても、支援技術がすでに監視している領域にページが言葉を書き込まない限り、何も読み上げられません。

## 利用者への影響

ボードは、全体を一目で見られる人には使えます。カードの新しい位置が、そのまま確認になるからです。カードが多いボードや、狭い画面で1列に並んだボードでは、一目で見渡せない人にとって、その位置を探すのが難しくなります。

## 自動チェックとAIの限界

自動チェックが見るのは、名前のあるボタン、見出しのある列、カードのリストで、どれも正しいマークアップです。「完了へ移動」を押さないので、カードが動くところを見ることはなく、件数が結果を伝えていることもマークアップからは分かりません。EaseWebの診断は、ドラッグできる項目のリストで、どの項目もドラッグせずに移動できるのに、ページにライブリージョンがない場合や、リストがライブリージョンの中にある場合に、手作業で確かめる項目として報告します。移動を読み上げるようにとAIに頼むと、ボード全体や各列に `aria-live="polite"` を付けることがよくあります。するとスクリーンリーダーが読むのは、加わったテキストです。移動したカードの題名とボタン、それに変わった2つの件数の数字だけで、どの列かを示す言葉はありません。`aria-atomic="true"` も付けると、移動のたびに、変わった領域を丸ごと読みます。ボード全体か2つの列の、すべてのカードです。何が読まれるかは、スクリーンリーダーを動かしてカードを移動しないと分かりません。

## 修正前：黙って移動するカード

```html
<section class="column" data-column="todo">
  <h2>未着手 <span class="count">2</span></h2>
  <ul>
    <li data-card="notes" draggable="true">
      <span id="card-notes">リリースノートを書く</span>
      <button type="button" data-to="doing" aria-describedby="card-notes">作業中へ移動</button>
      <button type="button" data-to="done" aria-describedby="card-notes">完了へ移動</button>
    </li>
    …
  </ul>
</section>
<!-- 作業中と完了も同じ：件数付きの見出しとカードのリスト -->
```

```js
// 移動ボタンからも、ドラッグのドロップからも、移動先の列のリストを渡して呼ばれる。
function moveCard(card, list) {
  const from = card.closest('ul');
  list.append(card);
  updateCount(from);
  updateCount(list);
  updateButtons(card); // 今いない列のボタンだけを出す
  card.querySelector('button').focus();
}
```

カードは移動し、件数は変わり、フォーカスもカードについていきますが、どれもライブリージョンにないため、スクリーンリーダーが読めるのはフォーカスのあるボタンだけです。

## 修正後：ボードの下に移動を伝える行

```html
<section class="column" data-column="todo">
  <h2>未着手 <span class="count">2</span></h2>
  <ul>
    <li data-card="notes" draggable="true">
      <span id="card-notes">リリースノートを書く</span>
      <button type="button" data-to="doing" aria-describedby="card-notes">作業中へ移動</button>
      <button type="button" data-to="done" aria-describedby="card-notes">完了へ移動</button>
    </li>
    …
  </ul>
</section>
<!-- 作業中と完了も同じ：件数付きの見出しとカードのリスト -->
<p id="board-status" role="status" class="board-status"></p>
```

```js
// 移動ボタンからも、ドラッグのドロップからも、移動先の列のリストを渡して呼ばれる。
function moveCard(card, list) {
  const from = card.closest('ul');
  list.append(card);
  updateCount(from);
  updateCount(list);
  updateButtons(card); // 今いない列のボタンだけを出す
  card.querySelector('button').focus();
  boardStatus.textContent = `「${cardTitle(card)}」を${columnName(list)}へ移動しました。`;
}
```

`role="status"` の `p` は最初からHTMLにあり、空で、列の外にあります。そのため、最初の移動で書き込む時点で、ブラウザーはすでにこの要素を監視しています。「完了へ移動」を押すと、スクリーンリーダーは新しくフォーカスしたボタンを読み、続けて「『リリースノートを書く』を完了へ移動しました。」と読みます。文には画面と同じ言葉でカード名と列名が入り、ボードのほかの部分は読まれません。ドラッグのドロップも同じ関数を呼ぶので、マウスで動かしても同じ言葉で読み上げられます。この行は、見える人にも直前の移動を示し、長いボードでカードが画面の外へ移ったときの助けにもなります。

## 修正後：ボードの見た目を変えない、隠したステータス領域

```html
<section class="column" data-column="todo">
  <h2>未着手 <span class="count">2</span></h2>
  <ul>
    <li data-card="notes" draggable="true">
      <span id="card-notes">リリースノートを書く</span>
      <button type="button" data-to="doing" aria-describedby="card-notes">作業中へ移動</button>
      <button type="button" data-to="done" aria-describedby="card-notes">完了へ移動</button>
    </li>
    …
  </ul>
</section>
<!-- 作業中と完了も同じ：件数付きの見出しとカードのリスト -->
<!-- ページのレイアウトに1回だけ。ボードが描き直すものの外に置く -->
<div id="board-status" role="status" class="visually-hidden"></div>
```

```js
// 移動ボタンからも、ドラッグのドロップからも、移動先の列のリストを渡して呼ばれる。
function moveCard(card, list) {
  const from = card.closest('ul');
  list.append(card);
  updateCount(from);
  updateCount(list);
  updateButtons(card); // 今いない列のボタンだけを出す
  card.querySelector('button').focus();
  boardStatus.textContent = `「${cardTitle(card)}」を${columnName(list)}へ移動しました。`;
}
```

ボードの見た目はそのままで、スクリーンリーダーは行の場合と同じ文を読みます。領域はレイアウトにあるため、移動の後でフレームワークが列やボード全体を描き直しても残り、書き込めば、その変化をブラウザーがスクリーンリーダーに伝えます。`display: none` や `hidden` にはしないでください。視覚的に隠すクラスなら、アクセシビリティツリーに残ります。

## どの方法を選ぶか

どちらも、フォーカスを移さずにすべての移動を読み上げ、コードも同じです。ボードの下の行は、見える人にも直前に起きたことを伝え、後から加える「元に戻す」やエラーを出す場所にもなります。デザインに場所があるなら、こちらを選んでください。隠した領域ならボードの見た目は変わらないので、変更できないデザインやテーマにも使えます。各メッセージに項目名と移動先が入っていれば、1つの領域でサイト中のボードとリストをまかなえます。

## 実装の判断

**どの移動方法でも、同じメッセージ。** 文は、カードを動かす1つの関数で書き込み、移動ボタン、「移動先」のリスト、キーボードドラッグ、マウスのドロップのどこからもその関数を呼びます。ボタンでの移動は読まれるのにドロップは黙っているボードでは、ドラッグするスクリーンリーダーの利用者や、読み上げ付きの画面拡大ソフトを使う人が取り残されます。

**何を伝えるか。** カード名と移動先の列名を、画面と同じ言葉で伝えます。たとえば「『リリースノートを書く』を完了へ移動しました。」です。位置は、移動で位置を選んだときや、列が決まった順に並んでいるときだけ加えます（「完了の3件中2番目に移動しました」）。ボードの新しい状態、件数、カードの詳細は読み上げません。知りたい人は列を読めます。サーバーが移動を受け付けないときや、列の枚数の上限で移動できないときも、言葉で伝えます。「『リリースノートを書く』を移動できませんでした。未着手のままです。」

**フォーカスが先、メッセージは後。** フォーカスをカードに移してから、文を書き込みます。politeのメッセージは、新しくフォーカスしたボタンをスクリーンリーダーが読み終えるまで待つことになっています。フォーカスを移す前に書き込むと、新しいフォーカスの読み上げに途中で切られることがあります。移動後にフォーカスを置く場所は、「[マウスでしか動かせないドラッグ＆ドロップのボード](/ja/learn/drag-and-drop)」で説明しています。

**ドラッグの途中は伝えない。** マウスでカードを動かしている間は何も伝えず、置いた先を伝えます。キーボードドラッグは別です。スクリーンリーダーの利用者にはカードの位置が見えないため、「持ち上げました」「作業中の列の上です」「完了に置きました」「未着手に戻しました」と、段階ごとにその場で伝えます。ドラッグ＆ドロップのガイドのキーボードドラッグも、この形です。

**修正にならないもの：ボードをライブリージョンにする。** ボード、列、リストに `aria-live` や `role="status"` を付けると、スクリーンリーダーは変化の意味ではなく、変わった内容を読みます。`aria-atomic` がなければ、加わったカードの題名とボタン、それに件数の数字だけです。あれば、移動のたびに領域全体を読みます。ボード全体か2つの列にある、すべてのカードです。メッセージは読み上げに埋もれるか、そもそも含まれません。

**領域は先に置く。** 領域はページに1回だけ、移動で描き直される列のコンポーネント、キー付きのリスト、`{#if}` の外に置きます。メッセージと一緒に現れた領域は、たいてい読み上げられません。この失敗は「[画面に出ても読み上げられないステータスメッセージ](/ja/learn/status-messages)」で詳しく扱っています。

**ドラッグのライブラリー。** ドラッグ＆ドロップのライブラリーには、独自のライブリージョンを持ち、標準でドラッグを読み上げるものがあります。使われる言葉を確認してください。たとえばdnd-kitの標準のメッセージは、項目とドロップ先をIDで呼ぶため、カードの題名と列の名前を使うメッセージを渡します。1回の移動に使うのは、ライブラリーの領域か自分の領域のどちらか一方にしてください。両方を使うと、移動のたびに2回読まれます。

**緊急度。** `role="status"` はスクリーンリーダーが読み終えるのを待つため、移動に向いています。`role="alert"` や `aria-live="assertive"` は使わないでください。新しいフォーカスの読み上げを遮ることがあります。

## 修正の確認

1. 何も移動する前にDOMを調べる。`role="status"`（または `aria-live="polite"`）の領域が1つあって空で、`display: none` や `aria-hidden` で隠されておらず、ボードの列やリストの外にあることを確認する。ボード、列、リストには、それ自体のライブリージョンがないことも確認する。
2. スクリーンリーダーを動かしたまま、ボタンでカードを移動する。新しくフォーカスした操作要素が読まれ、続けてカードの名前と移動先の列が1回読まれることを確認する。
3. マウスでカードをドラッグして移動し、「移動先」のリストやキーボードドラッグなど、ボードにあるほかの方法でも移動する。どの移動でも、移動し終えたときに同じ形の文が読まれ、動かしている途中には何も読まれないことを確認する。
4. 1枚のカードを2つの列の間で何度か行き来させる。移動のたびに読み上げられ、ボードのほかの部分は読まれないことを確認する。

OS、ブラウザーと支援技術のバージョン、ビルド、実施日、各手順の実際の結果を記録してください。ボードのコンポーネントやドラッグのライブラリーを変更した際は、再度確認します。

## 制限

ライブリージョンの動きはスクリーンリーダーやブラウザーによって異なり、とくに差が大きいのは、フォーカスの移動と重なったときのタイミングです。このパターンは、1つのボードで列の間をカードが移動する場合を扱います。ドラッグせずにカードを動かせるようにすることは「[マウスでしか動かせないドラッグ＆ドロップのボード](/ja/learn/drag-and-drop)」で、1つのリストの中で並べ替え、新しい位置を伝えることは「[ドラッグでしか動かせないか、動かしても何も伝えない並べ替えリスト](/ja/learn/sortable-lists)」で扱っています。ステータスメッセージ全般は「[画面に出ても読み上げられないステータスメッセージ](/ja/learn/status-messages)」で扱っています。ドラッグのライブラリー、列の枚数の上限、狭い画面のレイアウトを含め、実際のボードを、利用者が使うスクリーンリーダーで確認してください。

---

このガイドをコーディングエージェントで使う: MCP https://easeweb.dev/mcp（設定は https://easeweb.dev/ja/services/developers）
