CSS pointer-events 2016

Advent Calendar
CSS

今年お世話になった 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 をやめろ。


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