# ファイルを選ぶボタンがないドラッグ＆ドロップのアップロード欄

Source: https://easeweb.dev/learn/file-drop-zones
Topics: ドラッグ＆ドロップ > ファイルのドロップ領域に、ファイルを選ぶボタンもある (WCAG 2.1.1, 2.5.7, 4.1.2)
The fix: ドロップは残したまま、ファイル選択を開くコントロールをドロップゾーンに加えます。隠したfile inputのclick()を呼ぶ「ファイルを選択」ボタン、::file-selector-buttonで整えたfile inputそのもの、見た目だけを隠したfile inputに重ねてボタン風に整えたlabelのいずれかです。labelを使う場合は、フォーカスの枠をlabelに描きます。
Test with: keyboard, screen reader, mouse, touch, speech input
References: https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F108 https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/file https://developer.mozilla.org/en-US/docs/Web/API/File_API/Using_files_from_web_applications

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

## 場面

経費申請のフォームで、金額の次に領収書を添付します。領収書欄は点線の枠で、アップロードの矢印と「ここに領収書をドラッグ＆ドロップ」という文言があります。マウスでフォルダーからPDFをドラッグし、枠の上で離すと、枠の下にファイル名が出ます。「金額」の欄からTabキーを押すと、フォーカスは「申請する」へ直接移ります。枠の中にはフォーカスできるものも、押せるボタンもありません。スクリーンリーダーは「ここに領収書をドラッグ＆ドロップ」を1行の文として読みます。スマートフォンでは領収書は別のアプリにあるため、ドラッグするには、ファイルを持ったままブラウザーへ切り替える必要があり、そもそもできない端末もあります。

同じ枠に「またはクリックしてファイルを選択」と書かれ、枠のどこをクリックしてもファイル選択が開くこともよくあります。マウスの利用者は問題に気付きません。それでもTabキーは枠を飛ばし、Enterキーを押しても何も起きず、スクリーンリーダーは文言を文として読むだけで、押せるものだと伝えるロールはありません。

## 確認できる問題

枠は、スクリプトでイベントを受け取る `div` です。1つ目の形では、受け取るのはドロップだけです。ファイルを追加するには、ファイルを押し、運び、離すというドラッグの動作が必要です。WCAG 2.5.7は、ドラッグでできる操作を、1回のクリックやタップだけでもできるようにすることを求めています。その方法がない機能は失敗例F108に当たり、この枠にはその方法がありません。キーボードではフォルダーからページへファイルを運べず、枠の中にフォーカスできるものもないため、キーボードからは領収書を一切追加できず、2.1.1も満たしません。

2つ目の形では、枠の中に `hidden` 属性で隠したfile inputがあり、枠がクリックされるとスクリプトが `click()` で開きます。1回のクリックでファイルを追加できるので2.5.7は満たしますが、クリックを受けるのは相変わらず `div` です。Tabの順序に入らず、EnterキーもSpaceキーも効かないため、2.1.1を満たしません。クリックで動くのでコントロールですが、ロールも名前もありません。スクリーンリーダーは文言を文として読み、押せるものがあるとは伝えないため、4.1.2も満たしません。実際に働くfile inputは誰からも隠されているので、代わりになりません。

## 利用者への影響

経費の申請、応募、問い合わせでは、アップロードしないと送信できないことがよくあります。ドロップしか受け付けない枠が1つあるだけで、フォーム全体が止まります。ドラッグはできても時間がかかる人も、同じ問題で手間取ります。フォルダーとフォームを画面に並べ、枠の中に正確に落とさなければならないからです。

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

自動チェックに見えるのは、`div` と段落、2つ目の形ではそれに加えて誰からも隠されたfile inputだけです。隠されたinputにラベルを求めるルールはありません。クリックとドロップのイベントはマークアップではなくスクリプトにあるため、枠がコントロールだと示すものもありません。EaseWebの診断は、自身でドロップを受け取る枠、`display: none`、`visibility: hidden`、`hidden` 属性のどれかで隠したfile inputを囲む枠、ファイルのドラッグやドロップを求める文言のある点線や破線の枠を探し、中にTabで届くものがなく、横にもfile inputやファイルを選ぶ・アップロードするボタンがなければ、確認の手がかりとして報告します。フレームワークがルートにまとめて付けたイベントは見えず、ページの別の場所にあるボタンが同じinputを開くこともあるため、1つずつ人が確認します。AIにドロップゾーンのアクセシビリティ改善を頼むと、一部のアップロード用ライブラリーと同じように、枠に `tabindex="0"` を付けることがよくあります。枠はフォーカスを受けるようになりますが、ロールも名前もないためスクリーンリーダーはほとんど何も読まず、キー操作の処理も書かない限りEnterキーでは何も起きません。診断は、このような枠も確認の手がかりとして報告します。

