CSSで、押したときに沈むボタンを作る方法

​​CSS​HTML,Playwright

押したときに沈むボタンは、ボタンの下にぼかしのない box-shadow で「側面」を描き、押している間だけ、側面の厚みの分だけボタンを下げて側面を消すと作れます。下げるのは margin ではなく transform にすると、周りの要素が動きません。ただし、ボタンそのものを下げると、上の端を押したときに click がボタンに届かないことがあるので、button は動かさずに中の span だけを沈める書き方もあわせて紹介します。

補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML と CSS だけのページを Playwright で開き、マウスとキーボードで実際に押して、getComputedStyle() と getBoundingClientRect() の値、click イベントが届いた要素を記録しました。Firefox と Safari、スマートフォンの実機でのタッチ操作、Enter キーでも沈ませるための JavaScript の書き方は試していません。

「保存する」ボタンを3回クリックする。押している間はボタンが下がって下の濃い色の側面が消え、「押している間(:active)」と表示される。離すとボタンが戻り、「押した回数」が1つ増える
中の span を沈める書き方のボタンを3回クリックしたときの動き。沈む・戻る動きは、見やすいように実際の2倍の時間をかけて再生している(上の回数表示、マウスの絵、ピンクの文字は GIF 用に足したもの)

側面を box-shadow で描き、:active で下げる

まず、1つの button 要素だけで作る形です。

html
<button class="btn" type="button">保存する</button>
css
.btn {
  --edge: 4px; /* 側面の厚み。押したときに沈む距離 */
  padding: 12px 24px;
  border: none;
  border-radius: 8px;
  font: inherit;
  font-weight: bold;
  color: #fff;
  background: #067a98;
  box-shadow: 0 var(--edge) 0 #04566b; /* ぼかしのない影を、下に側面として出す */
  cursor: pointer;
  transition: transform 80ms ease-out, box-shadow 80ms ease-out;
}

.btn:active:not(:disabled) {
  transform: translateY(var(--edge)); /* 側面の厚みの分だけ下げる */
  box-shadow: 0 0 0 #04566b; /* 側面を消す */
}

.btn:disabled {
  background: #9aa5ab;
  box-shadow: 0 var(--edge) 0 #6f7a80;
  cursor: not-allowed;
}

box-shadow は、横と縦のずらし、ぼかし、色で影を描くプロパティです。横のずらしとぼかしを 0 にして、縦にだけ 4px ずらすと、ボタンの下に濃い色の帯が出ます。これがボタンの側面に見えます。

:active は、ユーザーが要素をアクティブにしている間に当てはまる擬似クラスです。マウスでは、主ボタンを押し下げたときから当てはまります。押している間は、transform でボタンを 4px 下げ、同時に影のずらしを 0 にします。ボタンの上の面が、側面のあった位置まで下りてくるので、押し込んだように見えます。

Chromium でボタンを押したまま、値を読み取った結果です。

押す前 押している間 離した後
:active 当てはまらない 当てはまる 当てはまらない
下への移動 なし 4px なし
側面の厚み 4px 0px 4px
ボタンの上の端 24px 28px 24px

押している間は、ボタンの上の端が 24px から 28px に下がり、側面の厚み(影の縦のずらし)が 4px から 0px になりました。離すと元に戻り、click はボタンに届いています。

transition を 80ms にしているのは、沈む動きを見せつつ、押した手ごたえが遅れないようにするためです。transition を一時停止し、時間を 20ms ずつ進めながら transform を読み取りました。

text
0ms matrix(1, 0, 0, 1, 0, 0)
20ms matrix(1, 0, 0, 1, 0, 1.51255)
40ms matrix(1, 0, 0, 1, 0, 2.73857)
60ms matrix(1, 0, 0, 1, 0, 3.62614)
80ms matrix(1, 0, 0, 1, 0, 4)

matrix() の最後の数字が、下に動いた距離です。ease-out なので、最初の 20ms で 1.5px 動き、残りはゆっくり 4px に落ち着きます。

下げるのは margin ではなく transform にする

ボタンを下げるだけなら、:active で margin-top: 4px を付ける書き方もできます。2つの書き方で、ボタンの下に置いた段落の位置を比べました。表の数字は、押す前 → 押している間の位置です。

ボタンの上の端 下の段落の上の端
margin-top で下げる 172px → 176px 236px → 240px
transform で下げる 276px → 280px 340px → 340px(動かない)

どちらもボタンは 4px 下がりますが、margin-top では下の段落も 4px 押し下げられました。margin はレイアウトそのものを変えるので、ボタンを押すたびに周りの要素が動きます。transform は描く位置を動かすだけなので、段落は動きませんでした。

box-shadow も、レイアウトの大きさには含まれません。MDN の box-shadow のページにも、ボックスシャドウはボックスモデルの寸法に影響を与えないとあります。実際、ボタンの高さは 48px で、側面の 4px は入っていませんでした。すぐ下に別の要素を置くと、側面がその要素に重なるので、ボタンの下には 4px 以上の余白を空けておきます。

上の端を押すと、click がボタンに届かないことがある

ボタンそのものを下げる書き方には、1つ落とし穴があります。ボタンの上の端のすぐ近くを押すと、ボタンが下がったせいで、押している点がボタンの外に出てしまいます。

上の端から 1px 下を押したまま、その点にある要素を調べました。ボタンごと沈める書き方と、次の節で紹介する、中の span だけを沈める書き方で比べています。

ボタンごと沈める書き方では、次のようになりました。

  • 押す前のボタンの上の端:24px(押した点の y 座標は 25px で、そこにある要素はボタン)
  • 押している間のボタンの上の端:28px
  • 押している間、押した点にある要素:外側の div
  • click が届いた要素:外側の div

