button-states について
ボタンなどのインタラクティブ要素に明確な視覚状態を定義するデザインスキルです。6つの状態、色の導出、キーボードフォーカス、無効状態、非同期処理中のフィードバックを扱います。
主な機能
- 016つのインタラクション状態
- 02状態色の算出指針
- 03視認できるキーボードフォーカス
- 04無効状態の動作
- 05読み込み中のフィードバックと再送防止
活用シーン
- 01デザインシステムでのボタン状態の定義
- 02キーボードフォーカスの視認性確認
- 03非同期処理のフィードバック設計
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dembrandt/ dembrandt-skills --skill button-statesインストール後、Claude Code で/button-statesを入力して読み込まれたか確認します。
詳細
ボタンとインタラクティブ要素の状態
すべてのインタラクティブコンポーネントには、各操作モードに対応する、視覚的に明確な状態が必要です。状態が欠けていたり曖昧だったりすると、UIが未完成に感じられ、ユーザーの信頼感が損なわれます。
6つの状態
| 状態 | トリガー | 視覚的な表示 |
|---|---|---|
| 通常 | デフォルト | 基本色、カーソル:pointer |
| ホバー | マウスオーバー | やや暗くなり、背景がわずかに変化 |
| アクティブ/押下中 | マウスダウン/タップ | はっきり暗くなり、わずかに縮小 |
| フォーカス | キーボードナビゲーション | 視認できるフォーカスリング、塗りつぶし色は変化しない |
| 無効 | 使用不可 | コントラストが低く、カーソル:not-allowed、操作不可 |
| 読み込み中 | 非同期処理の実行中 | スピナーまたはパルス表示、操作不可 |
アルゴリズムによる状態色の導出
状態色は個別に選ぶのではなく、HSLで明度を調整して基本色から導きます。これにより、パレット全体に一貫性が保たれます。
base: hsl(H, S%, L%)
hover: hsl(H, S%, L% - 8%) ← darken 8%
active: hsl(H, S%, L% - 14%) ← darken 14%
例:プライマリボタン #635BFF(hsl 243, 100%, 68%)
.btn-primary {
background: hsl(243, 100%, 68%); /* rest #635BFF */
}
.btn-primary:hover {
background: hsl(243, 100%, 60%); /* hover #4A40FF */
}
.btn-primary:active {
background: hsl(243, 100%, 54%); /* active #3429FF */
}
暗い背景に明るいボタンを配置する場合は、ロジックを逆にして、ホバー時に暗くするのではなく明るくします。
セカンダリ/アウトラインボタン
.btn-secondary {
background: transparent;
border: 1px solid var(--color-border);
color: var(--color-text);
}
.btn-secondary:hover {
background: var(--color-grey-100); /* subtle fill */
border-color: var(--color-grey-300);
}
.btn-secondary:active {
background: var(--color-grey-200);
}
フォーカス状態
フォーカスはキーボードナビゲーションに必要です(WCAG 2.2)。視認できるようにし、ホバースタイルだけに頼ってはいけません。キーボードユーザーにはホバーが発生しないためです。
.btn:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 3px;
}
- フォーカスリングには
box-shadowではなくoutlineを使用します。最新のブラウザーではoutlineがborder-radiusを尊重し、レイアウトにも影響しません outline-offset: 2–4pxにすると、リングとコンポーネントの端との間に余白ができます- 代わりのフォーカススタイルを用意せずに
outline: noneを使用してはいけません
無効状態
.btn:disabled,
.btn[aria-disabled="true"] {
opacity: 0.4;
cursor: not-allowed;
pointer-events: none;
}
- 無効な要素はWCAGのコントラスト要件の対象外です。低い不透明度は適切であり、意図されたものです
- JavaScriptを回避された場合でもクリックイベントを防ぐため、
pointer-events: noneを使用します - 無効なボタンの形状やサイズは変えないでください。変えるのは色とカーソルだけです
読み込み中の状態
ボタンが非同期処理を開始したら、ラベルをスピナーに置き換え、再送信を防ぎます。
.btn--loading {
pointer-events: none;
opacity: 0.7;
}
読み込み中もカーソルは変わりません。 カーソルが示すのは操作可能性であり、進行状況ではありません。通常のコンテンツには矢印、操作可能な要素には pointer を使います。進行状況は、マウスがたまたま置かれている場所ではなく、ユーザーがすでに見ている要素と読み込み領域で示します。また、cursor: wait は pointer-events: none と併用しても機能しないため、このルールは意図的なものとして読める一方、実際の動作には影響しません。
- 読み込み中もボタンの幅を一定に保ちます。ラベルをスピナーに置き換える際にレイアウトがずれないようにします
- 完了時(成功またはエラー)には通常状態に戻します
- 処理に時間がかかる場合は、ステータスメッセージを併用します。スピナーだけではユーザーに何が起きているか伝わりません
アクティブ時の拡大縮小(任意)
押したときにわずかに縮小すると、ディズニーの「つぶし」の原則を取り入れた、物理的なフィードバックが加わります。
.btn:active {
transform: scale(0.97);
transition: transform 80ms ease-out;
}
スケール値は 0.95–0.98 の範囲にします。0.95 を下回ると、ボタンが壊れそうに感じられます。
ボタンCSSの完全なリファレンス
.btn {
cursor: pointer;
background: var(--color-primary);
color: white;
border-radius: var(--radius-button);
padding: var(--component-padding-y-md) var(--component-padding-x-md);
height: var(--component-height-md);
border: none;
transition: background 120ms ease-out, transform 80ms ease-out;
}
.btn:hover { background: var(--color-primary-hover); }
.btn:active { background: var(--color-primary-active); transform: scale(0.97); }
.btn:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 3px; }
.btn:disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; }
.btn.btn--loading { opacity: 0.7; pointer-events: none; }
レビューチェックリスト
- すべてのインタラクティブ要素に6つの状態が定義されていますか?
- ホバー色とアクティブ色は、任意に選ぶのではなく、明度の調整によって基本色から導かれていますか?
- フォーカス状態は視認でき、
outlineを使用していますか(削除されていませんか)? - 無効状態は不透明度が低く、
cursor: not-allowedが設定されていますか? - 読み込み中の状態では再送信が防止されていますか?
- トランジション時間は80~150msですか(瞬時でも遅すぎでもありませんか)?
- 通常状態のすべてのインタラクティブ要素に
cursor: pointerが設定されていますか? - 処理中にカーソルは変わらず、進行状況は要素自体で示されていますか?