モードの変更

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: 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.style.pointerEvents が存在する。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 については、display: inlinea 要素に pointer-events: none を指定する場合 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 をやめろ。


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