[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-css-focus-visible-keyboard-focus-rings":44,"search-suggestions":60,"quiz-article-css-focus-visible-keyboard-focus-rings":105},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"01a03297-a328-752b-8d64-3b72182313bb","css-focus-visible-keyboard-focus-rings","PRACTICE_QUIZ",":focus-visible — keyboard-only focus rings","Check what you took from the post: why outline: none alone is an accessibility bug, what :focus-visible actually matches, and the one element type that breaks the simple mental model.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},6,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,69,73,77,81,84,88,91,94,98,102],{"slug":62,"name":63,"articles":64},"webdev","Webdev",72,{"slug":66,"name":67,"articles":68},"frontend","Frontend",62,{"slug":70,"name":71,"articles":72},"javascript","Javascript",60,{"slug":74,"name":75,"articles":76},"css","Css",28,{"slug":78,"name":79,"articles":80},"typescript","Typescript",11,{"slug":82,"name":83,"articles":80},"tutorial","Tutorial",{"slug":85,"name":86,"articles":87},"performance","Performance",9,{"slug":89,"name":90,"articles":51},"react","React",{"slug":92,"name":93,"articles":51},"grammar","Grammar",{"slug":95,"name":96,"articles":97},"node","Node",5,{"slug":99,"name":100,"articles":101},"programming","Programming",4,{"slug":103,"name":104,"articles":101},"html","Html",{"id":106,"slug":46,"title":107,"subtitle":52,"excerpt":108,"coverUrl":109,"locale":13,"readingMinutes":101,"publishedAt":110,"viewCount":111,"likeCount":19,"commentCount":19,"author":112,"vertical":117,"topic":118,"tags":120,"_count":127,"playground":129,"body":131,"bodyMd":250,"seo":251,"translationGroupId":253,"series":52,"podcastUrl":52,"verticalId":5,"thread":254,"assessments":256,"translations":259,"quiz":261},"01a03297-a2d3-7318-bffe-5edb58824c5d","The outline: none Debate Has Been Over Since 2020","A designer flags the ugly blue focus ring in review. An engineer deletes it with outline: none. Nobody notices the keyboard-only user who now can't tell where they are on the page.","\u002Fmedia\u002Fcovers\u002Fcss-focus-visible-keyboard-focus-rings.png","2026-08-29T12:32:09.792Z",36,{"id":113,"name":114,"username":115,"avatarUrl":52,"headline":116},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":74,"name":119},"CSS",[121,122,125,126],{"slug":74,"name":75,"color":52},{"slug":123,"name":124,"color":52},"accessibility","Accessibility",{"slug":62,"name":63,"color":52},{"slug":66,"name":67,"color":52},{"assessments":128},1,{"slug":46,"title":130},":focus-visible — interactive playground",{"blocks":132,"version":128},[133,137,142,145,148,151,155,158,163,167,170,173,176,179,182,185,188,191,195,198,201,204,207,210,213,216,219,222,225,228,231,234,237,240,243],{"id":134,"html":135,"type":136},"b1","\u003Cp>A design review flags it every time: a button gets clicked, and a chunky blue ring slaps itself around the edge for half a second. &quot;Can we remove that? It looks broken.&quot; The engineer knows the one-line fix.\u003C\u002Fp>","paragraph",{"id":138,"code":139,"type":140,"language":74,"highlight":141},"b2","*:focus {\n  outline: none;\n}","code",[],{"id":143,"html":144,"type":136},"b3","\u003Cp>Ship it. The review passes. The button looks clean in every screenshot, in every browser, on every click.\u003C\u002Fp>",{"id":146,"html":147,"type":136},"b4","\u003Cp>Three weeks later an accessibility audit comes back with a line item: &quot;Keyboard users cannot determine which element currently has focus.&quot; Nobody touched a mouse to find that bug. They tabbed through the page with their eyes closed to the cursor, because that&#39;s how they use the page every day — and your clean button is now indistinguishable from every other pixel on the screen.\u003C\u002Fp>",{"id":149,"html":150,"type":136},"b5","\u003Cp>That&#39;s not a nitpick. It&#39;s \u003Ca href=\"https:\u002F\u002Fwww.w3.org\u002FWAI\u002FWCAG21\u002FUnderstanding\u002Ffocus-visible.html\">WCAG 2.4.7\u003C\u002Fa>, a real success criterion, and \u003Ccode>outline: none\u003C\u002Fcode> on its own fails it every time.\u003C\u002Fp>",{"id":152,"html":153,"text":153,"type":154,"level":31},"b6","The fix people reach for next is also wrong","heading",{"id":156,"html":157,"type":136},"b7","\u003Cp>The instinct isn&#39;t to put the ring back — nobody wants the ugly version either. It&#39;s to make it conditional: show it for keyboard users, hide it for mouse users, since a mouse click already gives you visual feedback that something happened. Reasonable goal. Before 2020, CSS had no way to ask &quot;was this focus event a click or a keypress?&quot; — so people answered the question in JavaScript instead:\u003C\u002Fp>",{"id":159,"code":160,"type":140,"language":161,"highlight":162},"b8","let usingMouse = false;\ndocument.addEventListener(\"mousedown\", () => { usingMouse = true; });\ndocument.addEventListener(\"keydown\", (e) => {\n  if (e.key === \"Tab\") usingMouse = false;\n});\ndocument.addEventListener(\"focusin\", (e) => {\n  document.body.classList.toggle(\"using-mouse\", usingMouse);\n});","js",[],{"id":164,"code":165,"type":140,"language":74,"highlight":166},"b9","body.using-mouse :focus {\n  outline: none;\n}",[],{"id":168,"html":169,"type":136},"b10","\u003Cp>This is roughly what the WICG&#39;s own \u003Ccode>focus-visible\u003C\u002Fcode> polyfill did under the hood, and plenty of design systems shipped a version of it. It worked. It also meant every page paid for a global event-listener setup, a class toggling on \u003Ccode>&lt;body&gt;\u003C\u002Fcode> on every focus change, and a heuristic that could still drift out of sync — tab into a field, click somewhere unrelated, tab again, and the class-based guess doesn&#39;t always match what actually happened.\u003C\u002Fp>",{"id":171,"html":172,"type":136},"b11","\u003C!-- playground:start -->",{"id":174,"html":175,"text":175,"type":154,"level":31},"b12","🎮 Try it yourself",{"id":177,"html":178,"type":136},"b13","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-focus-visible-keyboard-focus-rings\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":180,"html":181,"type":136},"b14","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":183,"html":184,"type":136},"b15","\u003C!-- playground:end -->",{"id":186,"html":187,"text":187,"type":154,"level":31},"b16","The selector that made the JavaScript unnecessary",{"id":189,"html":190,"type":136},"b17","\u003Cp>Browsers already knew whether a focus event came from a keyboard or a pointer — they use that exact signal internally to decide their own default outline behavior. \u003Ccode>:focus-visible\u003C\u002Fcode> just exposes it to your CSS.\u003C\u002Fp>",{"id":192,"code":193,"type":140,"language":74,"highlight":194},"b18",".btn:focus {\n  outline: none;\n}\n.btn:focus-visible {\n  outline: 2px solid #4f8cff;\n  outline-offset: 2px;\n}",[],{"id":196,"html":197,"type":136},"b19","\u003Cp>\u003Ccode>:focus\u003C\u002Fcode> still matches on every focus event, same as always — a mouse click, a tap, a keyboard tab, a \u003Ccode>.focus()\u003C\u002Fcode> call from your own JS. \u003Ccode>:focus-visible\u003C\u002Fcode> only matches the subset of those where the browser&#39;s own heuristic decides a visible indicator is warranted. Tab to the button: it matches, the ring shows. Click the button with a mouse: it doesn&#39;t, no ring, and no JavaScript had to watch for either.\u003C\u002Fp>",{"id":199,"html":200,"type":136},"b20","\u003Cp>It shipped in Chrome and Edge in October 2020 (Chrome 86), and the rest followed within about two years — by 2022 it worked in every evergreen browser without a flag or a polyfill.\u003C\u002Fp>",{"id":202,"html":203,"text":203,"type":154,"level":31},"b21","The gotcha that trips people up on text fields",{"id":205,"html":206,"type":136},"b22","\u003Cp>Try the pattern above on a \u003Ccode>&lt;button&gt;\u003C\u002Fcode> and a mouse click gives you no ring, exactly as expected. Try it on a text \u003Ccode>&lt;input&gt;\u003C\u002Fcode> and clicking it \u003Cem>still\u003C\u002Fem> shows the ring, even with your mouse.\u003C\u002Fp>",{"id":208,"html":209,"type":136},"b23","\u003Cp>That&#39;s not a bug in your CSS — the browser&#39;s heuristic treats form fields you&#39;re about to type into differently from buttons you just clicked. A button click is a complete action; you got your feedback from the click itself. A text field click is the start of typing, and you still need to see that the field is focused so you know where the caret is. \u003Ccode>:focus-visible\u003C\u002Fcode> accounts for that per element, not just per input device — which is exactly the nuance the old \u003Ccode>body.using-mouse\u003C\u002Fcode> class-toggle approach couldn&#39;t express, because it only ever knew about the page as a whole.\u003C\u002Fp>",{"id":211,"html":212,"type":136},"b24","\u003C!-- quiz:start -->",{"id":214,"html":215,"text":215,"type":154,"level":31},"b25","🧠 Test yourself",{"id":217,"html":218,"type":136},"b26","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-focus-visible-keyboard-focus-rings\u002Fquiz\">Take the 6-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":220,"html":221,"type":136},"b27","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":223,"html":224,"type":136},"b28","\u003C!-- quiz:end -->",{"id":226,"html":227,"text":227,"type":154,"level":31},"b29","When outline: none alone is still fine",{"id":229,"html":230,"type":136},"b30","\u003Cp>There&#39;s one case where deleting the ring outright is the right call, not a shortcut: an element that&#39;s \u003Ccode>tabindex=&quot;-1&quot;\u003C\u002Fcode> and only ever receives \u003Cem>programmatic\u003C\u002Fem> focus your own script manages — a modal you focus on open, then visibly indicate some other way (a border, a heading, an announced live region). If a human never tabs to it and never clicks it, there&#39;s no keyboard user to leave stranded. Everywhere a real person can land with Tab, \u003Ccode>:focus-visible\u003C\u002Fcode> is the answer, not a suppressed outline and a hope that nobody notices.\u003C\u002Fp>",{"id":232,"html":233,"type":136},"b31","\u003Cp>Go find the \u003Ccode>*:focus { outline: none; }\u003C\u002Fcode> line in whatever you&#39;re shipping — or the \u003Ccode>using-mouse\u003C\u002Fcode> class hack if your codebase predates 2021 — and swap it for the two rules above. Which one does your codebase still have?\u003C\u002Fp>",{"id":235,"type":236},"b32","divider",{"id":238,"html":239,"type":136},"b33","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":241,"html":242,"type":136},"b34","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":244,"type":245,"items":246,"ordered":18},"b35","list",[247,248,249],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","A design review flags it every time: a button gets clicked, and a chunky blue ring slaps itself around the edge for half a second. \"Can we remove that? It looks broken.\" The engineer knows the one-line fix.\n\n```css\n*:focus {\n  outline: none;\n}\n```\n\nShip it. The review passes. The button looks clean in every screenshot, in every browser, on every click.\n\nThree weeks later an accessibility audit comes back with a line item: \"Keyboard users cannot determine which element currently has focus.\" Nobody touched a mouse to find that bug. They tabbed through the page with their eyes closed to the cursor, because that's how they use the page every day — and your clean button is now indistinguishable from every other pixel on the screen.\n\nThat's not a nitpick. It's [WCAG 2.4.7](https:\u002F\u002Fwww.w3.org\u002FWAI\u002FWCAG21\u002FUnderstanding\u002Ffocus-visible.html), a real success criterion, and `outline: none` on its own fails it every time.\n\n## The fix people reach for next is also wrong\n\nThe instinct isn't to put the ring back — nobody wants the ugly version either. It's to make it conditional: show it for keyboard users, hide it for mouse users, since a mouse click already gives you visual feedback that something happened. Reasonable goal. Before 2020, CSS had no way to ask \"was this focus event a click or a keypress?\" — so people answered the question in JavaScript instead:\n\n```js\nlet usingMouse = false;\ndocument.addEventListener(\"mousedown\", () => { usingMouse = true; });\ndocument.addEventListener(\"keydown\", (e) => {\n  if (e.key === \"Tab\") usingMouse = false;\n});\ndocument.addEventListener(\"focusin\", (e) => {\n  document.body.classList.toggle(\"using-mouse\", usingMouse);\n});\n```\n\n```css\nbody.using-mouse :focus {\n  outline: none;\n}\n```\n\nThis is roughly what the WICG's own `focus-visible` polyfill did under the hood, and plenty of design systems shipped a version of it. It worked. It also meant every page paid for a global event-listener setup, a class toggling on `\u003Cbody>` on every focus change, and a heuristic that could still drift out of sync — tab into a field, click somewhere unrelated, tab again, and the class-based guess doesn't always match what actually happened.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-focus-visible-keyboard-focus-rings\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## The selector that made the JavaScript unnecessary\n\nBrowsers already knew whether a focus event came from a keyboard or a pointer — they use that exact signal internally to decide their own default outline behavior. `:focus-visible` just exposes it to your CSS.\n\n```css\n.btn:focus {\n  outline: none;\n}\n.btn:focus-visible {\n  outline: 2px solid #4f8cff;\n  outline-offset: 2px;\n}\n```\n\n`:focus` still matches on every focus event, same as always — a mouse click, a tap, a keyboard tab, a `.focus()` call from your own JS. `:focus-visible` only matches the subset of those where the browser's own heuristic decides a visible indicator is warranted. Tab to the button: it matches, the ring shows. Click the button with a mouse: it doesn't, no ring, and no JavaScript had to watch for either.\n\nIt shipped in Chrome and Edge in October 2020 (Chrome 86), and the rest followed within about two years — by 2022 it worked in every evergreen browser without a flag or a polyfill.\n\n## The gotcha that trips people up on text fields\n\nTry the pattern above on a `\u003Cbutton>` and a mouse click gives you no ring, exactly as expected. Try it on a text `\u003Cinput>` and clicking it *still* shows the ring, even with your mouse.\n\nThat's not a bug in your CSS — the browser's heuristic treats form fields you're about to type into differently from buttons you just clicked. A button click is a complete action; you got your feedback from the click itself. A text field click is the start of typing, and you still need to see that the field is focused so you know where the caret is. `:focus-visible` accounts for that per element, not just per input device — which is exactly the nuance the old `body.using-mouse` class-toggle approach couldn't express, because it only ever knew about the page as a whole.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 6-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-focus-visible-keyboard-focus-rings\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\n## When outline: none alone is still fine\n\nThere's one case where deleting the ring outright is the right call, not a shortcut: an element that's `tabindex=\"-1\"` and only ever receives *programmatic* focus your own script manages — a modal you focus on open, then visibly indicate some other way (a border, a heading, an announced live region). If a human never tabs to it and never clicks it, there's no keyboard user to leave stranded. Everywhere a real person can land with Tab, `:focus-visible` is the answer, not a suppressed outline and a hope that nobody notices.\n\nGo find the `*:focus { outline: none; }` line in whatever you're shipping — or the `using-mouse` class hack if your codebase predates 2021 — and swap it for the two rules above. Which one does your codebase still have?\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":107,"canonical":252,"description":108},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-focus-visible-keyboard-focus-rings","01a03297-a2d4-70bf-a6c8-b8c6aa9f6c0a",{"id":255,"locked":18},"01a03297-a313-72db-b805-5366dc17eef8",[257],{"id":45,"slug":46,"title":48,"_count":258},{"questions":51},[260],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":262,"questionCount":51},{"questions":51}]