メインコンテンツまでスキップ
  • Created:
  • Updated:
  • Author:
    Takeshi Takatsudo

ホバー・フォーカス・アクティブ状態

問題

インタラクティブな要素には、ホバー、フォーカス、アクティブ状態の視覚的フィードバックが必要です。AIエージェントは、タッチデバイスでスティッキーなホバー状態を引き起こす :hover スタイルを追加したり、:focus:focus-visible のスタイリングを省略してキーボードアクセシビリティを壊したり、本来は区別すべき3つの状態すべてに同一のスタイルを適用したりします。その結果、マウスでは動作するものの、タッチユーザーやキーボードナビゲーターにはストレスの多いインターフェースになります。

解決方法

各インタラクション状態を目的に応じてスタイリングし、@media (hover: hover) でホバーエフェクトを対応デバイスに限定し、キーボード専用のフォーカスインジケーターには :focus ではなく :focus-visible を使用しましょう。

3つの状態

  • :hover — ポインティングデバイス(マウス、トラックパッド)が要素の上にある状態です。タッチデバイスでは確実に動作しません。
  • :focus — マウスクリック、キーボードのタブ、プログラム的なフォーカスなど、何らかの方法で要素がフォーカスされた状態です。
  • :focus-visible — 要素がフォーカスされ、かつブラウザが視覚的インジケーターが適切と判断した状態(通常はキーボードナビゲーション)です。ボタンのマウスクリックでは :focus-visible はトリガーされません。
  • :active — 要素がアクティベートされている状態(マウスボタン押下中、タッチで指を押している状態)です。
ボタンの状態 — ホバーとTabで状態を確認

コード例

基本的なボタンの状態

.button {
background-color: var(--color-primary, #2563eb);
color: white;
border: 2px solid transparent;
padding: 0.625rem 1.25rem;
border-radius: 0.375rem;
cursor: pointer;
transition: background-color 0.15s ease, transform 0.1s ease;
}

/* Hover: only on devices that support it */
@media (hover: hover) {
.button:hover {
background-color: var(--color-primary-dark, #1d4ed8);
}
}

/* Focus-visible: keyboard focus indicator */
.button:focus-visible {
outline: 2px solid var(--color-primary, #2563eb);
outline-offset: 2px;
}

/* Active: pressed state */
.button:active {
transform: scale(0.97);
}

リンクの状態(LVHAの順序)

リンクの擬似クラスは、詳細度の競合を避けるためにLVHAの順序に従うべきです:

a:link {
color: var(--color-link, #2563eb);
text-decoration: underline;
text-underline-offset: 0.2em;
}

a:visited {
color: var(--color-link-visited, #7c3aed);
}

@media (hover: hover) {
a:hover {
color: var(--color-link-hover, #1d4ed8);
text-decoration-thickness: 2px;
}
}

a:focus-visible {
outline: 2px solid var(--color-link, #2563eb);
outline-offset: 2px;
border-radius: 2px;
}

a:active {
color: var(--color-link-active, #1e40af);
}

カードのホバーエフェクト(タッチセーフ)

.card {
background: var(--color-surface, #ffffff);
border-radius: 0.5rem;
border: 1px solid var(--color-border, #e5e7eb);
padding: 1.5rem;
transition: box-shadow 0.2s ease, transform 0.2s ease;
}

/* Only apply hover elevation on hover-capable devices */
@media (hover: hover) {
.card:hover {
box-shadow: 0 4px 16px rgb(0 0 0 / 0.1);
transform: translateY(-2px);
}
}

/* Keyboard focus */
.card:focus-visible {
outline: 2px solid var(--color-primary, #2563eb);
outline-offset: 2px;
}

Focus-Visible と Focus の違い

/* Remove default focus ring for mouse users */
.interactive:focus {
outline: none;
}

/* Show focus ring only for keyboard users */
.interactive:focus-visible {
outline: 2px solid var(--color-primary, #2563eb);
outline-offset: 2px;
}

:focus スタイルを完全に削除しない、より安全なアプローチもあります:

/* Visible focus ring for keyboard navigation */
.interactive:focus-visible {
outline: 2px solid var(--color-primary, #2563eb);
outline-offset: 2px;
}

/* Subtle focus style for mouse clicks (if desired) */
.interactive:focus:not(:focus-visible) {
outline: none;
}

タッチ入力とマウス入力の検出

/* Base interactive styles */
.nav-link {
padding: 0.5rem 1rem;
color: var(--color-text);
text-decoration: none;
}

/* Hover effects only for precise pointers */
@media (hover: hover) and (pointer: fine) {
.nav-link:hover {
background-color: var(--color-surface-hover, #f3f4f6);
}
}

/* Larger touch targets for coarse pointers */
@media (pointer: coarse) {
.nav-link {
min-height: 44px;
display: flex;
align-items: center;
padding: 0.75rem 1rem;
}
}

フォーム入力のフォーカス状態

.input {
border: 1px solid var(--color-border, #d1d5db);
border-radius: 0.375rem;
padding: 0.5rem 0.75rem;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

/* All focus (mouse and keyboard) gets a border change */
.input:focus {
border-color: var(--color-primary, #2563eb);
box-shadow: 0 0 0 3px rgb(37 99 235 / 0.15);
outline: none;
}

フォーム入力には :focus-visible ではなく :focus を使うのが通常正しい選択です。ユーザーはフォーカスの方法に関係なく、どの入力フィールドに入力しているかを確認する必要があるためです。

AIがよくやるミス

  • @media (hover: hover) なしで :hover を追加する:ホバーエフェクトがタッチデバイスでタップ後に「スティッキー」な状態で残り、ユーザーを混乱させます。
  • 代替なしで :focus のアウトラインを削除する:focusoutline: none を書きながら視覚的フォーカスインジケーターを一切提供せず、キーボードユーザーにとってページがアクセス不能になります。
  • :focus-visible ではなく :focus を使用する:キーボードフォーカスのみ視覚的インジケーターが必要な場面で、マウスクリックのたびにフォーカスリングを表示してしまいます。
  • すべての状態に同一スタイルを適用する:hover:focus:active を同じ見た目にしてしまい、インタラクションの種類に関する意味のある視覚的フィードバックが失われます。
  • LVHAの順序を忘れる:visited の前に :hover を書いてしまい、リンクスタイリングで詳細度の競合が発生します。
  • タッチデバイスでテストしない:ホバーがどこでも機能すると思い込み、モバイルでのインタラクションを一切検証しません。
  • あらゆる要素に cursor: pointer を使用する:div のような非インタラクティブ要素に cursor: pointer を追加し、ユーザーを誤解させます。

使い分け

  • :hover@media (hover: hover):マウスやトラックパッドでのみ意味のある視覚的エンハンスメント(色の変化、シャドウ、エレベーション)に使います。
  • :focus-visible:ボタン、リンク、カスタムインタラクティブ要素のキーボードフォーカスインジケーターに使います。
  • :focus:すべてのフォーカスタイプで視覚的インジケーターが必要なフォーム入力に使います。
  • :active:ボタンやインタラクティブ要素の押下・タップフィードバック(スケール、色の変化)に使います。
  • @media (pointer: coarse):タッチデバイス向けにタッチターゲットサイズとパディングを増やす場合に使います。

Tailwind CSS

Tailwind は CSS擬似クラスに直接対応する hover:focus:focus-visible:active: のバリアントプレフィックスを提供しています。

Tailwind: インタラクティブ状態バリアント

参考リンク