# ホバーでしか現れないカードの操作ボタン

Source: https://easeweb.dev/learn/hover-actions
Topics: ドラッグ&ドロップ > ホバーで現れる操作に、キーボードやタッチでも届く (WCAG 2.1.1, 1.4.13)
The fix: ホバーでの表示を残すなら、ほかの入り口をすべて加えます。:hoverに加えて:focus-withinでも表示し、ホバーのない端末では常に表示し、カードを覆わない専用の場所に置きます。または隠すのをやめ、小さく控えめなボタンとして、すべてのカードに常に表示します。
Test with: keyboard, mouse, touch, zoom, screen reader
References: https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html https://www.w3.org/WAI/WCAG22/Understanding/content-on-hover-or-focus.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F54 https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-within https://developer.mozilla.org/en-US/docs/Web/CSS/@media/hover

よくある問題を再現した学習用のサンプルです。実在のクライアントの診断結果ではありません。コードは出発点となる実装例のため、実際の製品でも確認してください。ここでは支援技術でのテスト結果は報告していません。

## 場面

ショップの商品一覧は、すっきりとデザインされています。各カードには写真、商品名、価格だけがあります。カードにポインターを重ねると、価格の下に操作の行がふわりと現れます。「保存」と「カートに入れる」です。ポインターを離すと消えます。一覧は静かなまま、マウスを使う人には操作が届きます。

マウスを使わずにTabキーを押してみます。フォーカスは、あるカードの商品名から次のカードの商品名へ移ります。「保存」と「カートに入れる」には届きません。操作の行は、カードにホバーするまで `visibility: hidden` で隠されていて、隠れたボタンにはフォーカスが入らないからです。スマートフォンには、重ねておくポインターがありません。カードの写真や商品名をタップすると商品ページが開き、一覧で操作が表示されることはありません。

同じデザインには、もう一つの実装があります。行を `opacity: 0` で隠し、カードの下端に、価格の上に重ねて滑り込ませるものです。こちらではTabで「保存」に届きますが、フォーカスがある間もボタンは完全に透明なので、キーボードの利用者には何も変わって見えず、Enterで何が起きるか分かりません。画面拡大鏡では、ポインターが着いた途端に行が価格を覆い、Escapeを押しても何も起きません。

## 確認できる問題

操作は、止まったポインターに対してしか存在しません。WCAG 2.1.1（キーボード）は、すべての機能をキーボードで操作できることを求めています。失敗例F54は、同じことをスクリプトで行う場合を挙げています。ポインティングデバイスのイベントでしか届かない機能です。`visibility: hidden` や `display: none` で隠すと、ボタンはTabの順序から外れます。`opacity: 0` では順序に残りますが、見えません。見えないボタンを押せても、助けにはなりません。タッチスクリーンはそもそもホバーできないため、スマートフォンの利用者にはどちらの実装も届きません。

表示された行がほかの内容を覆う場合は、1.4.13（ホバー又はフォーカスで表示されるコンテンツ）にも関わります。ホバーで現れて元の内容を覆うコンテンツは、ポインターやフォーカスを動かさずに消せる必要があります。通常はEscapeです。価格の上に滑り込む操作は、閉じる手段がないため、これを満たしません。

## 利用者への影響

このパターンは、商品一覧、ファイルの一覧、写真のギャラリー、ダッシュボードなど、デザイナーが初期状態を静かに保ちたい場面でよく使われます。そのカードの「保存」や「削除」が、そこにしかないこともよくあります。

## 自動チェックとAIの限界

マークアップに問題はありません。カードの中に、名前のある本物のボタンがあります。静的なスキャンはそれを見て合格にします。問題はCSSのルールと、起きないことにあります。誰もホバーせず、ほかに行を表示するものがありません。ツールは、カードにホバーしてボタンが現れるのを確認し、次にそのボタンにフォーカスして、まだ見えているかを確かめる必要があります。修正をAIに頼むと、カードに `tabindex="0"` を付けることがあります。カードはTabの順序に入りますが、やはり何も表示されません。`visibility` を `opacity` に替えることもあり、問題の形が変わるだけです。

## 修正前：カードにホバーするまで隠れた操作

