📄
label で input[type="file"] を装飾するな
input[type="file"]
な要素を装飾する方法を検索したときに、次のような間違った label
要素の使い方を紹介する記事ばかりヒットするのが気になったので書きました。label 要素を使っても問題ない場合もありますが、間違った使い方をしている(しようとしている)人に届きやすいようにこのような表題にしています。
<label>
ファイルを選択
<input type="file" />
</label>
label {
/* 装飾 */
}
input {
display: none;
}
何が問題か
上記の方法は input
要素を display: none
で非表示にし、label
要素を使ってヒット領域を拡大させて「ファイルを選択」ボタンを表現するという方法ですが、この方法ではフォーカスが当たりません。また、tabindex
属性に非負の値を設定しフォーカスを当てられるようにしても label
は clickable な要素ではないので Space / Enter キーで click イベントを発火することができません。
どうするべきか
button
要素を使い、クリック時に input
要素の click イベントを発火させる方法が使えます。この方法は、アクセシビリティ向上に注力している[1] Twitter や、React でファイルの入力を扱うためのライブラリである react-dropzone などでも使われており、MDN の記事でも紹介されています。他にも方法はありますが、本質ではないので本記事では取り扱いません。
<button>ファイルを選択</button>
<input type="file" />
document.querySelector("button").addEventListener("click", () => {
document.querySelector("input").click();
});
button {
/* 装飾 */
}
input {
display: none;
}
button
要素を使うのが適切でない場合は、次のようにアクセシビリティに配慮したボタンを作りましょう。
<div role="button" tabindex="0">ファイルを選択</div>
<input type="file" />
const buttonElement = document.querySelector("div");
buttonElement.addEventListener("click", () => {
document.querySelector("input").click();
});
// Space / Enter キーで click イベントを発火できるようにする
buttonElement.addEventListener("keydown", (event) => {
if (!buttonElement.isEqualNode(event.target)) {
return;
}
if (event.keyCode === 32 || event.keyCode === 13) {
event.preventDefault();
document.querySelector("input").click();
}
});
div {
/* 装飾 */
}
input {
display: none;
}
Discussion
コードの例だとinput[type="file"]で使用できるドラッグ&ドロップによるファイル選択も使えなくなりますね。
やはり、「label で input[type="file"] を装飾するな」ですかねー
input要素を
display:none
にしちゃうと余計なtabindex設定が必要になるので::file-selector-button
疑似要素を使ったほうがよいと思います。::file-selector-button
使ってスタイルすればJSの実装ゼロにできますよ。Tailwindも
::file-selector-button
に対応してます。input::file-selector-buttonにdisplay:noneを設定するとファイル名を表示するのはそのままでボタンだけ消せますね。