🐕

なぜ <div> に onClick がダメなのか?

2023/06/18に公開
2

■ はじめに

<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>

アクセシビリティ、スクリーンローダー参考資料

<div><button>と同じ振る舞いをさせるには

つまり、<div>要素を<button>要素と同じ振る舞いをさせるためには、
tabindexroleを付与して、
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 にも非対話型コンテンツに不適切な役割を与えた場合に警告するものがあります。

https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-noninteractive-element-interactions.md#jsx-a11yno-noninteractive-element-interactions

// <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 で
YoutubeUdemyの要素を調べてみました。

すると、Youtube も Udemy も動画の部分は<video>要素だったものの、controls属性が付与されておらず(false)でした。

<video>: controls 属性がある場合に対話型コンテンツとなる

対話型コンテンツじゃないやん!、
なんでだろ、どなたか知っている詳しい方がいれば教えてください。

追記: どうやらブラウザの標準の動画コントロールを非表示にして、独自に実装しているようでした

試しに Youtube で Chrome DevTools から controls属性を付与(trueに)してみました。
すると、「ブラウザの標準の動画コントロール」と「Youtube が独自に実装しているであろう動画コントロール」両方が表示されました。

※ 動画コントロール:再生、停止や音量調節をする部分

おそらく、デザイン性や細かい動作を調整することが理由だと考えられます。

コメントで教えていただいた、Naughie(なっふぃ)さんありがとうございました。

■ さいごに

ユーザーの操作が可能である要素は、可能な限り対話型コンテンツを使って実装していきましょう!!
そして、ユーザーにやさしいサービスをつくっていきましょう!

最後までお読みいただきありがとうございました!

参考にさせていただいたもの

対話型コンテンツ

アクセシビリティ、スクリーンローダー

tabindex

role

Discussion

Naughie(なっふぃ)Naughie(なっふぃ)

YouTube の場合は,あえて controls = false にすることで,ブラウザの builtin 機能を使わず独自に実装しているのだと思います.こちらの記事で言うところの,div に無理やり button と同じ振る舞いをさせているように.
試しに devtools で controls="" を指定してみると,「ブラウザの builtin のコントロール」と「YouTube が独自に実装しているコントロール」が重なって表示されますね.

おそらくデザイン性と,細かい機能の差異を実装するのが理由でしょうか……?

match35match35

教えていただきありがとうございます!
おっしゃる通り重なって表示されました!

コメントいただいた内容をもとに内容を少し修正しました!