CSSで、ボタンの押せる範囲を見た目より広げる方法

​​CSS​HTML,Playwright,アクセシビリティ

ボタンの押せる範囲を見た目より広げるには、ボタンに position: relative を付け、透明な ::before を position: absolute; inset: -10px; でボタンの外にはみ出させます。はみ出した部分をクリックしても、click はボタンに届きます。見た目もレイアウトも変わりません。ただし、隣のボタンと並べたときは、広げる幅を隙間の半分までにしないと、隙間を押したときに隣のボタンのほうが反応します。

補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML と CSS だけのページを Playwright で開き、位置を決めてマウスでクリックして、click イベントが届いた要素と document.elementFromPoint() の結果を記録しました。タッチ操作は Playwright のタッチのエミュレーションで試しただけで、スマートフォンの実機では試していません。Firefox と Safari も試していません。

直径 24px の丸いボタンの周りに、ボタンに click が届く範囲を薄いピンクの四角で示した画面。マウスで4回クリックする。1回目は円の中、2回目は円の右端から 8px 外、3回目はボタンの箱の左下の角から左と下に 8px ずつ外で、どれも「ボタンに届いた click」の回数がピンクの文字で1つずつ増える。4回目は円の右端から 16px 外で、回数は3回のまま「届かなかった」と灰色で出る
::before で押せる範囲を広げたボタンを4回クリックしたときの動き(薄いピンクの範囲、マウスの絵、下の表示は GIF 用に足したもの)

::before をボタンの外にはみ出させる

例として、直径 24px の丸い「閉じる」ボタンを、44px 四方まで押せるようにします。

html
<button class="icon-btn" type="button" aria-label="閉じる">×</button>
css
.icon-btn {
  position: relative;
  width: 24px;
  height: 24px;
  padding: 0;
  border: none;
  border-radius: 50%;
}

.icon-btn::before {
  content: "";
  position: absolute;
  inset: -10px;
}

inset は top・right・bottom・left をまとめて書くプロパティです。-10px にすると、::before はボタンの四方に 10px ずつはみ出し、24 + 10 + 10 で 44px 四方になります。position: absolute の要素は、位置指定された一番近い祖先を基準に置かれるので、ボタンに position: relative を付けて、ボタンを基準にしています。

検証環境で、見た目の円の右端から右へ少しずつ離れた位置をクリックしました。

円の右端からの距離 何もしないボタン ::before で広げたボタン
2px 届かない 届く
6px 届かない 届く
9px 届かない 届く
11px 届かない 届かない

広げたボタンでは、10px の内側まで click がボタンに届き、10px より外では届きませんでした。click イベントの event.target はボタンそのもので、::before を気にせずにボタンのイベントとして受け取れます。

はみ出した部分にマウスを重ねると、ボタンは :hover に当てはまりました。cursor は子に引き継がれるプロパティなので、ボタンに cursor: pointer を書いていれば、はみ出した部分でも指の形のカーソルになります(検証では、::before の cursor は pointer でした)。

::before は透明で、ボタンの外にはみ出しているだけなので、ボタンの大きさや、周りの要素の位置は変わりません。検証でも、ボタンの右に置いた文字の位置は、何もしないボタンのときと同じでした。

padding と負の margin で広げる方法との違い

::before を使わずに、ボタン自体の padding を増やし、同じ分だけ負の margin で打ち消す方法もあります。背景を background-clip: content-box で中身の部分だけに塗れば、見た目の大きさは変わりません。

html
<button class="pad-btn" type="button" aria-label="閉じる"><span>×</span></button>
css
.pad-btn {
  padding: 10px;
  margin: -10px;
  border: none;
  border-radius: 50%;
  box-sizing: content-box;
  background: #067a98;
  background-clip: content-box;
}

.pad-btn > span {
  display: block;
  width: 24px;
  height: 24px;
}

ボタンの大きさは中の span で決めます。

この方法でも、ボタンの右の文字の位置は変わらず、円の右端から 9px 外のクリックはボタンに届きました。ボタンの箱(getBoundingClientRect())は 44×44px になります。

違いが出たのは、ボタンの箱の角の部分でした。elementFromPoint() で、ボタンに当たる位置を 1px ずつ調べて塗ると、次のようになりました。

3つのボタンを縦に並べ、ボタンに click が届く範囲をピンクで塗った画面。何もしないボタンは、見た目の円と同じ直径 24px の範囲だけで、ボタンに隠れてほとんど見えない。::before を外に広げたボタンは、44×44px の四角。padding と負の margin で広げたボタンは、直径 44px の円で、四角の角の部分は塗られていない
ボタンに click が届く範囲(ピンク)。elementFromPoint() で 1px ずつ調べて塗った

padding で広げた方法では、ボタンの箱の左上の角から 2px 内側をクリックしても、ボタンには届きませんでした。CSS の仕様(CSS Backgrounds and Borders Module Level 3)では、border-radius を付けた要素は、ポインターのイベントを受け付ける範囲も、丸めた border の外側の線で切り取られます。padding で広げると、border-radius: 50% も広がった箱に効くので、押せる範囲は直径 44px の円になります。::before には border-radius を付けていないので、四角のままです。

