CSS pointer-events 2016

今年お世話になったCSSアドベントカレンダー2016、22日目

CSSのpointer-eventsプロパティーにはかねてからお世話になっていた。このプロパティーとの出会いはまぁまぁ古く、 2011年の9月の終わり頃だったもよう。勢い余って雑な検証しかしていない記事も 書いた。HTML+CSS+JSでブラウザゲームを作っていた頃はあまりのお世話のなりすぎさに何度も感謝している痕跡がツイッターにある。

気持ち悪い。

先日、 DIST.13というイベントでLTをしてpointer-eventsプロパティーの凄さと凄くなさを 少し紹介させてもらってた。持ち時間が5分だったのでa要素に対する指定時の挙動に焦点を当てて話した。LTの内容を下記にかいつまんでおく。


pointer-eventsではその名の通りポインターデバイス(マウスやタッチ)によるイベントを制御できるCSSプロパティーで、アンカーリンクにpointer-events: noneを指定するとイベントをトリガーしなくなる。これを利用して ナビゲーションの現在地のリンクを無効にできる。CSS的には:link :visited :hover :active といった擬似クラスが当たらなくなる。

しかし、キーイベントは通常どおりトリガーされるので、Tabキーでa要素にフォーカスできるし、Enterキーを押せば指定されたURLへ移動できてしまう。リンクの無効化を本気でやるなら普通にa要素をやめるかJavaScriptで制御するしかない。

また、pointer-events: noneを指定した要素の子にpointer-events: autoを指定すると、その子要素はポインターイベントをトリガーするので親要素へイベントのバブリングが起こり、そのさらに親要素へ(最終的にはルート要素まで)イベントは伝わっていく。複雑なイベント管理をしなければならないコンテンツではpointer-eventsに頼れない。


pointer-events: noneのイベントの透過とバブリングを確認できるデモを作ってみた。

divには擬似要素でクラス名が出るようにしている。tomato色のボーダーの各div上でクリックするとdiv:activeによりボーダーがpurpleになるはずだが、skyeblue色のボーダーに設定したdiv.item以外はpointer-events: noneが指定されているのでボーダーの色は変わらない。クリックイベントは透過されて最背後bodyをクリックしていることになるので、body:activeのスタイルが適用される。

div.itemをクリックした場合は自身のボーダー色が変わるとともに、:activeがバブリングして親要素のdivのボーダー色も変わり、bodyの背景色も変わる。

このようにpointer-events: noneは自身のマウス/タッチ操作を無効にするだけで、event.stopPropagation()のようにバブリングを止めるわけではないことには注意が必要だと思う。


ブラウザ対応とバグ(あるいは仕様)

pointer-eventsプロパティーは元はSVGの仕様で、HTMLの要素に対して適用できるのはautononeallとなっているが、SVG要素向けにfillstrokeなどの値もある。

仕様はこの辺り。

Can I use…から確認できる情報をまとめると以下の通り。

ブラウザ バージョン リリース日
Internet Explorer 11 2013/10/17
Edge 12+ 2015/5/30
Firefox 3.6+ 2010/1/21
Googel Chrome 4+ 2010/1/25
Safari 4+ 2009/7/8
Opera 15+(Blink) 2013/6/2
iOS Safari 3.2+ 2010/4/3
Android Browser 2.1+ 2009/10/26
Android Chrome 54+ 2016/10/19

さらにCan I use…では下記がIssueとしてリストアップされている。

  1. IE9, 10ではJavaScriptでdocument.documentElement.style.pointerEventsを呼ぶと空文字列を返すのでCSS的には対応しているように見えるが、SVG要素に対してのみの対応なのでHTML要素には使えない。
  2. IE11ではselect要素の親にpointer-events: noneを指定してもselect要素が無効にならないが、select要素に指定すると効く。
  3. overflow: scrollな要素にpointer-events: noneを指定すると、Firefoxではスクロール不能になるが、ChromeとIE11ではスクロールバーをクリックすることでスクロールできてしまう。
  4. IE11とEdgeでは、a要素のdisplayの値がinline-blockblock以外だとpointer-events: noneを指定しても効かない。

1については、JavaScriptを使ってブラウザのCSSプロパティーの対応を調べる時の話だ。IE9,10ではHTML要素へのpointer-eventsプロパティー指定は有効ではないが、SVG要素への指定が有効なためにdocumentElement.stylepointerEventsプロパティー(ここではオブジェクトとしての「プロパティー」という意味)が存在する。HTMLの要素にCSSのpointer-eventsプロパティーを使いたい時に、document.documentElement.style.pointerEventsを用いてブラウザ判定しているとハマるので注意が必要となる。だけど もはや2017年なのでIE10以下のことは忘れていいと思う。

2については、IE11はselect要素の親にpointer-events: noneを指定しても子要素のselectが通常通りプルダウンが展開できてしまうというもの。らしいのだが、手元で確認したところどうもそうではなかった。

