スマホでタップ後に:hoverが残る問題の直し方|@media (hover: hover)とタップ挙動の実務対応

スマホでタップ後に:hoverが残る問題の直し方|@media (hover: hover)とタップ挙動の実務対応

結論:hoverは「マウスがある環境だけ」に閉じる

  • スマホのタップは内部的に hover → click の順で扱われる=タップした瞬間に:hoverが当たる
  • iOS Safariは他の場所を触るまで:hoverが解除されない=見た目が張り付いて残る
  • 直し方は @media (hover: hover) and (pointer: fine) の中にhover装飾を移すだけ
  • hoverを外したらタップのフィードバックが消えるので :active で代替する
  • :hoverだけで開くドロップダウンはタッチ端末で操作不能。JSでのtoggleに置き換える

PCで作ったボタンやカードをスマホ実機で確認すると、タップしたあとhoverの見た目(色が変わる・浮き上がる・画像が拡大する)がそのまま残り続ける——この現象はCSSの書き間違いではなく、タッチ端末で :hover がどう扱われるかという仕様どおりの挙動です。原因が分かれば直し方は一行のメディアクエリに集約されます。実案件で踏んだ順に、判定の考え方と実務で使っているコードをまとめます。

なぜスマホでは:hoverが「残る」のか

タッチ端末にはマウスカーソルがありません。それでも :hover が当たるのは、ブラウザがタップをマウス操作としてエミュレートしているからです。

タップは「hoverしてからclickされた」として処理される

指が要素に触れると、ブラウザは互換性のために mouseover → mousedown → mouseup → click という一連のマウスイベントを合成して発火させます。このとき :hover の状態も一緒に付与されます。つまりタップした瞬間、その要素はhover状態になるのが正常な動作です。

問題は「解除されるタイミング」がないこと

マウスならカーソルが要素から離れた時点でhoverが外れます。ところがタッチでは「指が離れた=どこへ移動した」という情報がないため、ブラウザはhover状態を解除する契機を持ちません。結果として次に別の場所をタップするまでhoverが当たり続けます。

実機で確認した範囲では、iOS Safari(iPhone 13/iOS 18系)が最も分かりやすく残り、カード一覧をタップして詳細から戻ってくると、戻った先のカードが拡大したままになっていました。Android Chrome(Pixel系)はスクロールすると解除されることが多いものの、その場でタップして指を離しただけでは同じく残ります。「Androidでは起きないから大丈夫」と判断すると取りこぼします。

直し方:hover装飾を @media (hover: hover) の中に入れる

解決策はシンプルで、hoverによる装飾を「マウスなどの正確なポインタを持つ環境」だけに適用することです。CSS Media Queries Level 4 のインタラクションメディア特性を使います。

/* NG: すべての環境で当たる */
.c-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 8px 24px rgba(0, 0, 0, .12);
}

/* OK: マウス環境だけに閉じる */
@media (hover: hover) and (pointer: fine) {
  .c-card:hover {
    transform: translateY(-4px);
    box-shadow: 0 8px 24px rgba(0, 0, 0, .12);
  }
}

hover: hover と pointer: fine を両方書く理由

hover: hover は「主入力デバイスが要素をホバーできるか」、pointer: fine は「主入力デバイスが正確に位置を指せるか(マウスやスタイラス)」の判定です。片方だけでは取りこぼしが出ます。

  • hover: hover のみ:タッチ操作可能なWindowsノートPCが hover: hover を返す構成があり、タッチで使っているときにhoverが残る余地が生まれます
  • pointer: fine のみ:スタイラス主体の環境でhoverできないのに装飾が当たります

実務では両方をANDで結ぶのが最も事故が少ないというのが結論です。ここ1年ほどの案件はすべてこの形で固定していますが、これが原因の不具合報告は出ていません。

SCSSではmixinにして機械的に置換する

既存サイトの改修では、hoverを書いている箇所が数十カ所に散らばっているのが普通です。都度メディアクエリを書くと抜けが出るので、mixinを一つ用意して置換します。

@mixin hover {
  @media (hover: hover) and (pointer: fine) {
    &:hover {
      @content;
    }
  }
}

.c-btn {
  transition: background-color .2s;

  @include hover {
    background-color: #0d9488;
  }
}

