JSのfocus()でCSS :has(:focus-visible) が効かない?挙動の違いを解消する実装パターン

JavaScriptの.focus()を使って要素にフォーカスを当てた際、CSSの:focus-visibleが反応せず、親要素のスタイル(:hasなど)が更新されないことがあります。

私の経験上、アクセシビリティを意識してモーダルを開いた瞬間に特定のボタンへオートフォーカスさせる際、この挙動の違いが原因で「キーボード操作の時だけデザインが変わらない」という不具合に繋がることがよくあります。今回はこの問題を解決する具体的なコードを紹介します。

なぜJSからの操作だと効かないのか?

:focus-visibleは、ブラウザが「今はフォーカスを強調して表示すべきだ」と自動で判断したときだけ有効になる仕組みだからです。具体的には以下のような違いがあります。

  • キーボード操作(Tabキー等):ブラウザが「強調が必要」と判断し、:focus-visible が有効になる。
  • マウス操作:「強調は不要」と判断され、:focus-visible は効かない。
  • JSの .focus():マウス操作と同じく「強調不要」とみなされるのが一般的。

このため、親要素の状態を検知する :has(:focus-visible) も連動して動かなくなってしまい、UIの一貫性が損なわれる課題が発生します。

解決策:最新APIとクラス付与の併用

実際の運用現場では、一部の最新ブラウザで使えるオプションを活用しつつ、未対応ブラウザ向けにクラスを付与するハイブリッドな手法が最も安定します。

/* CSS: :has()と補助クラスの両方に対応させる */
.container:has(:focus-visible),
.container.is-focused-programmatic {
  outline: 2px solid #007bff;
  border-color: #007bff;
}
/* JS: フォーカス時に明示的にフラグを立てる */
const targetButton = document.querySelector('#button');
const container = targetButton.parentElement;

function safeFocus(el) {
  // 1. 最新のオプションを試す (ChromeなどはこれだけでOK)
  el.focus({ focusVisible: true });

  // 2. 反応しなかったブラウザのためにクラスで補う
  if (!el.matches(':focus-visible')) {
    container.classList.add('is-focused-programmatic');
    
    // フォーカスが外れたらクラスを消す
    el.addEventListener('blur', () => {
      container.classList.remove('is-focused-programmatic');
    }, { once: true });
  }
}

実装時の注意ポイント

  • 要素による違い:input要素はJSからのフォーカスでも :focus-visible が効くことが多いですが、button要素は効かないケースがほとんどです。
  • 安易な :focus の使用は避ける:すべてを :focus に変えてしまうと、マウスで普通にクリックした時にも枠線が出てしまい、デザインの質が落ちるリスクがあります。
  • アクセシビリティ(WCAG)の遵守:この対応を入れることで、操作方法に関わらず「今どこが選択されているか」を確実にユーザーへ伝えられるようになります。

まとめ

強力な :has() ですが、ブラウザの自動判定だけに頼り切るのはまだ少し早いです。focusVisible: true オプションと補助的なクラス制御を組み合わせることで、どんな操作環境でも使いやすい、堅牢なUIを作り上げましょう。

タイトルとURLをコピーしました