結論
プロは状態を「色をなんとなく変える」では作らない。状態ごとに不透明度・トランジション時間・コントラストを数値で固定し(Material Design 3 は hover 8% / focus 10% / pressed 10% / dragged 16%、disabled は content 38% / container 12%)、focus は :focus-visible でキーボード時だけ 2px 厚・隣接色 3:1 のリングを出し、押下は 34–150ms で即応・戻りはゆっくりと非対称に動かす。disabled は色だけでなく cursor や aria でも伝える——この4点を定量化するだけで触り心地は一段プロになる。
01 — 状態は色ではなく「不透明度レイヤー」で作る
状態差を background の色いじりで作ると、色覚特性や低視力のユーザーに伝わらず、値も場当たりになる。プロは on-color(白)を半透明オーバーレイとして重ねる state layer 方式で、hover 8% / pressed 10% / focus 10% / dragged 16% と比率を固定する。1レイヤーで全状態を一貫管理でき、design token 化すれば全ボタンで揃う。
8%
10%
16%
primaryStates (State layers) — Material Design 3
02 — focus は :focus-visible でゲートし、消すなら代わりを置く
最も典型的な「雑」サインが button:focus{outline:none} の裸書きだ。代替なしでアウトラインを消すとキーボード操作者が現在地を見失い、WCAG 2.4.7 違反になる。上書きした瞬間、3:1 確保の義務が自分に移る。正解は :focus-visible でキーボード時のみリングを出し、マウスクリック時の残留リングは :focus:not(:focus-visible) で抑える。
:focus-visible の挙動)。primary:focus-visible — MDN
blogWCAG 2.4.11 Focus Appearance Minimum — TestParty
03 — focus リングは「2px 厚相当・隣接色 3:1」を満たす
リングを出してもペラペラの 1px・低コントラストでは低視力ユーザーに見えない。WCAG 2.4.13 は焦点表示に「2px 厚の周長と同等以上の面積」かつ focused/unfocused 間 3:1 を要求する。solid なら 2px、dashed/dotted は約 4px、内側 outline は 3px 以上。色だけでなく stroke/outline の太さで示し、比率は四捨五入せず計算する(2.999:1 は不合格)。任意背景に効かせるには outline + box-shadow の二色リングが堅い。
primaryUnderstanding SC 2.4.13: Focus Appearance — W3C
blogDesigning accessible WCAG-conformant focus indicators — Sara Soueidan
04 — トランジションは非対称に。押下は即応、戻りはゆっくり
transition:all .3s で全状態をまとめて処理すると、押下が鈍く感じてユーザーが連打する。プロは時間を状態ごとに分ける——押下フィードバックは 34–150ms で即応、hover は 250ms 前後、戻り(equilibrium)は 600ms とゆっくり。Josh Comeau の 3D ボタンは active 34ms(60fps で約2フレーム)/ hover 250ms / 戻り 600ms と非対称に設計し、これが「押した感」を生む。
blogBuilding a Magical 3D Button — Josh W. Comeau
secondaryButton States: Communicate Interaction — NN/g
05 — disabled は色以外でも伝え、薄くしすぎない
disabled は 3:1 コントラスト義務から免除されるので低 opacity で構わない——が、opacity:.25 でボタンごと消すのは雑だ。文面が読めなければ何のボタンか分からない。Material は content 38% / container 12% で「読めるが押せない」を担保する。さらに cursor:not-allowed と、必要なら aria-disabled="true"(タブ順を保ちつつ SR に無効と読ませる)で非色チャネルも併用する。
blogDesigning button states: best practices — LogRocket
primaryUnderstanding SC 1.4.11: Non-text Contrast — W3C WAI
06 — hover を唯一の信号にしない(タッチ/キーボード対策)
hover だけで状態を伝えると、タッチ端末は default→active へ直行して hover を飛ばし、キーボードにもそもそも hover が無い。hover 単独依存はモバイルとキーボードで状態が消える。hover はあくまで一信号に留め、active と focus を独立して必ず用意する。下の左は hover でしか色が変わらず、タップでは何も起きない例。右は hover も active も同じ state layer で重畳されるので、タッチでも押下が伝わる。
primaryStates — Material Design 3
blogButton States Explained: Complete Design Guide 2026 — UXPin
実装スニペット
状態レイヤー方式の基本ボタン(MD3 比率)。色を直接いじらず1レイヤーで全状態を管理する。
.btn{
position:relative; isolation:isolate;
--on:255 255 255; /* on-color RGB */
background:#6750a4; color:#fff;
border:none; border-radius:8px; padding:12px 20px;
cursor:pointer;
transition:background-color 120ms ease;
}
.btn::after{ /* state layer overlay */
content:""; position:absolute; inset:0; z-index:-1;
border-radius:inherit;
background:rgb(var(--on)); opacity:0;
transition:opacity 120ms ease;
}
.btn:hover::after{opacity:.08} /* hover 8% */
.btn:focus-visible::after{opacity:.10}/* focus 10% */
.btn:active::after{opacity:.10} /* pressed 10% */
WCAG 準拠の堅牢な focus リング(任意背景・強制カラー対応)。
.btn:focus-visible{
outline:3px solid #000;
outline-offset:2px;
box-shadow:0 0 0 6px #fff; /* 二色リングで任意背景に効く */
}
/* マウスクリック時はリングを出さない */
.btn:focus:not(:focus-visible){outline:none}
@media (forced-colors: active){
.btn:focus-visible{outline-color:CanvasText}
}
disabled の正しい伝達(色以外の信号も付与)。
.btn:disabled,
.btn[aria-disabled="true"]{
background:rgba(0,0,0,.12); /* container 12% */
color:rgba(0,0,0,.38); /* content 38% */
cursor:not-allowed;
box-shadow:none;
}
/* aria-disabled はクリックだけ無効化しフォーカスは維持 */
.btn[aria-disabled="true"]{pointer-events:none}
反応の良い押下感(非対称トランジション・CTA 向け)。
.btn{
transition:transform 600ms cubic-bezier(.3,.7,.4,1); /* 戻りはゆっくり */
-webkit-tap-highlight-color:transparent;
}
.btn:hover{
transform:translateY(-6px);
transition-duration:250ms;
transition-timing-function:cubic-bezier(.3,.7,.4,1.5); /* springy */
}
.btn:active{
transform:translateY(-2px);
transition-duration:34ms; /* 押下は即応 */
}
チェックリスト
- default / hover / active / focus / disabled の5状態すべてを定義したか(hover 単独依存になっていないか)
- 状態差を色だけで作っていないか(hover=オーバーレイ、focus=outline、disabled=cursor+opacity など非色チャネルを併用)
- `outline:none` を書いたなら、必ず `:focus-visible` の代替リングをセットにしたか
- focus リングは 2px 厚相当(solid 2px / dashed 4px / inner 3px)かつ隣接色 3:1 を満たすか(四捨五入せず計算)
- `transition:all .3s` で一括していないか。押下 34–150ms / 戻りはゆっくり、と非対称になっているか
- ラベル文字は default だけでなく暗転 hover / pressed でも 4.5:1(AA)を保つか
- disabled のテキストは判読可能か(content 38% 目安、消えるほど薄くしていないか)
- SR に無効を伝えたいなら `aria-disabled="true"`、タブ順から外したいなら native `disabled` と使い分けたか
- タッチ端末のヒットエリアは 44×44px 以上あるか
限界 / 出典
aria-disabled と native disabled は挙動が異なり(前者はフォーカス維持+クリック無効化を別途実装、後者はタブ順から除外)、用途で使い分ける——どちらが正解という単純な話ではない。focus 免除 / hover 免除は「ブラウザ既定 outline を使う前提」の話で、見た目を上書きした瞬間に 3:1 義務が作者へ移る。SC 2.4.13 は AAA のため法的必須でない案件が多い一方、上書きする限り AA の 2.4.7 / 1.4.11 は実質必須。各値は 2026年6月時点の各ソース記載に基づく。primaryStates (State layers) — Material Design 3
primaryStates — Material Design 3
primaryUnderstanding SC 2.4.13: Focus Appearance — W3C
primaryUnderstanding SC 1.4.11: Non-text Contrast — W3C WAI
primary:focus-visible — MDN
blogDesigning accessible WCAG-conformant focus indicators — Sara Soueidan
blogBuilding a Magical 3D Button — Josh W. Comeau
secondaryButton States: Communicate Interaction — NN/g
blogDesigning button states: best practices — LogRocket
blogButton States Explained: Complete Design Guide 2026 — UXPin
blogHow Do You Meet WCAG 2.4.11 Focus Appearance Minimum — TestParty