## 修正前：ドロップしか受け付けない枠

```html
<div class="field">
  <span class="field-label" id="receipts-label">領収書</span>
  <div class="drop-zone">
    <p>ここに領収書をドラッグ＆ドロップ</p>
  </div>
  <ul class="file-list"></ul>
</div>
```

```css
.drop-zone {
  border: 2px dashed #8a94a6;
  border-radius: 12px;
  padding: 24px;
  text-align: center;
}
```

```js
const zone = document.querySelector('.drop-zone');
function dropFiles(event) {
  event.preventDefault();
  addFiles(event.dataTransfer.files); // 各ファイル名を.file-listに並べる
}
zone.addEventListener('dragover', (event) => event.preventDefault());
zone.addEventListener('drop', dropFiles);
```

- ファイルを入れる方法はドロップだけです。
- 枠の中に、フォーカスできるものがありません。
- クリックでもキーでも、ファイルを追加できません。

## 修正前：枠をクリックすると隠したfile inputが開く

```html
<div class="field">
  <span class="field-label" id="receipts-label">領収書</span>
  <div class="drop-zone">
    <p>ここに領収書をドラッグ＆ドロップ、またはクリックしてファイルを選択</p>
    <input type="file" id="receipts" multiple hidden>
  </div>
  <ul class="file-list"></ul>
</div>
```

```css
.drop-zone {
  border: 2px dashed #8a94a6;
  border-radius: 12px;
  padding: 24px;
  text-align: center;
  cursor: pointer;
}
```

```js
const zone = document.querySelector('.drop-zone');
function dropFiles(event) {
  event.preventDefault();
  addFiles(event.dataTransfer.files); // 各ファイル名を.file-listに並べる
}
zone.addEventListener('dragover', (event) => event.preventDefault());
zone.addEventListener('drop', dropFiles);
const input = document.getElementById('receipts');
zone.addEventListener('click', () => input.click());
input.addEventListener('change', () => addFiles(input.files));
```

マウスではドロップのほかにクリックも使えますが、クリックを受けるのは `div` です。

- Tabキーは枠を通り過ぎます。
- EnterキーもSpaceキーも効きません。
- ロールも名前もありません。
- 本物のfile inputは、誰からも隠されています。

## 修正後：隠したinputを開く「ファイルを選択」ボタン

```html
<div class="field">
  <span class="field-label" id="receipts-label">領収書</span>
  <div class="drop-zone">
    <p>ここに領収書をドラッグ＆ドロップ、または</p>
    <button type="button" class="choose" aria-describedby="receipts-label">ファイルを選択</button>
    <input type="file" id="receipts" multiple hidden>
  </div>
  <ul class="file-list"></ul>
</div>
```

```css
.drop-zone {
  border: 2px dashed #8a94a6;
  border-radius: 12px;
  padding: 24px;
  text-align: center;
}
.choose {
  border: 0;
  border-radius: 999px;
  background: #1f5fbf;
  color: #fff;
  padding: 8px 18px;
  font: inherit;
}
```

```js
const zone = document.querySelector('.drop-zone');
function dropFiles(event) {
  event.preventDefault();
  addFiles(event.dataTransfer.files); // 各ファイル名を.file-listに並べる
}
zone.addEventListener('dragover', (event) => event.preventDefault());
zone.addEventListener('drop', dropFiles);
const input = document.getElementById('receipts');
zone.querySelector('.choose').addEventListener('click', () => input.click());
input.addEventListener('change', () => addFiles(input.files));
```

1. 枠の中に、HTML標準の「ファイルを選択」ボタンを置きます。ブラウザーがTabの順序に入れ、EnterキーやSpaceキーで押せるようにし、ロールも付けます。
2. 名前はボタン自身の文字にし、欄のラベルを説明にします。「ファイルを選択、ボタン、領収書」のように読まれます。
3. inputを開くクリックを枠からボタンへ移し、枠の `cursor: pointer` を外します。1回のクリック、タップ、キー操作でファイル選択が開き、音声操作では「ファイルを選択をクリック」と言えます。

これで二つの修正前の例はどちらも直り、2.1.1、2.5.7、4.1.2を満たします。inputは `hidden` で隠したままです。現在のブラウザーでは、この状態でも `click()` でファイル選択が開き、必要な操作はすべてボタンでできます。ドロップはこれまでどおり使えます。

## 修正後：ドロップゾーンの中にfile inputそのものを置く

```html
<div class="field">
  <label class="field-label" for="receipts">領収書</label>
  <div class="drop-zone">
    <p>ここに領収書をドラッグ＆ドロップ、または</p>
    <input type="file" id="receipts" multiple>
  </div>
  <ul class="file-list"></ul>
</div>
```