この形にしておくと、:hover { を @include hover { に置き換えるだけの単純作業になり、レビューでも「素の :hover が残っていないか」をgrep一発で確認できます。既存プロジェクトの一括改修では、grep -rn ":hover" scss/ の結果件数をゼロに近づける進め方が確実でした。

hoverを外すとタップの手応えが消える——:activeで補う

hoverをマウス環境に閉じた直後によく言われるのが「スマホでボタンを押しても反応した感じがしない」というものです。これまではhoverが押下フィードバックの代わりをしていたためで、タッチ用には :active を別に用意します。

.c-btn {
  transition: background-color .2s, transform .1s;
}
.c-btn:active {
  transform: scale(.97);
  background-color: #0b7f76;
}

iOSで:activeが発火しないときの対処

iOS Safariでは、要素またはその祖先に touchstart のリスナーが一つも無いと :active が発火しないことがあります。実装が正しいのに効かない場合は、body に空のリスナーを登録すると解決します。

document.body.addEventListener('touchstart', function () {}, { passive: true });

また -webkit-tap-highlight-color: transparent; を全体に当てているサイトでは、OS標準のタップ時ハイライトも消えています。これを外すか、上記のように :active で明示的なフィードバックを用意するかのどちらかは必要です。両方消してしまうと「押したのに何も起きない」体験になり、二重タップや離脱の原因になります。

個別ケース:現場で実際に詰まった3パターン

① :hoverだけで開くドロップダウンメニュー

PC用に .nav-item:hover .submenu { display: block; } で組んだメニューは、タッチ端末では最初のタップで開き、そのまま閉じられない(あるいは親リンクへ遷移してしまう)状態になります。これはメディアクエリで囲っても解決せず、タッチ環境では明示的にJSでの開閉に切り替える必要があります。@media (hover: hover) の中でhover開閉、外ではボタン要素として aria-expanded をトグルする実装に分けるのが定石です。

② カード全体のhoverで画像をズームする演出

一覧ページで多用されるパターンです。タップで拡大したまま固定されるうえ、ブラウザバックで戻ったときにも拡大状態が復元されて見えるため、崩れが目立ちます。ズーム量が大きいほど気づかれやすいので、優先して直す価値があります。

③ 表内のリンクやアイコンだけhoverが残る

コンポーネント単位でhoverを整理していると、テーブルや装飾用の小さなパーツだけ古い書き方が残りがちです。ここは前述のgrepでの機械的な洗い出しが効きます。目視レビューでは必ず取りこぼします。

スマホ主体のサイトほど効いてくる

この対応は見た目の細部の話に見えますが、モバイル比率が高いサイトほど体験差になって表れます。特に多言語サイトはスマホからの流入比率が高くなりやすく、タップ操作の違和感がそのまま直帰につながります。多言語サイト全体の設計と実装の進め方は多言語サイトの作り方で整理しているので、あわせて確認してください。

なお、タップ後の見た目が残る問題はCSS側の話ですが、「タップしてから反応するまでが遅い」場合は原因が別にあります。サーバーの応答速度が効いている場合はWeb制作者が選ぶサーバー比較、乗り換えを検討する段階ならWordPressのサーバー移行手順が参考になります。

チェックリスト

  • SCSS/CSS内に素の :hover が残っていないか(grepで確認)
  • hover装飾は @media (hover: hover) and (pointer: fine) の中にあるか
  • タッチ環境向けに :active のフィードバックがあるか
  • -webkit-tap-highlight-color: transparent と :active の両方を消していないか
  • hoverで開くメニューがタッチ環境でJS開閉に切り替わっているか
  • 実機(iOS Safari)で、タップ後に戻ってきた画面の見た目が元に戻るか

ここまでの内容は、既存サイトなら1〜2時間、新規なら最初からmixinを入れておくだけで済みます。スマホで「なんとなく操作しづらい」と言われるサイトの原因は、たいていこの積み重ねです。

多言語・ベトナム語サイトの制作でお困りではありませんか?

フォント・文字化け・WordPress多言語化・サーバー選定まで、多言語サイト制作の実績を持つ制作会社が一括対応します。お見積り・ご相談は無料です。

多言語サイト制作の詳細を見る →

すぐに相談したい方は お問い合わせフォームへ

関連記事(コーディング実務Tips)

Web制作Tips(コーディング実務)カテゴリの最新記事