Windows 10のIE11ではselectの親にpointer-events: noneを指定した場合も期待通りポインターイベントは封じられ、selectのプルダウンは展開されなくなった。しかし、 Windows 8.1のIE11では、select要素自体への指定でもその親への指定でも、両方ともpointer-events: noneは効かずにプルダウンが展開できてしまった。modern.ieからダウンロードできるVirtualBoxのイメージを利用したので、実機では違うのかもしれない。

Test: pointer-events: none to select element

3については、overflow: scrollなどとしたスクロール可能な要素にpointer-events: noneを指定すると、その要素上でのスクロール(スクロールバーのクリック、およびマウスホイールでのスクロール)ができなくなるはずだが、ChromeとIE11はできてしまうというもの。らしいのだが、これも現在は状況が異なるようだった。

手元では Safari 10.0.1(El Capitan/Sierra)でスクロールバーのクリックでスクロールできてしまうが、ChromeとIE11(Win 8.1/10)はちゃんとクリックできなくなっているのでスクロールもできない。FirefoxとEdgeは期待通りスクロールバーのクリックもpointer-events: noneによって無効化されていた。Macでは環境設定 > 一般 > スクロールバーの表示のところで「常に表示」を選択すれば確認できる。

しかし、Windows 10だとテキスト選択で下へスクロールができた。これはブラウザは問わないようだった。Windows 8.1以下もできるのかもしれない。

Test: pointer-evenst: none to scrollable element

4については、a要素にpointer-events: noneを指定する時にdisplay: inlineのままだとIE11とEdgeでポインターイベントが無効にならないというもの。Can I use…ではinline-blockblock以外だとだめみたいな書き方だったが、実際のところはinline以外ならなんでも良いようだった。

Test: pointer-events: none and display: * to a element

今回はデスクトップブラウザでしか見ていない。モバイル版のChromeやSafariだとさらに状況が複雑かもしれない。

アクセシビリティーの視点

DISTで発表しておいて申し訳ないのだけど、個人的にはインタラクティブ要素(リンクやフォームパーツ、デベロッパーがカスタムイベントを定義した要素)をpointer-events: noneするのはよろしくないと思っている。制御できるのはポインターデバイスのイベントだけで、キーボードは関係ない。要素のroleも変わらないので、特定のa要素に対してpointer-events: noneを指定してもスクリーンリーダーでは通常のリンクとして読み上げられるし、フォーカス後のキーエンターでページ移動することもできる。tabindex属性がついている要素であればインタラクティブ要素でなくてもフォーカスできるので、キー操作の対象になりうる。前述した スクロールできる要素にpointer-events: noneを指定したデモで一番右のボックスはフォーカスできるようにしているので、フォーカス後にスペースキーや上下キーでスクロールできるのは試してみればわかる。

つまり、インタラクティブ要素にpointer-events: noneを指定すると「ある種の入力では操作不能で、別の入力では操作可能」になってしまう。これでいいと思う人はそんなにいないはずだ。pointer-events: nonedisabledな状態を代替しようとするのが間違っているのだろう。

ではどこで役に立つのか?

この世の中には、IE11でform label imgなHTMLの時に画像をクリックしてもlabelへイベントがバブリングしないという現実がある。labelにバブリングしないとfor属性で紐づけたinput要素にフォーカスしないし、 ラジオボタンやチェックボックスがトグルできない

IE11では、ブラウザUIのパーツをクリックすれば選択できるが、画像の上をクリックしても選択が反映されない。

この問題は当該箇所の画像にpointer-events: noneを指定することで簡単に解決できた。IE8対応をしていた頃はブラウザ分岐やらをしてJSで管理していたけどもうそれはしなくてよくなった。「ポインターイベントを透過させる」ことをうまく利用できた例だと思う。

また、pointer-events: noneはイベントを透過させるので、z軸上で上に被さった要素を「触らせない」ようにできる。込み入った装飾をposition: fixedで最前面に表示している時、通常ならかぶさっている要素のせいでz軸上の下側の要素は押せなくなる。そういった装飾要素にpointer-evenst: noneをあてればイベントが透過するので下側のコンテンツのリンクなども触れるようになる。

inputselect に擬似要素で作ったなんかのアイコンを乗せている時も、そのアイコンにpointer-events: noneしておくとクリックの時に邪魔にならない。

繰り返すが依然としてキーボードのイベントはなかったことにはできないから、使う場合はインタラクティブ要素でないことが望ましいと思う。フォーカスする必要がなく、クリックイベントをトリガーしてしまうが故に邪魔になるタイプには使っていい。インタラクティブ要素でイベントを封じたいなら普通にJavaScriptを書こう。

込み入った装飾のposition: fixedな要素はそもそもfixedをやめろ。


来年もよろしくお願いいたします。