```css
.drop-zone {
  border: 2px dashed #8a94a6;
  border-radius: 12px;
  padding: 24px;
  text-align: center;
}
.drop-zone input::file-selector-button {
  border: 0;
  border-radius: 999px;
  background: #1f5fbf;
  color: #fff;
  padding: 8px 18px;
  font: inherit;
}
```

```js
const zone = document.querySelector('.drop-zone');
function dropFiles(event) {
  event.preventDefault();
  addFiles(event.dataTransfer.files); // 各ファイル名を.file-listに並べる
}
zone.addEventListener('dragover', (event) => event.preventDefault());
zone.addEventListener('drop', dropFiles);
const input = document.getElementById('receipts');
input.addEventListener('change', () => addFiles(input.files));
```

1. file inputを隠さずに置き、欄の名前をその `label` にします。フォーカスを受け、EnterキーやSpaceキーで開き、ラベルの「領収書」を名前とするボタン（ブラウザーによっては、ボタンを含むまとまり）として読まれます。
2. `click()` の呼び出しを外します。スクリプトで開く必要はありません。
3. `::file-selector-button` でボタン部分の見た目を整えます。ただし、その文字や、ブラウザーが横に書く文言は変えられません。英語のChromeなら「Choose Files」と「No file chosen」、Firefoxなら「Browse…」と「No files selected.」で、どちらもページではなくブラウザーの言語で表示されます。

これで二つの修正前の例はどちらも直り、2.1.1、2.5.7、4.1.2を満たします。枠へのドロップではinputにファイルが入らないため、ドロップした後も、ファイルが選ばれていないという表示のままです。スクリプトでドロップしたファイルをinputに移さない限り、枠の下の一覧と食い違います。

## 修正後：見た目だけ隠したinputに重ねたボタン風のlabel

```html
<div class="field">
  <span class="field-label" id="receipts-label">領収書</span>
  <div class="drop-zone">
    <p>ここに領収書をドラッグ＆ドロップ、または</p>
    <input type="file" id="receipts" class="visually-hidden" multiple aria-describedby="receipts-label">
    <label class="choose" for="receipts">ファイルを選択</label>
  </div>
  <ul class="file-list"></ul>
</div>
```

```css
.drop-zone {
  border: 2px dashed #8a94a6;
  border-radius: 12px;
  padding: 24px;
  text-align: center;
}
.choose {
  border-radius: 999px;
  background: #1f5fbf;
  color: #fff;
  padding: 8px 18px;
}
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.drop-zone input:focus-visible + .choose {
  outline: 3px solid #1f5fbf;
  outline-offset: 3px;
}
```

```js
const zone = document.querySelector('.drop-zone');
function dropFiles(event) {
  event.preventDefault();
  addFiles(event.dataTransfer.files); // 各ファイル名を.file-listに並べる
}
zone.addEventListener('dragover', (event) => event.preventDefault());
zone.addEventListener('drop', dropFiles);
const input = document.getElementById('receipts');
input.addEventListener('change', () => addFiles(input.files));
```

1. `visually-hidden` で、inputを見た目だけ隠し、キーボードからは隠さないようにします。Tabキーは止まり、EnterキーかSpaceキーでファイル選択が開きます。
2. inputのすぐ後に、ボタン風の `label` を置きます。labelをクリックすれば、スクリプトなしでファイル選択が開きます。
3. 欄の名前をinputの説明にします。スクリーンリーダーは、labelの「ファイルを選択」を名前とするボタン（ブラウザーによっては、ボタンを含むまとまり）として読み、続けて「領収書」を読みます。
4. input自体は何も描かないため、inputの `:focus-visible` からlabelにフォーカスの枠を描きます。

これで二つの修正前の例はどちらも直り、2.1.1、2.5.7、4.1.2を満たします。ただし、inputは必ずこの方法で隠してください。`display: none`、`visibility: hidden`、`hidden` のどれを使っても、inputはTabの順序から外れ、labelはクリックやタップでしか使えなくなります。フォーカスの枠は、labelがinputのすぐ後にあることを前提にしています。

## どの方法を選ぶか

3つとも基準を満たし、どれもドロップを残します。特に理由がなければボタンを使ってください。どのブラウザーでもページ自身の文言を表示でき、フォーカスの枠もフォーカスを受ける要素そのものに出る唯一の方法で、必要なスクリプトは1行です。file inputそのものはスクリプトも隠す要素も要らないため、簡素なフォームや、フォームを使う前にスクリプトを実行できないページに向いています。ただし、ボタンの文字と、ファイルが選ばれていないという横の表示はブラウザーが自身の言語で書くため、ドロップしたファイルと表示を合わせる必要があります。隠したinputに重ねるlabelもスクリプトは要らず、デザインどおりの見た目にできますが、フォーカスの枠とフォーカスを受ける要素が別々で、CSSのセレクターでつながっているだけです。マークアップを変えると、ほかは何も壊れないまま枠だけが消えることがあります。