```html
<article class="card">
  <a href="/tote"><img src="tote.jpg" alt="リネンのトート、ナチュラル">リネンのトート</a>
  <p class="price">$48</p>
  <div class="actions">
    <button type="button">保存</button>
    <button type="button">カートに入れる</button>
  </div>
</article>
```

```css
.card .actions {
  visibility: hidden;
  opacity: 0;
  transition: opacity 0.15s;
}
.card:hover .actions {
  visibility: visible;
  opacity: 1;
}
```

行を表示するのは `:hover` だけです。`visibility: hidden` で隠れたボタンはTabの順序から外れるため、キーボードでは届かず、タッチスクリーンはホバーしません。

## 修正前：透明なまま価格に重ねて滑り込む操作

```css
.card {
  position: relative;
}
.card .actions {
  position: absolute;
  inset: auto 0 0;
  opacity: 0;
  translate: 0 100%;
  transition: opacity 0.15s, translate 0.15s;
}
.card:hover .actions {
  opacity: 1;
  translate: 0;
}
```

Tabでボタンに届くようにはなりますが、フォーカス中も `opacity: 0` のままなので、フォーカスの位置も、Enterで押されるものも画面に表示されません。表示されると行がカードの下部を覆い、どのキーでも消せません。

## 修正後：フォーカス時とタッチスクリーンでも表示し、専用の行に置く

```css
/* 行に専用の場所を与え、写真や価格を覆わないようにします。 */
.card .actions {
  visibility: hidden;
  opacity: 0;
  transition: opacity 0.15s;
}
/* ホバーと、カード内のどこかへのキーボードフォーカスで表示します。 */
.card:hover .actions,
.card:focus-within .actions {
  visibility: visible;
  opacity: 1;
}
/* 重ねておくホバーがない端末では、常に表示します。 */
@media (hover: none) {
  .card .actions {
    visibility: visible;
    opacity: 1;
  }
}
@media (prefers-reduced-motion: reduce) {
  .card .actions {
    transition: none;
  }
}
```

`:focus-within` は、カードの中のどれかの要素にフォーカスがある間、カードに一致します。Tabでカードのリンクに着いた時点で行が現れ、その後のボタンにフォーカスが届いたときには見えています。`visibility` も同じルールで切り替えないと、ボタンはTabの順序から外れたままです。メディアクエリー `hover: none` は、スマートフォンや、タッチで使うタブレットで真になります。それがなければ、行は一度も表示されません。行は価格の下の専用の場所に置かれたままなので、現れても何も覆わず、1.4.13が消す手段を求めることもありません。ポインターでの表示は、ポインターを使うすべての人のために、そのまま残ります。

## 修正後：常に表示し、なじむように描く

```html
<article class="card">
  <a href="/tote"><img src="tote.jpg" alt="リネンのトート、ナチュラル">リネンのトート</a>
  <p class="price">$48</p>
  <div class="actions">
    <button type="button" class="quiet">保存</button>
    <button type="button" class="quiet">カートに入れる</button>
  </div>
</article>
```

```css
/* カードの文字色の小さな枠線ボタン。写真と張り合わずに、そこにあります。 */
.card .actions {
  display: flex;
  gap: 0.5rem;
}
.card .quiet {
  min-height: 24px;
  padding: 0.25rem 0.75rem;
  border: 1px solid currentColor;
  border-radius: 999px;
  background: transparent;
  color: inherit;
  font-size: 0.875rem;
}
.card .quiet:hover,
.card .quiet:focus-visible {
  background: var(--ink);
  color: var(--paper);
}
```

何も隠していないので、表示する仕組みも要りません。ボタンはTabの順序にあり、フォーカス時に見え、スマートフォンにもあり、何も覆いません。静けさは、隠すのではなくデザインで作ります。カードの文字色の枠線だけの小さなピル型ボタンで、ホバーとフォーカスでは塗りつぶします。初期状態の一覧は以前とほぼ変わらない落ち着きのまま、どの使い方でも動きます。

## どの方法を選ぶか

