ドラッグ&ドロップ アップロード
点線の枠にファイルをドラッグするか、クリック/タップで選択。ファイル名と疑似プログレスバーで進捗を表示する。
プレビュー
操作ドラッグ/スワイプで操作できます(PC・スマホ対応)。
仕様書
- 向く場面
- PC のドラッグとスマホのタップを同じ枠で受ける単一ファイル送信
- ファイル名と進捗バーをその場で見せたい画面
- キーボードで開ける1つのタブストップにしたい入力
- 避ける場面
- 複数ファイルの同時アップロード(先頭1件だけ処理し残りは件数を知らせる)
- 本物の進捗が要る本番(疑似進捗のまま出さない)
- 構成要素
- ラベル
- ドロップゾーン(アイコン・文言)
- 隠しファイル入力
- ファイル名
- 進捗バー
- 状態文
- 持っている状態
- 通常
- フォーカス
- 押下中
- 読み込み中
- 成功
- キーボード
ゾーンに Tab で到達(tabindex=0)、Enter/Space でファイル選択ダイアログを開く
- 支援技術
ゾーンは role=button+tabindex=0+aria-label、本物の input は aria-hidden・tabindex=-1 で二重のタブストップを避ける。バーは role=progressbar+aria-valuenow、状態文は aria-live=polite
- 動き
枠色・背景 200ms(ドラッグ中の受け入れ表示)、バー幅 50ms linear(進捗)。reduced-motion で無効
デザインの要点
- 本物の input[type=file] を透明にしてゾーンに重ねる。クリックはネイティブのダイアログ、ドラッグはゾーンの drop で受ける
- キーボードのタブストップはゾーン1つ。input は tabindex=-1 にして、Tab が2回止まらないようにする
- dragover では必ず preventDefault する。忘れるとブラウザがドロップ自体を拒否し、何も起きない
- 受け付けたファイル名を即座に出す。進捗バーだけだと、どのファイルが送られているか分からない
使いどころ
ドラッグ操作が使えるデスクトップ環境ではドラッグの手軽さを、タッチ端末ではタップでのファイル選択を、同じゾーンで両立させたい単一ファイルのアップロードに向く。この実装はドロップでもタップでも同じ .fu-zone が受け口になっており、ドラッグに対応していない端末でも操作の入口が失われない設計になっている。反面、実際のアップロード進捗はsetIntervalで作った疑似的なランダム増加であり、本物の通信の進み具合とは一切連動していない。実運用でXHRやfetchの進捗イベントに差し替える前提のプレースホルダーとして扱うべきで、このまま本番に出すと「進捗バーが100%になったのに実際のアップロードはまだ終わっていない」ような食い違いが起こり得る。また dataTransfer.files[0] / input.files[0] のみを読んでおり、複数ファイルの同時ドロップには対応していない。
仕組み
見た目を作る .fu-zone(role="button" tabindex="0")と、実際のファイル選択を担う input[type=file] を同じ位置に重ねている。inputは position: absolute; inset: 0; opacity: 0 で完全に透明化された状態でゾーンの上に載っており、マウスでゾーンをクリックするとその透明なinputに直接ヒットしてOS標準のファイル選択ダイアログが開く。一方でinputにはaria-hidden="true"とtabindex="-1"が付けられてTab移動の対象から外されており、キーボードユーザーは代わりに.fu-zone自体にフォーカスが当たり、Enter/Spaceのkeydownハンドラでinput.click()を呼ぶことで同じダイアログを開く。ドラッグ&ドロップはdragover(毎回preventDefault必須。呼ばないとブラウザがドロップ自体を拒否する)、dragleave、dropの3イベントで完結しており、input要素とは独立して動く別経路である。
実装の注意
複数ファイルを一度にドロップしても最初の1つしか処理されない。見本では「他 N 件は送られていません」と状態文に添えて残りが無視されたことを知らせているが、本当に複数を扱うなら files を全件ループする実装に変える必要がある。次に、dragoverでpreventDefault()を呼び忘れる、あるいは一度だけ呼んで以降呼ばなくなるような書き換えをすると、ブラウザはドロップ操作自体を拒否してしまい、外見上はゾーンにファイルを乗せているのに何も起きないという分かりにくい壊れ方になる。疑似進捗のMath.random()による増加は見た目としては自然だが、本物の通信に差し替える際は進捗の駆動源そのものを丸ごと入れ替える必要があり、失敗時の再試行・エラー表示の状態遷移もこのコードには一切用意されていない。ファイル形式やサイズの事前検証も無く、accept属性はファイル選択ダイアログのフィルタとしてしか働かずドラッグ&ドロップには効かないため、ドロップ経路でも別途JS側でチェックする必要がある。
アクセシビリティ
aria-hidden かつ tabindex="-1" にした本物のinputと、role="button" tabindex="0" のゾーンで役割を分けている構成は、キーボードユーザーに1つのタブストップだけを提供しつつマウス/タッチユーザーにはネイティブのファイル選択・ドラッグの挙動をそのまま使わせる、意図的で妥当な設計である。ただしこれはkeydownハンドラ(Enter/Spaceでinput.click())が生きていて初めて成立する設計であり、もしこのハンドラだけ削除されると、キーボードユーザーにとってゾーンは押しても何も起きない要素になり、実質的にファイルを選ぶ手段を失う。状態文の #fu-status は aria-live="polite" なので、「アップロード中…」から「完了!」への変化がスクリーンリーダー利用者にも自動的に伝わる。進捗バーの外枠には role="progressbar" と aria-valuemin/max、そして進捗のたびに更新する aria-valuenow を付けており、支援技術は数値としての進捗を取得できる。バーの中身の div は幅を変えるだけの装飾なので、数値は必ず外枠の属性で渡す。
調整して使う
複数ファイル対応(dataTransfer.files/ input.files を全件ループし、ファイルごとに行と進捗バーを持たせる)が最も需要のある拡張である。疑似進捗を本物のXHR upload.onprogressに差し替える改修、画像アップロード向けにaccept="image/*"とサムネイルプレビューを追加する改修、一度選択したファイルを取り消して選び直すキャンセル操作の追加なども考えられる。エラー時の状態(ネットワーク断・サーバー拒否)を#fu-statusに反映する分岐も、現状は成功パスしか用意されていないため別途必要になる。
コード(コピーして使えます)
<div class="fu-wrap">
<label class="fu-label">ファイルをアップロード</label>
<div class="fu-zone" id="fu-zone" tabindex="0" role="button" aria-label="ファイルを選択またはドロップ">
<div class="fu-icon" aria-hidden="true">
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#2b50c7" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/>
<path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"/>
</svg>
</div>
<p class="fu-text">ここにドロップ</p>
<p class="fu-sub">またはタップして選択</p>
<input id="fu-input" class="fu-hidden-input" type="file" aria-hidden="true" tabindex="-1" />
</div>
<div class="fu-result" id="fu-result" hidden>
<p class="fu-filename" id="fu-filename"></p>
<div class="fu-bar-wrap" id="fu-bar-wrap" role="progressbar" aria-label="アップロードの進捗" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
<div class="fu-bar" id="fu-bar"></div>
</div>
<p class="fu-status" id="fu-status" aria-live="polite">アップロード中…</p>
</div>
</div>