フォーカスリングは:focusではなく:focus-visibleで定義する
開発環境
- Next.js(App Router)
- React 19
- Tailwind CSS v4
前提
ボタンやリンクをキーボードの Tab キーで移動すると、フォーカスされた要素のまわりに枠線が出ます。これがフォーカスリング(outline)で、キーボード操作のユーザーが「今どこにいるか」を知るための大事な目印です。
Tailwind を使っている Next.js のプロジェクトで、デザイン都合でこのリングを調整しようと focus: バリアントを触ったところ、マウス操作でも常にリングが出てしまう挙動にハマりました。
ローカルで npm run dev を立ち上げ、Tab キーとマウスクリックの両方で挙動を見比べながら確認しています。
本題
focus: はマウスでもキーボードでも反応する
⚠️ たとえばボタンのフォーカスリングを自前のスタイルに差し替えようとして、こう書いたとします。
<button className="focus:outline-2 focus:outline-blue-500">
送信
</button>focus:(CSS の :focus)は「要素がフォーカスを持っている状態」すべてに当たります。キーボードの Tab で移動したときだけでなく、マウスでクリックしたときにもフォーカスは当たるため、クリックのたびにリングが出ます。
逆に、リングが邪魔だからと消すパターンも厄介です。
{/* マウスユーザーのために消したつもり */}
<button className="focus:outline-none">
送信
</button>これだとキーボードユーザーからもリングが消えてしまい、「今どこをフォーカスしているか」がわからなくなります。アクセシビリティ的にはかなり良くない状態です。
focus-visible: はキーボード操作のときだけ当たる
🛠 そこで focus-visible: バリアントを使います。
<button className="focus-visible:outline-2 focus-visible:outline-blue-500">
送信
</button>focus-visible:(CSS の :focus-visible)は、ブラウザが「フォーカスリングを見せるべき」と判断したときだけ当たります。判断はブラウザのヒューリスティックに任されていて、ざっくり次のような挙動になります。
- キーボード(Tab キーなど)でフォーカスした → リングが出る
- マウスでボタンをクリックしてフォーカスした → リングが出ない
👉 これで「キーボードユーザーには見せる、マウスユーザーには見せない」が、JavaScript なしで実現できます。
実際に npm run dev で動かして、Tab で移動するとリングが出て、マウスクリックでは出ないことをブラウザ上で確認できました。
outline を消したいときの定番パターン
リセットで outline-none を全体にかけてしまうと、キーボードユーザーが迷子になります。消したうえで focus-visible: で出し直すのが安全です。
<button className="outline-none focus-visible:outline-2 focus-visible:outline-blue-500 focus-visible:outline-offset-2">
送信
</button>outline-offset-2 を足すと、要素とリングの間に余白ができて見やすくなります。
毎回このクラスを並べるのが冗長なら、共通ボタンコンポーネントに切り出しておくと使い回しやすくなります。
const Button = ({ children, ...props }: React.ComponentProps<'button'>) => (
<button
className="outline-none focus-visible:outline-2 focus-visible:outline-blue-500 focus-visible:outline-offset-2"
{...props}
>
{children}
</button>
)テキスト入力欄では focus-visible: でも出る
注意点として、focus-visible: は「マウスなら必ず出ない」わけではありません。テキスト入力欄(input や textarea)はマウスでクリックしてフォーカスしても、ブラウザはリングを出すべきと判断します。
- ボタンやリンク:マウスクリックではリングが出ない
- テキスト入力欄:マウスクリックでもリングが出る
これは「入力欄は今どこに文字が入るかを示す必要がある」という判断によるもので、意図どおりの挙動です。focus-visible: に任せておけば、要素の種類に応じていい感じに出し分けてくれます。
さいごに
フォーカスリングは「消すか出すか」の二択で考えがちですが、本当にやりたいのは「マウスのときは出さない、キーボードのときは出す」という出し分けです。
focus: でこれをやろうとすると JavaScript で操作種別を見分ける必要がありましたが、focus-visible: ならブラウザがその判断を肩代わりしてくれます。
Tailwind でフォーカス周りのスタイルを書くときは、まず focus: ではなく focus-visible: を第一候補にする、と覚えておくと迷わなくなりそうです。