どちらも基準を満たします。フォーカスとタッチでの表示は、ポインターの利用者にはデザインどおりの見た目を保ち、CSSのルール3つで済みます。ただし、カードを置く場所が増えるたびに同じルールが必要で、`hover: none` の代替によってスマートフォンのレイアウトはパソコンと違う見た目になります。デザインファイルにその状態を描いておくべきです。常に表示する方法は初期状態の見た目を変え、ボタンがそこになじむようにデザイナーが描く必要があります。ショップの一覧の「カートに入れる」や、ファイル一覧の「削除」のように、操作が重要なカードでは、ボタンを描き込んでください。ショップが頼る操作を、誰かがホバーに気付くことに委ねるべきではありません。ホバーでの表示は、ボタンを並べると込み合う一覧の、二次的な操作に限り、フォーカスとタッチのルールを添えて使います。

三つ目の形として、各カードに常に表示するメニューボタンを置き、操作をそのメニューに入れる方法があります。場所を最も取らず、メニューの全体は[右クリックや長押し、スワイプでしか出ない操作メニュー](/ja/learn/context-menus)のガイドで示しています。

## デザインファイルで

ホバーでの表示は状態の一つで、状態には描画が必要です。ホバー状態の隣に、カードのフォーカス状態を加えてください。操作を表示し、最初のボタンにフォーカスの枠を描きます。さらに、操作を常に表示したタッチ用の状態も加えます。開発者はそこから `:focus-within` と `hover: none` のルールを組み立てられます。常に表示する版では、ボタンを実際のサイズで描いてください。2.5.8が求める最小のターゲットは24 × 24ピクセルで、枠線だけのピル型ボタンは、枠線がカードに対して3:1、文字が4.5:1のコントラストを必要とします。コントラスト比は色見本の横に書いておくと、配色を変えても残ります。

## 実装の判断

一覧に多くのカードがあるときは、各ボタンに、どの商品に対する操作かが分かる名前を付けます。視覚的に隠した「リネンのトートを保存」というspanか、カードの商品名を指す `aria-describedby` です。スクリーンリーダーでボタンだけをたどる人に、「保存、保存、保存」と聞こえないようにします。表示されたボタンのフォーカスの枠は、ほかと同じくカードに対して3:1が必要です。[フォーカス表示が見えない独自デザインのボタン](/ja/learn/visible-focus)を参照してください。カード全体がリンクで、その中にボタンがある場合、操作要素の入れ子は不正なHTMLで、読み上げも乱れます。リンクは商品名と写真に付け、ボタンはその隣に置いてください。

強制配色でも `opacity` と `visibility` はそのまま効くため、隠した行はそこでも隠れたままです。上の修正はそのまま動きます。`display: none` と `display: flex` で切り替える行も、修正の `visibility` と同じように動きますが、ふわりと表示することはできません。どちらかに決めて、すべてのカードコンポーネントに同じ方法を使ってください。

## 修正の確認

1. マウスを使わずにTabで一覧を移動し、どのカードでも、カードかその操作にフォーカスがある間は操作が見えているか確認する。
2. キーボードから「保存」と「カートに入れる」でEnterまたはSpaceを押し、それぞれが動くか確認する。
3. スマートフォンか、デベロッパーツールのデバイスツールバーでタッチ端末にして、ホバーなしで操作が見え、1回のタップで動くか確認する。
4. 400%の拡大表示か画面拡大鏡でカードにホバーし、操作が何も覆わないか、ポインターをカードに置いたままEscapeで隠せるか確認する。
5. スクリーンリーダーでボタンを順にたどり、それぞれがどの商品に対する操作かを伝えるか確認する。

OS、ブラウザーと支援技術のバージョン、ビルド、実施日、各手順の実際の結果を記録してください。カードのコンポーネントや一覧のテーマを変更した際は、再度確認します。

## 制限

このサンプルは、どのアプリケーションでも同じ結果になることを保証するものではありません。表示のルールは、操作がカードの中の普通のボタンであることを前提にしています。ライブラリーのカードコンポーネントは、ボタンをDOMの別の場所に描くことがあり、そこでは `:focus-within` が効きません。実際の一覧で、キーボード、スマートフォン、画面拡大鏡を使って確認してください。

---

このガイドをコーディングエージェントで使う: MCP https://easeweb.dev/mcp（設定は https://easeweb.dev/ja/services/developers）
