CSSで、押したときに沈むボタンを作る方法
押したときに沈むボタンは、ボタンの下にぼかしのない 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 の書き方は試していません。

側面を box-shadow で描き、:active で下げる
まず、1つの button 要素だけで作る形です。
<button class="btn" type="button">保存する</button>
.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 を読み取りました。
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 は呼ばれません。

ただし、押してから 10ms ですぐ離したときは、どちらの書き方でも click はボタンに届きました。先ほどの値では 20ms の時点で 1.5px しか下がっていないので、10ms では、押している点がまだボタンの中に残っていたためと考えられます。つまり、押している時間と押した位置によって、click が届いたり届かなかったりします。
押したままポインターをボタンの外に動かしたときも、ボタンは沈んだままでした。:hover は外れましたが :active は残り、外で離すと click はボタンではなく body に届きました。
- 外に出した後の
:active:当てはまる - 外に出した後の
:hover:当てはまらない - 外に出した後の下への移動:4px(ボタンの上の端は 28px のまま)
- 外で離した後、click が届いた要素:
body
button は動かさず、中の span を沈める
押せる範囲を動かさないようにするには、button 自体は動かさず、見た目の面を中の span に描いて、その span だけを沈めます。
<button class="btn2" type="button"><span class="btn2__face">保存する</span></button>
.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 キーで押したときです。