中の span だけを沈める書き方では、次のようになりました。

  • 押す前の button の上の端:96px(押した点の y 座標は 97px で、そこにある要素はボタン)
  • 押している間の button の上の端:96px(動かない)
  • 押している間の面(span)の上の端:100px
  • 押している間、押した点にある要素:ボタン
  • click が届いた要素:ボタン

ボタンごと沈める書き方では、押す前は y=25 の点にボタンがありました。押している間にボタンの上の端が 28px まで下がったので、同じ点はボタンを包む div の上になります。そのまま離すと、click はボタンではなく、外側の div に届きました。

click イベントの説明にあるとおり、マウスのボタンを押した要素と離した要素が違うと、click は両方を含むいちばん近い祖先の要素で起きます。この場合は、押したのが button、離したのが外側の div なので、click は div で起き、ボタンの onclick は呼ばれません。

上下2つの比較。上はボタンごと沈めたもので、黒の点線(押す前の上の端)のすぐ下にあるピンクの点が、button 要素の範囲を示すピンクの破線の枠ごと下がったボタンの外に出ている。押している点の下の要素も click の行き先も「外側の div」。下は中の span を沈めたもので、ピンクの破線の枠は黒の点線の位置から動かず、点は枠の中にある。点の下の要素も click の行き先も「ボタン」
上の端から 1px 下を押したまま撮ったもの。ピンクの破線の枠が button 要素の範囲で、ボタンごと沈めると枠も下がって押している点が外に出る。span を沈めると枠は動かず、点は枠の中に残る

ただし、押してから 10ms ですぐ離したときは、どちらの書き方でも click はボタンに届きました。先ほどの値では 20ms の時点で 1.5px しか下がっていないので、10ms では、押している点がまだボタンの中に残っていたためと考えられます。つまり、押している時間と押した位置によって、click が届いたり届かなかったりします。

押したままポインターをボタンの外に動かしたときも、ボタンは沈んだままでした。:hover は外れましたが :active は残り、外で離すと click はボタンではなく body に届きました。

  • 外に出した後の :active:当てはまる
  • 外に出した後の :hover:当てはまらない
  • 外に出した後の下への移動:4px(ボタンの上の端は 28px のまま)
  • 外で離した後、click が届いた要素:body

button は動かさず、中の span を沈める

押せる範囲を動かさないようにするには、button 自体は動かさず、見た目の面を中の span に描いて、その span だけを沈めます。

html
<button class="btn2" type="button"><span class="btn2__face">保存する</span></button>
css
.btn2 {
  --edge: 4px;
  padding: 0 0 var(--edge); /* 側面の分だけ、button の高さを足しておく */
  border: none;
  border-radius: 8px;
  font: inherit;
  font-weight: bold;
  background: none;
  cursor: pointer;
}

.btn2__face {
  display: block; /* インラインのままだと transform が効かない */
  padding: 12px 24px;
  border-radius: 8px;
  color: #fff;
  background: #067a98;
  box-shadow: 0 var(--edge) 0 #04566b;
  transition: transform 80ms ease-out, box-shadow 80ms ease-out;
}

.btn2:active:not(:disabled) .btn2__face {
  transform: translateY(var(--edge));
  box-shadow: 0 0 0 #04566b;
}

.btn2:disabled .btn2__face {
  background: #9aa5ab;
  box-shadow: 0 var(--edge) 0 #6f7a80;
}

.btn2:disabled {
  cursor: not-allowed;
}

button の下の padding を側面と同じ 4px にしているので、button の高さは 52px、中の面は 48px になりました。button は側面まで含んだ大きさになり、押している間も上の端は 96px のままです。下がるのは中の面だけ(96px から 100px)なので、上の端を押しても、押している点は button の中に残り、click は button に届きました。

span には display: block が要ります。transform は、置換要素でないインラインボックスには効かないからです。display: inline のままの span で試すと、getComputedStyle() の transform は matrix(1, 0, 0, 1, 0, 4) になるのに、面の上の端は 84px のまま動きませんでした。

押す前 押している間
:active 当てはまらない 当てはまる
getComputedStyle() の transform none matrix(1, 0, 0, 1, 0, 4)
面(span)の上の端 84px 84px(動かない)

disabled のボタンは :not(:disabled) で沈めない

:active のセレクターに :not(:disabled) を付けているのは、Chromium では、disabled のボタンを押している間も :active が当てはまったからです。

disabled のボタンを押している間の値です。

記事の CSS :not(:disabled) を付けない CSS
:active 当てはまる 当てはまる
下への移動 なし 4px
側面の厚み 4px 0px
click 起きない 起きない

記事の CSS では、:active は当てはまっても、transform は none のままで沈みません。:not(:disabled) を付けずに :active だけで沈めたボタンは、押せないはずなのに 4px 沈みました。どちらも click は起きていません。押せないボタンが沈むと、押せたように見えてしまうので、:not(:disabled) で外しておきます。

キーボードで押したときの沈み方

ボタンにフォーカスを当て、Space キーと Enter キーで押しました。

:active 下への移動 click
Space を押している間 当てはまる 4px まだ起きない
Space を離した後 当てはまらない なし 起きる
Enter を押している間 当てはまらない なし 起きる
Enter を離した後 当てはまらない なし 新たには起きない

Space キーは、押している間 :active が当てはまってボタンが沈み、離したときに click が起きました。マウスと同じ動きです。一方、Enter キーは押した時点で click が起き、:active は当てはまらないので、ボタンは沈みませんでした。この CSS でボタンが沈むのは、マウスと Space キーで押したときです。

​Posted by 86