- CSS
CSSボタンデザインのコピペ集|ホバーアニメーションと4つの状態
CSS のボタンは、「色とサイズを変数で持つ土台のクラス」を1つ作り、hover・focus-visible・active・disabled の4つの状態をそこで決めておくと、デザインを何種類に増やしても操作感がそろいます。 見た目のバリエーションは、土台に小さなクラスを1つ足すだけで作れます。
この記事では、同じ土台の上に 25 種類のボタンデザインを用意し、すべてのボタンで4つの状態が動くことを Playwright(Chromium 141)で計算値を取って確認しました。 ホバーアニメーションは、スマートフォンでタップしたあとに色が残らないよう @media (hover: hover) で囲み、動きを減らす設定(prefers-reduced-motion)にも対応しています。後半では、a タグと button の使い分け、タップ領域 44px、文字と背景のコントラスト比も、測った数値で整理します。
CSSボタンデザインの早見表
すべて <button class="btn btn--〇〇"> の形で使います。
| No. | デザイン | クラス | ホバーしたときの動き |
|---|---|---|---|
| 1 | 塗り(基本) | btn |
色が少し濃くなる |
| 2 | 枠線 | btn btn--outline |
塗りに変わる |
| 3 | 文字だけ(ゴースト) | btn btn--ghost |
淡い背景が出る |
| 4 | 淡色 | btn btn--soft |
背景が少し濃くなる |
| 5 | 角なし | btn btn--square |
基本と同じ |
| 6 | ピル(丸い両端) | btn btn--pill |
基本と同じ |
| 7 | グラデーション | btn btn--grad |
色が横に流れる |
| 8 | グラデーションの枠線 | btn btn--grad-border |
グラデーションの塗りに変わる |
| 9 | 立体 | btn btn--3d |
少し持ち上がり、押すと沈む |
| 10 | 影つき | btn btn--shadow |
浮き上がって影が広がる |
| 11 | ぼかさない影(ポップ) | btn btn--pop |
影のほうへ動く |
| 12 | 光る(グロー) | btn btn--glow |
まわりが光る |
| 13 | 輪が広がる | btn btn--ring |
半透明の輪が出る |
| 14 | 少し大きくなる | btn btn--scale |
1.05 倍になる |
| 15 | 矢印が動く | btn btn--arrow |
矢印が右へ 4px 動く |
| 16 | 矢印が出てくる | btn btn--arrow-in |
隠れていた矢印が現れる |
| 17 | 背景が左から流れる | btn btn--slide |
左から色が入る |
| 18 | 背景が下から上がる | btn btn--rise |
下から色が入る |
| 19 | 背景が中央から広がる | btn btn--center |
中央から色が広がる |
| 20 | 光が走る | btn btn--shine |
斜めの光が横切る |
| 21 | 下線が伸びる | btn btn--underline |
左から下線が伸びる |
| 22 | アイコン付き | btn + svg |
基本と同じ |
| 23 | アイコンだけ | btn btn--icon-only |
基本と同じ |
| 24 | 読み込み中 | btn is-loading |
操作を受け付けず、回転する輪を表示 |
| 25 | 全幅 | btn btn--block |
基本と同じ |
| 4つの状態 | いつ | セレクタ | この記事の土台での見た目 |
|---|---|---|---|
| hover | マウスを載せた | @media (hover: hover) の中の .btn:hover |
色を 15% 暗く |
| focus-visible | キーボードでフォーカスした | .btn:focus-visible |
3px の輪(outline) |
| active | 押している間 | .btn:active |
色を 30% 暗く、1px 下へ |
| disabled | 押せない | .btn:disabled, .btn[aria-disabled="true"] |
透明度 0.45、操作を受け付けない |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「CSSボタンデザインのコピペ集|ホバーアニメーションと4つの状態」
確認した環境
| 項目 | 内容 |
|---|---|
| ブラウザ | Chromium 141.0.7390.37(Playwright から起動) |
| 確認方法 | マウスを載せる・押す・Tab キーで移動する、の操作を行い、getComputedStyle で計算値を取得 |
| タッチ端末 | Playwright の hasTouch: true, isMobile: true(幅 390px)で再現 |
色の計算に使っている color-mix() は、主要ブラウザで 2023 年から使える関数です(対応状況は MDN で確認)。
土台のCSS(色・サイズを変数で変える)
最初に、すべてのボタンが共通で使う土台です。これを一度貼っておけば、あとのデザインは数行ずつ足すだけで使えます。
/* ===== 土台:色とサイズは変数で変える ===== */
.btn {
--btn-color: #2563eb; /* 主役の色 */
--btn-on: #ffffff; /* 主役の色の上に載せる文字色 */
--btn-hover: color-mix(in srgb, var(--btn-color), #000 15%); /* ホバー時の色 */
--btn-active: color-mix(in srgb, var(--btn-color), #000 30%); /* 押している間の色 */
--btn-tint: color-mix(in srgb, var(--btn-color), #fff 88%); /* 淡い背景色 */
--btn-radius: 8px;
--btn-font: 1rem;
--btn-py: 0.625em;
--btn-px: 1.25em;
--btn-speed: 0.2s;
position: relative;
box-sizing: border-box; /* a と button で高さをそろえる */
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5em;
min-height: 44px; /* タップ領域 */
min-width: 44px;
padding: var(--btn-py) var(--btn-px);
border: 2px solid var(--btn-color);
border-radius: var(--btn-radius);
background-color: var(--btn-color);
color: var(--btn-on);
font: inherit;
font-size: var(--btn-font);
font-weight: 600;
line-height: 1.2;
text-decoration: none;
cursor: pointer;
user-select: none;
touch-action: manipulation; /* ダブルタップ拡大の待ち時間をなくす */
-webkit-tap-highlight-color: transparent;
transition: background-color var(--btn-speed), border-color var(--btn-speed),
color var(--btn-speed), box-shadow var(--btn-speed), transform var(--btn-speed);
}
/* 状態1:hover(ホバーできる端末だけ) */
@media (hover: hover) {
.btn:hover {
background-color: var(--btn-hover);
border-color: var(--btn-hover);
}
}
/* 状態2:focus-visible(キーボード操作のときだけ輪を出す) */
.btn:focus-visible {
outline: 3px solid var(--btn-color);
outline-offset: 3px;
}
/* 状態3:active(押している間) */
.btn:active {
background-color: var(--btn-active);
border-color: var(--btn-active);
transform: translateY(1px);
}
/* 状態4:disabled */
.btn:disabled,
.btn[aria-disabled="true"] {
opacity: 0.45;
cursor: not-allowed;
pointer-events: none; /* hover と active を止める */
}
<button type="button" class="btn">送信する</button>
<a href="/tasks" class="btn">一覧へ</a>
<button type="button" class="btn" disabled>送信する</button>
変数の一覧
| 変数 | 役割 | 初期値 |
|---|---|---|
--btn-color |
主役の色。これ1つ変えれば、ホバー・押下・淡色・輪の色がすべて連動する | #2563eb |
--btn-on |
主役の色の上に載せる文字色 | #ffffff |
--btn-hover |
ホバー時の色(主役の色に黒を 15% 混ぜる) | 自動計算 |
--btn-active |
押している間の色(黒を 30% 混ぜる) | 自動計算 |
--btn-tint |
淡い背景色(白を 88% 混ぜる) | 自動計算 |
--btn-radius |
角の丸み | 8px |
--btn-font / --btn-py / --btn-px |
文字サイズ/上下の余白/左右の余白 | 1rem / 0.625em / 1.25em |
--btn-speed |
変化にかける時間 | 0.2s |
余白を em で書いているので、--btn-font を変えると余白も同じ比率で変わります。
サイズ・色・形を変える
/* ===== サイズと色 ===== */
.btn--sm { --btn-font: 0.875rem; --btn-py: 0.5em; --btn-px: 1em; }
.btn--lg { --btn-font: 1.125rem; --btn-py: 0.8em; --btn-px: 1.75em; }
.btn--block { display: flex; width: 100%; }
.btn--green { --btn-color: #15803d; }
.btn--red { --btn-color: #b91c1c; }
.btn--dark { --btn-color: #1f2937; }
/* ===== 形 ===== */
.btn--square { --btn-radius: 0; }
.btn--pill { --btn-radius: 999px; }
<button class="btn btn--sm">小</button>
<button class="btn btn--lg btn--green btn--pill">大きい緑のピル</button>
<button class="btn" style="--btn-color: #7c3aed">その場で色を変える</button>
実際の大きさ(計算値)です。
| クラス | 文字サイズ | 余白(上下 左右) | 高さ |
|---|---|---|---|
btn btn--sm |
14px | 7px 14px | 44px(min-height が効く) |
btn |
16px | 10px 20px | 44px |
btn btn--lg |
18px | 14.4px 31.5px | 54.4px |
btn btn--icon-only |
16px | 0 | 44×44px |
btn btn--block(親の幅 300px) |
16px | 10px 20px | 300×44px |
4つの状態の計算値
基本の btn を操作して、状態ごとに変わった値だけを抜き出しました。
normal : bg=rgb(37, 99, 235); color=rgb(255, 255, 255); transform=none; outline=none; opacity=1
hover : bg=color(srgb 0.123333 0.33 0.783333)
active : bg=color(srgb 0.101569 0.271765 0.645098); transform=matrix(1, 0, 0, 1, 0, 1)
focus-visible : outline=solid 3px rgb(37, 99, 235) offset=3px
disabled : opacity=0.45 bg=rgb(37, 99, 235)(マウスを載せても変化なし)
color(srgb 0.123333 0.33 0.783333) は color-mix() の計算結果で、RGB に直すとおよそ rgb(31, 84, 200) です。active はおよそ rgb(26, 69, 165) になります。
| 状態 | 土台での工夫 | 理由 |
|---|---|---|
| hover | @media (hover: hover) で囲む |
タッチ端末でタップ後に色が残らないようにする |
| focus-visible | :focus ではなく :focus-visible を使う |
マウスでクリックしたときは輪を出さず、キーボード操作のときだけ出す |
| active | 色と 1px の移動の両方を変える | 押した手応えを、色でも動きでも伝える |
| disabled | pointer-events: none を付ける |
hover と active のスタイルが当たらなくなり、デザインごとに打ち消しを書かなくて済む |
マウスでクリックしたあとの計算値は :focus-visible=false / outline=none でした。outline: none でフォーカス表示を消すのではなく、:focus-visible に任せると、マウス操作の見た目とキーボード操作のわかりやすさを両立できます。
ボタンデザイン集(コピペ用)
ここからは、土台に足すクラスです。必要なものだけ貼って使えます。
枠線・ゴースト・淡色(No.2〜4)
/* ===== 2. 枠線 ===== */
.btn--outline { background-color: transparent; color: var(--btn-color); }
@media (hover: hover) {
.btn--outline:hover { background-color: var(--btn-color); border-color: var(--btn-color); color: var(--btn-on); }
}
.btn--outline:active { background-color: var(--btn-active); color: var(--btn-on); }
/* ===== 3. 文字だけ(ゴースト) ===== */
.btn--ghost { background-color: transparent; border-color: transparent; color: var(--btn-color); }
@media (hover: hover) {
.btn--ghost:hover { background-color: var(--btn-tint); border-color: var(--btn-tint); }
}
.btn--ghost:active { background-color: color-mix(in srgb, var(--btn-color), #fff 75%); border-color: transparent; }
/* ===== 4. 淡色 ===== */
.btn--soft { background-color: var(--btn-tint); border-color: var(--btn-tint); color: var(--btn-active); }
@media (hover: hover) {
.btn--soft:hover { background-color: color-mix(in srgb, var(--btn-color), #fff 78%); border-color: color-mix(in srgb, var(--btn-color), #fff 78%); }
}
.btn--soft:active { background-color: color-mix(in srgb, var(--btn-color), #fff 68%); border-color: transparent; }
<button class="btn btn--outline">戻る</button>
<button class="btn btn--ghost">キャンセル</button>
<button class="btn btn--soft">下書き保存</button>
| デザイン | 通常 | ホバー | 向いている場所 |
|---|---|---|---|
| 塗り | 背景が主役の色 | 15% 暗く | いちばん押してほしいボタン(画面に1つ) |
| 枠線 | 背景は透明、文字と枠が主役の色 | 塗りに変わる | 2番目に大事な操作 |
| ゴースト | 文字だけ | 淡い背景 | キャンセル、補助的な操作 |
| 淡色 | 淡い背景に濃い文字 | 背景が少し濃く | 塗りほど目立たせたくない操作 |
1つの画面で「塗り」は1つにして、ほかを枠線やゴーストにすると、どれを押せばよいかが伝わりやすくなります。
グラデーション(No.7〜8)
/* ===== 5. グラデーション(色が流れる) ===== */
.btn--grad {
border: 0;
background-image: linear-gradient(90deg, #2563eb, #7c3aed, #2563eb);
background-size: 200% 100%;
background-position: 0% 0;
transition: background-position 0.4s, transform var(--btn-speed);
}
@media (hover: hover) { .btn--grad:hover { background-position: 100% 0; } }
.btn--grad:active { background-position: 100% 0; }
/* ===== 6. グラデーションの枠線 ===== */
.btn--grad-border {
border-color: transparent;
color: #1f2937;
background: linear-gradient(#fff, #fff) padding-box,
linear-gradient(90deg, #2563eb, #db2777) border-box;
}
@media (hover: hover) {
.btn--grad-border:hover { color: #fff; border-color: transparent;
background: linear-gradient(90deg, #2563eb, #db2777) padding-box, linear-gradient(90deg, #2563eb, #db2777) border-box; }
}
.btn--grad-border:active { border-color: transparent; color: #fff;
background: linear-gradient(90deg, #1e40af, #9d174d) padding-box, linear-gradient(90deg, #1e40af, #9d174d) border-box; }
<button class="btn btn--grad">はじめる</button>
<button class="btn btn--grad-border">詳しく見る</button>
グラデーションそのものは transition で変化させられないため、背景を横幅の2倍にしておき、background-position を動かして色が流れるように見せています(計算値は通常 0% 0px、ホバー 100% 0px)。枠線のグラデーションは、padding-box までの白い背景と border-box までのグラデーション背景を重ね、透明な枠線の下にグラデーションを見せる書き方です。グラデーションの色の作り方は、CSS グラデーションの記事で詳しく扱っています。
立体・影・ポップ(No.9〜11)
/* ===== 7. 立体(押すと沈む) ===== */
.btn--3d { box-shadow: 0 4px 0 var(--btn-active); }
@media (hover: hover) { .btn--3d:hover { transform: translateY(-1px); box-shadow: 0 5px 0 var(--btn-active); } }
.btn--3d:active { background-color: var(--btn-hover); border-color: var(--btn-hover); transform: translateY(4px); box-shadow: 0 0 0 var(--btn-active); }
/* ===== 8. 影つき(浮き上がる) ===== */
.btn--shadow { box-shadow: 0 2px 6px rgb(0 0 0 / 0.2); }
@media (hover: hover) { .btn--shadow:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgb(0 0 0 / 0.25); } }
.btn--shadow:active { transform: translateY(0); box-shadow: 0 1px 2px rgb(0 0 0 / 0.2); }
/* ===== 9. ぼかさない影(ポップ) ===== */
.btn--pop { background-color: #fff; color: #1f2937; border-color: #1f2937; box-shadow: 4px 4px 0 #1f2937; }
@media (hover: hover) { .btn--pop:hover { background-color: #fff; border-color: #1f2937; transform: translate(2px, 2px); box-shadow: 2px 2px 0 #1f2937; } }
.btn--pop:active { background-color: #f3f4f6; border-color: #1f2937; transform: translate(4px, 4px); box-shadow: 0 0 0 #1f2937; }
.btn--pop:focus-visible { outline-color: #1f2937; }
<button class="btn btn--3d">押せる</button>
<button class="btn btn--shadow">浮き上がる</button>
<button class="btn btn--pop">ポップ</button>
状態ごとの計算値です。
| デザイン | 通常 | ホバー | 押している間 |
|---|---|---|---|
| 立体 | 影 0 4px 0、移動なし |
影 0 5px 0、上へ 1px |
影 0 0 0、下へ 4px |
| 影つき | 影 0 2px 6px |
影 0 8px 20px、上へ 2px |
影 0 1px 2px、移動なし |
| ポップ | 影 4px 4px 0 |
影 2px 2px 0、右下へ 2px |
影 0 0 0、右下へ 4px |
立体とポップは「影の長さを減らしたぶんだけ、ボタンを同じ方向に動かす」のがコツです。 影の先端の位置が変わらないので、ボタンが台に押し込まれたように見えます。
光る・輪が広がる・大きくなる(No.12〜14)
/* ===== 10. 光る(グロー) ===== */
@media (hover: hover) { .btn--glow:hover { box-shadow: 0 0 18px color-mix(in srgb, var(--btn-color), transparent 35%); } }
/* ===== 11. 輪が広がる ===== */
@media (hover: hover) { .btn--ring:hover { box-shadow: 0 0 0 5px color-mix(in srgb, var(--btn-color), transparent 70%); } }
/* ===== 12. 少し大きくなる ===== */
@media (hover: hover) { .btn--scale:hover { transform: scale(1.05); } }
.btn--scale:active { transform: scale(0.97); }
<button class="btn btn--glow">光る</button>
<button class="btn btn--ring">輪が広がる</button>
<button class="btn btn--scale">大きくなる</button>
color-mix(in srgb, var(--btn-color), transparent 70%) は、主役の色を半透明にする書き方です。--btn-color を変えれば、光や輪の色も一緒に変わります。拡大は width や padding ではなく transform: scale() で行うと、まわりの要素を押しのけません。
矢印が動く・矢印が出てくる(No.15〜16)
/* ===== 13. 矢印が動く ===== */
.btn--arrow::after { content: "→"; transition: transform var(--btn-speed); }
@media (hover: hover) { .btn--arrow:hover::after { transform: translateX(4px); } }
/* ===== 14. 矢印が出てくる ===== */
.btn--arrow-in::after {
content: "→";
margin-left: -1.5em; /* gap と文字幅のぶんを打ち消して隠す */
opacity: 0;
transform: translateX(-6px);
transition: margin-left var(--btn-speed), opacity var(--btn-speed), transform var(--btn-speed);
}
@media (hover: hover) { .btn--arrow-in:hover::after { margin-left: 0; opacity: 1; transform: translateX(0); } }
.btn--arrow-in:focus-visible::after { margin-left: 0; opacity: 1; transform: translateX(0); }
<a href="/tasks" class="btn btn--arrow">一覧を見る</a>
<a href="/next" class="btn btn--arrow-in">次へ</a>
arrow 通常 ::after transform=none
arrow ホバー ::after transform=matrix(1, 0, 0, 1, 4, 0)
arrow-in 通常 ::after opacity=0 margin-left=-24px
arrow-in ホバー ::after opacity=1 margin-left=0px
arrow-in Tab キーでフォーカス ::after opacity=1
矢印は疑似要素(::after)で出しているので、HTML に余分な要素は要りません。「矢印が出てくる」は、ホバーだけでなく focus-visible でも矢印を出して、キーボード操作でも同じ見た目になるようにしています。
背景が流れる・光が走る(No.17〜20)
/* ===== 15〜17. 背景が流れる(左から/下から/中央から) ===== */
.btn--slide, .btn--rise, .btn--center {
isolation: isolate; /* ::before を背面に置くための重なりの基準 */
overflow: hidden;
background-color: transparent;
color: var(--btn-color);
}
.btn--slide::before, .btn--rise::before, .btn--center::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
background-color: var(--btn-color);
transition: transform 0.3s ease;
}
.btn--slide::before { transform: scaleX(0); transform-origin: left; }
.btn--rise::before { transform: scaleY(0); transform-origin: bottom; }
.btn--center::before { transform: scaleX(0); transform-origin: center; }
@media (hover: hover) {
.btn--slide:hover, .btn--rise:hover, .btn--center:hover { background-color: transparent; border-color: var(--btn-color); color: var(--btn-on); }
.btn--slide:hover::before, .btn--center:hover::before { transform: scaleX(1); }
.btn--rise:hover::before { transform: scaleY(1); }
}
.btn--slide:active, .btn--rise:active, .btn--center:active { background-color: var(--btn-active); color: var(--btn-on); }
/* ===== 18. 光が走る ===== */
.btn--shine { overflow: hidden; }
.btn--shine::before {
content: "";
position: absolute;
top: 0;
left: -75%;
width: 50%;
height: 100%;
background: linear-gradient(100deg, transparent, rgb(255 255 255 / 0.5), transparent);
transform: skewX(-20deg);
}
@media (hover: hover) { .btn--shine:hover::before { left: 125%; transition: left 0.6s ease; } }
<button class="btn btn--slide">左から</button>
<button class="btn btn--rise">下から</button>
<button class="btn btn--center">中央から</button>
<button class="btn btn--shine">光が走る</button>
slide 通常 ::before transform=matrix(0, 0, 0, 1, 0, 0) ホバー ::before transform=matrix(1, 0, 0, 1, 0, 0)
rise 通常 ::before transform=matrix(1, 0, 0, 0, 0, 0) ホバー ::before transform=matrix(1, 0, 0, 1, 0, 0)
shine 通常 ::before left=-78.5625px ホバー ::before left=130.938px
しくみは3つとも同じで、ボタンと同じ大きさの色つきの疑似要素を、幅または高さ 0 倍に縮めておき、ホバーで 1 倍に戻します。 違いは transform-origin(どこを起点に伸びるか)だけです。
| 押さえる点 | 書き方 | 理由 |
|---|---|---|
| 疑似要素を文字の後ろに置く | ボタンに isolation: isolate、疑似要素に z-index: -1 |
ボタンの中で重なりの基準を作り、文字より後ろ・ボタンの背景より前に置く |
| はみ出しを切る | overflow: hidden |
角丸の外に色が出ないようにする |
幅ではなく transform で伸ばす |
scaleX(0) から scaleX(1) |
レイアウトの再計算が起きず、動きがなめらか |
重なり順のしくみは z-index の記事で、transition と keyframes の使い分けは CSS アニメーションの記事で扱っています。
下線が伸びる(No.21)
/* ===== 19. 下線が伸びる(テキストボタン) ===== */
.btn--underline {
background-color: transparent;
border-color: transparent;
color: var(--btn-color);
--btn-px: 0.25em;
}
.btn--underline::after {
content: "";
position: absolute;
left: var(--btn-px);
right: var(--btn-px);
bottom: 8px;
height: 2px;
background-color: currentColor;
transform: scaleX(0);
transform-origin: left;
transition: transform 0.3s ease;
}
@media (hover: hover) {
.btn--underline:hover { background-color: transparent; border-color: transparent; }
.btn--underline:hover::after { transform: scaleX(1); }
}
.btn--underline:focus-visible::after { transform: scaleX(1); }
.btn--underline:active { background-color: transparent; border-color: transparent; color: var(--btn-active); }
<a href="/about" class="btn btn--underline">詳しく見る</a>
文字だけのリンク風ボタンです。見た目は文字だけでも、土台の min-height: 44px が効いているので、タップできる高さは確保されています。
アイコン付き・アイコンだけ・読み込み中(No.22〜24)
/* ===== 20. アイコン付き/21. アイコンだけ ===== */
.btn svg { width: 1.25em; height: 1.25em; flex-shrink: 0; fill: currentColor; }
.btn--icon-only { padding: 0; width: 44px; height: 44px; }
/* ===== 22. 読み込み中 ===== */
.btn.is-loading { color: transparent; pointer-events: none; }
.btn.is-loading::after {
content: "";
position: absolute;
width: 1.25em;
height: 1.25em;
border: 2px solid var(--btn-on);
border-right-color: transparent;
border-radius: 50%;
animation: btn-spin 0.7s linear infinite;
}
@keyframes btn-spin { to { transform: rotate(360deg); } }
<!-- アイコン付き:アイコンは飾りなので aria-hidden -->
<button class="btn">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 4a6 6 0 104.5 10l5 5 1.4-1.4-5-5A6 6 0 0010 4z"/></svg>
検索
</button>
<!-- アイコンだけ:文字がないので aria-label で名前を付ける -->
<button class="btn btn--icon-only" aria-label="検索">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 4a6 6 0 104.5 10l5 5 1.4-1.4-5-5A6 6 0 0010 4z"/></svg>
</button>
<!-- 読み込み中 -->
<button class="btn is-loading" aria-busy="true">送信する</button>
| 項目 | 計算値 |
|---|---|
| アイコンだけのボタンの大きさ | 44×44px |
| 読み込み中のボタンの幅 | 108.8px(通常時と同じ) |
| 読み込み中の文字色 | rgba(0, 0, 0, 0)(透明) |
| 回転する輪 | animation: btn-spin 0.7s infinite |
読み込み中は、文字を消すのではなく透明にしています。 文字が幅を保ったままなので、ボタンの大きさが変わらず、まわりのレイアウトが動きません。is-loading はマウスとタッチの操作を止めますが、キーボードの Enter は届くので、送信の二重実行を防ぐには JavaScript 側でも処理中の再実行を止めておきます。
スマホでホバーが残る問題と対処
タッチ端末には「マウスを載せる」操作がないため、ブラウザはタップしたときに hover の状態を当て、ほかの場所をタップするまでそのままにします。 その結果、押し終わったボタンの色が変わったまま残ります。
@media で囲まない素の :hover(赤に変わる)と、この記事の土台のボタンを、タッチ端末の設定でタップして比べました。
matchMedia: (hover: hover)=false (hover: none)=true (pointer: coarse)=true
タップ後 @media なしのボタン : 背景 rgb(200, 0, 0) ← ホバーの色のまま
タップ後 @media ありのボタン : 背景 rgb(37, 99, 235) ← 元の色
別の場所をタップしたあとの @media なしのボタン : 背景 rgb(37, 99, 235)
対処は、hover のスタイルを @media (hover: hover) の中に書くことです。
/* 直す前:タッチ端末でタップ後も色が残る */
.btn:hover { background-color: var(--btn-hover); }
/* 直した後:ホバーできる端末だけに当てる */
@media (hover: hover) {
.btn:hover { background-color: var(--btn-hover); }
}
| メディア特性 | 意味 | マウスのある PC | スマートフォン |
|---|---|---|---|
(hover: hover) |
主な入力手段でホバーできる | 当てはまる | 当てはまらない |
(hover: none) |
主な入力手段でホバーできない | 当てはまらない | 当てはまる |
(pointer: coarse) |
主な入力手段の精度が粗い(指) | 当てはまらない | 当てはまる |
(any-hover: hover) |
つながっている入力手段のどれかでホバーできる | 当てはまる | マウスをつないでいれば当てはまる |
タッチ端末では hover の代わりに active が「押した手応え」を伝える役目をします。 そのため、土台では active を @media の外に書き、色と 1px の移動の両方を変えています。背景が流れるボタン(No.17〜19)も、active で背景色を直接塗る指定を入れてあり、タップした瞬間に色が変わります。
動きを減らす設定(prefers-reduced-motion)への対応
OS の設定で「視差効果を減らす」「アニメーションを減らす」を選んでいる人には、動きを控えた表示を返します。
/* ===== 動きを減らす設定への対応 ===== */
@media (prefers-reduced-motion: reduce) {
.btn, .btn::before, .btn::after { transition-duration: 0.01ms !important; }
.btn:hover, .btn:active { transform: none; }
.btn.is-loading::after { animation-duration: 1.5s; }
}
設定を再現して計算値を取りました。
reduced: transition-duration = 1e-05s(0.01ms)
reduced: btn--scale をホバーしたときの transform = none
reduced: 読み込み中の輪の animation-duration = 1.5s
| 対象 | 動きを減らす設定のとき | 理由 |
|---|---|---|
| 色の変化 | 一瞬で切り替わる | 色が変わること自体は、状態を伝えるために残す |
| 拡大・移動 | 行わない | 位置や大きさが動く表現を控える |
| 読み込み中の輪 | ゆっくり回す | 処理中であることを伝える役目があるので、止めずに速度を落とす |
transition: none ではなく 0.01ms にしているのは、transition の終了を待つ JavaScript(transitionend)がある場合にも、イベントが発生するようにするためです。
aタグとbuttonの使い分け
見た目が同じでも、「別のページへ移動する」なら a、「その場で何かを実行する」なら button です。キーボードでの動きが違うことを確かめました。
各要素に Tab キーで移動し、Enter と Space を1回ずつ押して、click イベントが何回起きたかを数えています。
| 要素 | Tab で止まるか | Enter | Space | click の回数 |
|---|---|---|---|---|
<button type="button"> |
止まる | 実行される | 実行される | 2 |
<a href="..."> |
止まる | 実行される | 実行されない | 1 |
<a>(href なし) |
止まらない | なし | なし | 0 |
<div class="btn"> |
止まらない | なし | なし | 0 |
<button disabled> |
止まらない | なし | なし | 0 |
<a href="..." aria-disabled="true"> |
止まる | 実行される | 実行されない | 1 |
| やりたいこと | 使う要素 |
|---|---|
| 別のページ・別の場所へ移動する | <a href="..."> |
| フォームを送信する | <button type="submit"> |
| 開閉、保存、削除、モーダルを開く | <button type="button"> |
| 新しいタブで開けるようにしたい | <a href="...">(button では開けない) |
div や href のない a は、Tab キーで止まらず、キーボードでは押せません。 見た目だけボタンにするのではなく、役割に合った要素を選ぶと、フォーカスもキー操作も最初から付いてきます。フォームの中の button は type を省くと送信ボタンになるので、送信以外の用途では type="button" を書いておきます。
リンクを押せない状態にするとき
表の最後の行に注目してください。aria-disabled="true" と pointer-events: none を付けたリンクは、マウスでは押せなくなりましたが、Tab キーでフォーカスでき、Enter で移動しました(location.hash が切り替わることを確認)。pointer-events はマウスとタッチだけを止めるためです。
リンクを押せない状態にするときは、href を外します。
<!-- 押せるとき -->
<a href="/next" class="btn">次へ</a>
<!-- 押せないとき:href を外すと、Tab でも止まらず Enter でも移動しない -->
<a class="btn" role="link" aria-disabled="true">次へ</a>
href を外した a は、確認した環境で Tab キーの移動先に入らず、click も起きませんでした。
aとbuttonで高さがそろわないとき
土台に box-sizing: border-box を入れているのは、次の違いを吸収するためです。
| 要素 | box-sizing の初期値 |
min-height: 44px を付けたときの高さ(余白 10px+枠 2px) |
|---|---|---|
button |
border-box(ブラウザの既定スタイル) |
44px |
a |
content-box |
68px(44 + 余白 20 + 枠 4) |
a + box-sizing: border-box |
border-box |
44px |
box-sizing を書かずに測ったところ、同じクラスを付けた a だけ高さが 68px になりました。a と button を並べて使う土台では、box-sizing: border-box を明示しておくと高さがそろいます。
タップ領域44pxとコントラスト
タップ領域
指で押すボタンは、見た目より「押せる範囲」の大きさが大切です。
| 基準 | 大きさ |
|---|---|
| WCAG 2.2 達成基準 2.5.8(レベル AA) | 24×24 CSS px 以上 |
| WCAG 2.2 達成基準 2.5.5(レベル AAA) | 44×44 CSS px 以上 |
| この記事の土台 | min-height: 44px; min-width: 44px; |
余白と文字サイズだけで作った小さなボタン(padding: 4px 8px; font-size: 12px)は、実測で 32×29px でした。土台の btn--sm は、文字サイズが 14px でも高さ 44px を保っています。文字サイズを小さくしても、min-height で押せる範囲は小さくしない、という考え方です。見た目を小さくしたいテキストリンク風のボタンも、高さだけは確保しておくと押しやすくなります。
コントラスト比
文字と背景のコントラスト比は、通常の大きさの文字で 4.5:1 以上(WCAG 2.2 達成基準 1.4.3)、ボタンの枠線など部品の輪郭は 3:1 以上(1.4.11)が目安です。--btn-color に入れる色ごとに、白文字との比を計算しました。
--btn-color |
白文字との比(通常) | 白文字との比(ホバー) | 白い背景の上の枠線・文字(枠線ボタン) | 4.5:1 |
|---|---|---|---|---|
#2563eb(青) |
5.17 | 6.64 | 5.17 | 満たす |
#15803d(緑) |
5.02 | 6.44 | 5.02 | 満たす |
#b91c1c(赤) |
6.47 | 8.15 | 6.47 | 満たす |
#1f2937(濃いグレー) |
14.68 | 15.84 | 14.68 | 満たす |
#3b82f6(明るい青) |
3.68 | 4.85 | 3.68 | 届かない |
#22c55e(明るい緑) |
2.28 | 3.14 | 2.28 | 届かない |
#f59e0b(オレンジ) |
2.15 | 2.96 | 2.15 | 届かない |
明るい色を主役にしたいときは、文字色のほうを変えます。 --btn-on: #1f2937; と濃い文字にすると、明るい緑やオレンジでも比を確保できます。
.btn--amber { --btn-color: #f59e0b; --btn-on: #1f2937; }
disabled のボタンは、透明度 0.45 にした結果、白文字との比が 1.96 になります。押せない部品はコントラストの基準の対象外とされていますが、なぜ押せないのかが文字で読める場所(ボタンの近くの説明文など)を用意しておくと、利用者が次に何をすればよいか迷いません。
仕上げのチェックリスト
| 確認すること | 見るところ |
|---|---|
| Tab キーでフォーカスの輪が見える | :focus-visible の outline |
| マウスを載せたときと押したときで見た目が違う | hover と active |
| スマートフォンでタップ後に色が残らない | @media (hover: hover) |
| 押せないボタンにマウスを載せても変化しない | disabled の pointer-events: none |
| 高さが 44px 以上ある | min-height |
| 文字と背景の比が 4.5:1 以上 | --btn-color と --btn-on |
| 移動は a、実行は button になっている | HTML の要素 |
| 動きを減らす設定で拡大・移動が止まる | prefers-reduced-motion |
よくある質問
Q. ホバーアニメーションがスマホで残ってしまいます。どう直しますか?
A. hover のスタイルを @media (hover: hover) { ... } の中に移します。タッチ端末では (hover: hover) が当てはまらないので、タップ後に色が残らなくなります。押した手応えは :active で付けます。
Q. outline: none でフォーカスの枠を消してもよいですか?
A. 消すと、キーボードで操作している人が今どこにいるかわからなくなります。:focus-visible を使えば、マウスでクリックしたときは輪が出ず、Tab キーで移動したときだけ輪が出ます。この記事の土台はその形になっています。
Q. ボタンの色を変えるには、どこを直せばよいですか?
A. --btn-color を変えるだけです。ホバー・押下・淡色・輪の色は color-mix() で自動計算されます。クラスで .btn--green { --btn-color: #15803d; } と用意するか、style="--btn-color: #7c3aed" とその場で指定します。明るい色のときは --btn-on も濃い色にします。
Q. a タグに disabled は使えますか?
A. a に disabled 属性はありません。aria-disabled="true" と pointer-events: none だけでは、キーボードの Enter で移動できることを確認しました。押せない状態にするときは href を外し、role="link" aria-disabled="true" を付けます。
まとめ
- 土台のクラスを1つ作り、色・サイズを CSS 変数にする。
--btn-colorを変えるだけで、ホバーや押下の色まで連動する - hover・focus-visible・active・disabled の4状態を土台でそろえる。 デザインを増やしても操作感が変わらない
- hover は
@media (hover: hover)で囲む。 タッチ端末でタップ後に色が残らなくなる - フォーカスの輪は
:focus-visibleで出す。outline: noneで消さない - 背景が流れる・下線が伸びる動きは、疑似要素を
transform: scaleで伸ばす。 違いはtransform-originだけ - 移動は a、実行は button。 並べて使うなら
box-sizing: border-boxで高さをそろえる - 高さは 44px 以上、文字と背景の比は 4.5:1 以上。 明るい色のボタンは文字色を濃くする
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「CSSボタンデザインのコピペ集|ホバーアニメーションと4つの状態」
株式会社RJCでは、CSSのボタンデザインを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。