CSS pointer-events 2016
- 公開日
- タギング
- Advent Calendar
- CSS
今年お世話になった CSS アドベントカレンダー 2016、22 日目
CSS の pointer-events プロパティーにはかねてからお世話になっていた。このプロパティーとの出会いはまぁまぁ古く、2011 年の 9 月の終わり頃だったもよう。勢い余って雑な検証しかしていない記事も書いた。HTML+CSS+JS でブラウザゲームを作っていた頃はあまりのお世話のなりすぎさに何度も感謝している痕跡がツイッターにある。
pointer-events最高!
— 越智 (@o_ti) 2014年11月11日
pointer-eventsが女だったら全力で口説いてるレベル
— 越智 (@o_ti) 2014年11月11日
pointer-eventsが人間だったら抱きたい
— 越智 (@o_ti) 2014年11月11日
pointer-events: none; に感謝の正拳突き1万回
— 越智 (@o_ti) 2014年1月31日
CSSの方のpointer-events: none;には100万回は助けられてるので6億円欲しい
— 越智 (@o_ti) 2015年2月26日
親愛なるpointer-events: noneへ。大好きなので付き合ってください。
— 越智 (@o_ti) 2016年2月25日
気持ち悪い。
先日、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 の要素に対して適用できるのは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 については、a
要素にpointer-events: none
を指定する時にdisplay: inline
のままだと 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 をやめろ。
来年もよろしくお願いいたします。