角まで押せるようにしたいなら ::before の方法、丸いボタンで円のままでよいなら padding の方法、と使い分けられます。padding の方法は ::before を使いません。

隣のボタンとの隙間より広げると、隣のボタンが反応する

隙間 8px で並べた「前へ」「次へ」の2つのボタンを、どちらも inset: -10px で広げました。片方を 10px 広げただけで、8px の隙間を越えてしまいます。

この状態で、「前へ」の右端から 1px 右(隙間のいちばん左)をクリックすると、click は「次へ」に届きました。3px、5px、7px の位置も、すべて「次へ」でした。8px の隙間は、どこを押しても「次へ」が反応します。さらに、「前へ」の円の右端から 1px 内側をクリックしても、「次へ」に届きました(3px 内側は「前へ」)。「次へ」の ::before が、「前へ」の円の右端の 2px にまで入り込んでいるためです。

2つの例を縦に並べた画面。上は、隙間 8px で2つのボタンを 10px ずつ広げた例で、水色(左のボタン「‹」の範囲)とピンク(右のボタン「›」の範囲)の境目は「‹」の円の右端より少し左にあり、隙間はすべてピンクになっている。下は 4px ずつ広げた例で、隙間の真ん中で水色とピンクが分かれている
隣り合うボタンの、click が届く範囲(水色が「‹」、ピンクが「›」)

2つの ::before が重なった部分では、上に描かれているほうがクリックを受け取ります。document.elementsFromPoint() の仕様(CSSOM View Module)でも、その位置にある要素を、描かれる順の上から並べることになっています。どちらのボタンも position: relative なので、z-index のない重ね合わせでは HTML に出てくる順に下から描かれ、後ろにある「次へ」の ::before が上に来ます。

広げる幅を隙間の半分の 4px にすると、「前へ」の円の中と、右端から 1px・3px は「前へ」、4px から右(4px・5px・7px)は「次へ」に届き、隙間の真ん中で分かれました。

ポイント

並んだボタンでは、広げる幅を隙間の半分までにします。隙間 8px で 10px ずつ広げると、隙間を押したときに、後ろにあるボタンが反応します。

overflow: hidden の親の外には広がらない

ボタンを overflow: hidden の要素の中に置き、ボタンの左上の角を、親の左上の角にそろえました。ボタンの右や下の 6px 外(親の中)をクリックするとボタンに届き、左や上の 6px 外(親の外)をクリックしても届きませんでした。

MDN の overflow のページにあるとおり、hidden では親の padding の枠からはみ出した部分が切り取られます。はみ出した ::before も同じで、切り取られた部分は押せません。カードの右上の「閉じる」ボタンのように、overflow: hidden の要素の端にボタンを置くときは、親の外に出る側は広がらないので、ボタンを内側に寄せて置きます。

どこまで広げるか

WCAG 2.2 の達成基準 2.5.8「ターゲットのサイズ(最低限)」(レベル AA)では、ポインターで操作するターゲットの大きさを、少なくとも 24×24 CSS ピクセルにすることを求めています。24px より小さいターゲットでも、それぞれの中心に直径 24px の円を描いて、円がほかのターゲットや円と重ならなければ、例外の1つとして認められます。レベル AAA の 2.5.5「ターゲットのサイズ(強化)」では、44×44 CSS ピクセルです。

WCAG の「ターゲット」の定義は、ポインターの操作を受け付ける画面の領域です。定義には、ターゲットどうしが重なっているときは、同じ操作をする場合や同じページを開く場合を除いて、重なった部分を大きさに含めないことも書かれています。隣のボタンとの節で見たように、広げた範囲どうしが重なっても、重なった部分は大きさに数えられません。

2.5.8 の解説(Understanding)には、見た目の大きさを変えずに、操作できる範囲を広げる選択肢を用意すると役に立つことがある、とも書かれています。

タッチのエミュレーションでは、CSS の範囲より外でも反応する

Playwright でタッチ操作のエミュレーション(hasTouch: true、isMobile: true)を有効にし、何もしないボタンの右端から 14px 外や 20px 外をタップしても、click はボタンに届きました。30px 外では届きませんでした。hasTouch: true のまま isMobile: false にすると、何もしないボタンは 11px 外まで届き、14px 外では届きませんでした。

Chromium のソースにある touch_adjustment.cc には、タップの位置を、近くにある押せる要素に寄せる処理(touch adjustment)が書かれています。タッチで試すと、CSS で決めた範囲よりも外で反応することがあるので、押せる範囲が CSS のとおりになっているかは、マウスでクリックするか、document.elementFromPoint(x, y) で、その位置にある要素を調べて確かめます。

js
// ボタンの右端から 9px 外の位置にある要素を調べる
const r = document.querySelector('.icon-btn').getBoundingClientRect();
document.elementFromPoint(r.right + 9, r.top + r.height / 2);

::before で広げた位置なら、ボタンの要素が返ります。この記事の画像の、押せる範囲を塗った部分も、この方法で 1px ずつ調べて塗りました。

​Posted by 86