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 の要素に対して適用できるのは auto
、none
、all
となっているが、SVG 要素向けに fill
や stroke
などの値もある。
仕様はこの辺り。
- pointer-events - CSS | MDN
- SVG1.1 Interactivity - 16.6
- #pointer-events - css4-ui feature list [CSS Working Group Wiki]
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 としてリストアップされている。
- IE9/10 では JavaScript で
document.documentElement.style.pointerEvents
を呼ぶと空文字列を返すので CSS 的には対応しているように見えるが、SVG 要素に対してのみの対応なので HTML 要素には使えない。 - IE11 では
select
要素の親にpointer-events: none
を指定してもselect
要素が無効にならないが、select
要素に指定すると効く。 overflow: scroll
な要素にpointer-events: none
を指定すると、Firefox ではスクロール不能になるが、Chrome と IE11 ではスクロールバーをクリックすることでスクロールできてしまう。- IE11 と Edge では、
a
要素のdisplay
の値がinline-block
かblock
以外だと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: inline
の a
要素に pointer-events: none
を指定する場合 IE11 と Edge でポインターイベントが無効にならないというもの。Can I use…では inline-block
か block
以外だとだめみたいな書き方だったが、実際のところは 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: none
で disabled
な状態を代替しようとするのが間違っているのだろう。
ではどこで役に立つのか?
この世の中には、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
をあてればイベントが透過するので下側のコンテンツのリンクなども触れるようになる。
input
や select
へ擬似要素で作ったなにかのアイコンを乗せているときなど、アイコンを pointer-events: none
しておくとクリックのとき邪魔にならない。
繰り返すが依然としてキーボードのイベントはなかったことにはできないから、使う場合はインタラクティブ要素でないことが望ましいと思う。フォーカスする必要がなく、クリックイベントをトリガーしてしまうが故に邪魔になるタイプには使っていい。インタラクティブ要素でイベントを封じたいなら普通に JavaScript を書こう。
込み入った装飾の position: fixed
な要素はそもそも fixed をやめろ。
来年もよろしくお願いいたします。