なぜ <div> に onClick がダメなのか?
■ はじめに
<div>
要素にonClick
を渡すべきではない、ということ聞いたことはないでしょうか?
ただ、なぜ渡すべきでないのか?
理解してなかったので今回調べてみました。
サンプルコード
今回動作確認に利用したサンプルリポジトリのコードはReact
で書いています。
<div>
にonClick
を定義するのがなぜダメなのか?
■ 結論:ユーザーにとって操作性の低いボタンになってしまうから、です!
要するに UX が悪くなってしまうから!
その理由を解説していきます!
■ 操作性の低いボタンになってしまう理由
大きく3つあると考えています。
div
要素は
-
focus
を持たないから -
returnキー
,spaceキー
をonClick
に変換しないから - スクリーンリーダーが認識しない要素だから
◎ focus を持たないから
<div>
要素はfocus
を持ちません。
なので、tabキー
で要素にフォーカスを当てようとしても当たりません。
-
div
: フォーカスが当たらない -
button
: フォーカスが当たる
例
// div, button 両方に focus 時 border style を付与
:focus {
border: 2px solid blue;
}
divに フォーカスを当てるためには tabindex を付与する
div
要素にフォーカスを当てるためにはtabindex
属性を付与する必要があります。
<div tabIndex={1}>
div
</div>
こうすればdiv
にフォーカスを当てることができます。
◎ returnキー, spaceキー を onClick に変換しないから
フォーカスできるようにしても、
div
要素はreturnキー
、 spaceキー
イベントをonClick
に変換してくれません。
-
div
:returnキー
、spaceキー
イベントをonClick
に変換しない -
button
:returnキー
、spaceキー
イベントをonClick
に変換する
例
アラートダイアログを表示する関数を定義します。
const handleClick = (element: string) => {
window.alert(element + "からクリック");
};
onClick
に関数を渡します。
<div tabIndex={1} onClick={() => handleClick("div")}>
div
</div>
<button onClick={() => handleClick("button")}>
button
</button>
button
にフォーカスが当たった状態でreturnキー
、 spaceキー
どちらかを押すとonClick
イベントが発火します。
div
の場合はreturnキー
、 spaceキー
を押してもonClick
が発火しないので、アラートダイアログは表示されません。
div でreturnキー, spaceキー で onClick を発火させるためには onKeyDown などを利用する
onKeyDown
などを利用して頑張って実装する形になります。
<div
tabIndex={1}
onClick={() => handleClick("div")}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
// Enter or Space で実行
handleClick("div");
}
}}
>
div
</div>
◎ スクリーンリーダーが認識しない要素だから
スクリーンリーダーとは、「コンピューターの画面読み上げソフト」のことです。
視覚障害を持つ方などがPCを操作する際の手助けをします。
しかし、スクリーンリーダーなどの支援ツールはdiv
要素をクリック可能な要素として認識しません。
div
要素だと要素の役割が明確ではありません。そのため、div
要素がクリック可能要素であったとしても、
スクリーンリーダーを利用して該当のdiv
要素を見つけてクリックすることが困難になります。
-
div
: スクリーンリーダーはクリック可能な要素として認識しない -
button
: スクリーンリーダーはクリック可能な要素として認識する
div をスクリーンリーダーが認識する要素にするには role を付与する
role
を付与することでスクリーンリーダーにクリック可能要素であることを認識させることができます。
<div
role="button"
tabIndex={1}
onClick={() => handleClick("div")}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
// Enter or Space で実行
handleClick("div");
}
}}
>
div
</div>
アクセシビリティ、スクリーンローダー参考資料
- https://developer.mozilla.org/ja/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#画面リーダー
- https://developer.mozilla.org/ja/docs/Learn/Accessibility/HTML
- https://www.casleydi.com/blog/engineer/6692/
<div>
に<button>
と同じ振る舞いをさせるには
◎つまり、<div>
要素を<button>
要素と同じ振る舞いをさせるためには、
tabindex
とrole
を付与して、
onKeyDown
などを利用してけっこう頑張って実装しないといけないわけです。
<div
role="button"
tabIndex={1}
onClick={() => handleClick("div")}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
// Enter or Space で実行
handleClick("div");
}
}}
>
div
</div>
■ HTML の対話型コンテンツと非対話型コンテンツ
今回、<div>
要素にonClick
を定義した場合を例に紹介してきました。
しかし実は
<div>
要素に限った話ではなく他にもonClick
を付与するなどユーザー操作イベント(イベントリスナー)を定義するべきではない要素は多くあります。
それを説明していきます。
HTML
にはコンテンツカテゴリーという概念があり、カテゴリーごとに共通した特徴を持つ要素を分類しているようです。
そのカテゴリーの1つが対話型コンテンツです。
MDNには対話型コンテンツの説明として
対話型コンテンツ (interactive content) にはユーザとのやり取りのために固有にデザインされた要素が含まれます。
と記述されています。
つまり下記のように考えられます。
- 対話型コンテンツ: ユーザーの操作を前提としている要素
- 非対話型コンテンツ: ユーザーの操作を前提としない要素
◎デフォルトで対話型コンテンツに属する要素
<a>
<button>
<details>
<embed>
<iframe>
-
<keygen>
非推奨 <label>
<select>
<textarea>
◎特定の条件下にある場合のみ対話型コンテンツに属する要素
-
<audio>
: controls 属性がある場合 -
<img>
: usemap 属性がある場合 -
<input>
: type 属性が hidden 状態ではない場合 -
<menu>
: type 属性が toolbar 状態ではない場合 -
<object>
: usemap 属性がある場合 -
<video>
: controls 属性がある場合
◎対話型コンテンツ以外が非対話型コンテンツ
対話型コンテンツ以外が非対話型コンテンツに該当します。
つまり、今回説明してきた<div>
のような挙動をする要素ということです。
そのため、<div>
に限った話ではなく
ユーザーの操作が可能である要素は、可能な限り対話型コンテンツを使って実装するベきといえます。
ESLint の no-noninteractive-element-interactions
ちなみに、ESLint にも非対話型コンテンツに不適切な役割を与えた場合に警告するものがあります。
// <div>要素にonClickを追加すると怒られる
<div onClick={handleClick}>...</div>
ESLint: Avoid non-native interactive elements.
If using native HTML is not possible, add an appropriate role and support for tabbing, mouse, keyboard, and touch inputs to an interactive content element.(jsx-a11y/no-static-element-interactions)
和訳
ネイティブでないインタラクティブな要素は避ける。
ネイティブのHTMLを使用することが不可能な場合、インタラクティブなコンテンツ要素に適切な役割とタブ、マウス、キーボード、タッチ入力のサポートを追加する
■ おまけ: 動画サービスを調べてみた
そういえば、動画サービスでよくreturnキー
やspaceキー
を押して動画の再生と停止を切り替えていたことを思い出しました。
あれはきっと対話型コンテンツで実装されているに違いないと思い、
Chrome DevTools で
YoutubeとUdemyの要素を調べてみました。
すると、Youtube も Udemy も動画の部分は<video>
要素だったものの、controls属性
が付与されておらず(false
)でした。
<video>
: controls 属性がある場合に対話型コンテンツとなる
対話型コンテンツじゃないやん!、
なんでだろ、どなたか知っている詳しい方がいれば教えてください。
追記: どうやらブラウザの標準の動画コントロールを非表示にして、独自に実装しているようでした
試しに Youtube で Chrome DevTools から controls属性
を付与(true
に)してみました。
すると、「ブラウザの標準の動画コントロール」と「Youtube が独自に実装しているであろう動画コントロール」両方が表示されました。
※ 動画コントロール:再生、停止や音量調節をする部分
おそらく、デザイン性や細かい動作を調整することが理由だと考えられます。
コメントで教えていただいた、Naughie(なっふぃ)さんありがとうございました。
■ さいごに
ユーザーの操作が可能である要素は、可能な限り対話型コンテンツを使って実装していきましょう!!
そして、ユーザーにやさしいサービスをつくっていきましょう!
最後までお読みいただきありがとうございました!
参考にさせていただいたもの
対話型コンテンツ
- https://developer.mozilla.org/ja/docs/Web/HTML/Content_categories#対話型コンテンツ
- https://www.dkrk-blog.net/html/img_click_dame_zettai
- https://catnose.me/learning/tags/対話型コンテンツ
- https://web.havincoffee.com/html/html5/com_b_interactive.html
- https://triple-underscore.github.io/HTML-interactive-elements-ja.html#the-dialog-element
- https://datacadamia.com/ui/action
- https://coliss.com/articles/build-websites/operation/work/need-to-know-about-buttons.html
- https://developer.mozilla.org/ja/docs/Learn/Accessibility/HTML
アクセシビリティ、スクリーンローダー
- https://www.casleydi.com/blog/engineer/6692/
- https://developer.mozilla.org/ja/docs/Learn/Accessibility/HTML
- https://developer.mozilla.org/ja/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#画面リーダー
tabindex
role
Discussion
YouTube の場合は,あえて
controls = false
にすることで,ブラウザの builtin 機能を使わず独自に実装しているのだと思います.こちらの記事で言うところの,div
に無理やりbutton
と同じ振る舞いをさせているように.試しに devtools で
controls=""
を指定してみると,「ブラウザの builtin のコントロール」と「YouTube が独自に実装しているコントロール」が重なって表示されますね.おそらくデザイン性と,細かい機能の差異を実装するのが理由でしょうか……?
教えていただきありがとうございます!
おっしゃる通り重なって表示されました!
コメントいただいた内容をもとに内容を少し修正しました!