## 実装の判断

ドロップは残してください。フォルダーをフォームの横に開いている人には速い方法で、ボタンがあればドロップは唯一の方法ではなく近道になります。枠全体のクリックも、ボタンがある限り、マウス用の大きな的として残してかまいません。ただし、枠のクリックの処理ではボタンから始まったクリックを無視してください。そうしないと、ファイル選択を2回開こうとすることがあります。

ファイルが入った後の動きは、いくつかの小さな判断で決まります。

- 追加したことを伝える：ドロップした後やファイル選択を閉じた後は、枠にファイルを一覧表示し、「2件のファイルを追加しました：taxi-receipt.pdf、hotel-invoice.pdf」のようにステータス領域でも伝える。ファイル選択を閉じるとフォーカスはボタンに戻るため、伝えないとスクリーンリーダーの利用者は自分で探すことになる。詳しくは「[画面に出ても読み上げられないステータスメッセージ](/ja/learn/status-messages)」を参照。
- ファイルの削除：一覧の各ファイルに、ファイル名を含む名前の削除ボタン（「taxi-receipt.pdfを削除」）を付け、削除後は次のファイルか「ファイルを選択」へフォーカスを移す。
- 必須のアップロード：`hidden` で隠したfile inputには、ブラウザーが自身のメッセージを出すためにフォーカスを移せないため、`required` を付けると、何も表示されないままフォームが止まることがある。ファイルの有無は自分で確認し、メッセージをボタンの横に置いて関連付ける。詳しくは「[直し方がわからないフォームエラー](/ja/learn/accessible-form-errors)」を参照。
- スマートフォン：ボタンは同じinputを開くため、カメラ、写真ライブラリー、ファイルなど、端末自身の選択肢がそのまま出る。`accept="image/*,.pdf"` で選べる種類を絞れる。同じ種類を枠の文言にも書き、エラーで初めて知ることがないようにする。
- アップロード用ライブラリー：Dropzone.jsは枠全体をクリックできるようにし、react-dropzoneは枠そのものをTabで止まるようにする。それぞれ何を支援技術に伝えているかを確認し、枠しかない場合は本物のボタンを加える。

## 修正の確認

1. マウスを使わずに、ドロップゾーンの前の欄からTabキーを押し、ドロップゾーンのボタンかfile inputでフォーカスが止まり、フォーカスの枠が見えるか確認する。
2. Enterキーを押してファイル選択が開くか確認し、2つのファイルを選んで両方が一覧に出るか確認する。Spaceキーでも試す。
3. スクリーンリーダーでコントロールまでTabで移動し、名前付きのボタンとして読まれるか、ファイル選択を閉じた後に選んだファイルを見つけられるか確認する。
4. マウスのクリックだけでファイルを追加する。スマートフォンではタップだけで、音声操作ではコントロールの表示テキストを言って、同じように追加する。
5. フォルダーからファイルをドロップゾーンへドラッグし、これまでどおり追加されて一覧に並ぶか確認する。

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

## 制限

このサンプルは、どのアプリケーションでも同じ結果になることを保証するものではありません。カードやリストの項目をドラッグで動かすことは「[マウスでしか動かせないドラッグ＆ドロップのボード](/ja/learn/drag-and-drop)」で、アップロードした写真の内容を尋ねることは「[代替テキストを尋ねない画像のアップロード](/ja/learn/alt-text-prompts)」で扱っています。大きなファイルのアップロードの進み具合を示す表示には別の配慮が必要です。「[スクリーンリーダーで読めないプログレスバーとメーター](/ja/learn/progress-bars)」を参照してください。ファイルの貼り付けやフォルダーごとのアップロードは、ここでは扱いません。ファイル選択の画面はOSのもので、キー操作やスクリーンリーダーの対応はOSによって異なります。対象とする支援技術で、操作全体を確認してください。

## スクリーンリーダーでの動き（EaseWebが実機で確認）

`aria-describedby`: ページ内の別のテキスト（ヒントやエラーなど）を、要素の説明にします。

| スクリーンリーダー | ブラウザーとOS | Tabで移動 | 下矢印キー |
| --- | --- | --- | --- |
| NVDA | Chrome, Windows 11 | 未確認 | 未確認 |
| JAWS | Chrome, Windows 11 | 未確認 | 未確認 |
| VoiceOver | Safari, iOS | 未確認 | 未確認 |
| TalkBack | Chrome, Android | 未確認 | 未確認 |

https://easeweb.dev/ja/learn/screen-reader-support

---

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