mrkeyoor.com_
Fri 23 Oct 23:37 UTC
npmWeb Frontendupdated 23 Oct 2026

focus-visible review

A mouse click on a writable text field can still get a focus ring. focus-visible 5.2.1 is a browser polyfill for the :focus-visible CSS pseudo-class. The polyfill adds a focus-visible class to focused elements when its heuristics call for a visible indicator. Writable text inputs and textareas can receive the class even after a pointer click. That exception matters when you want to quiet a button's mouse-focus outline without hiding the indicator on a field someone is about to type into. The maintainers recommend native :focus-visible CSS where possible, so check your browser requirements before adding this dependency.

Verdict

Keep focus-visible for a browser requirement or an existing class-based integration that still needs it. If native :focus-visible meets your requirements, start with the defensive CSS recipe below and check the resulting rings before removing the dependency.

Answers from our run

focus-visible or what-input: which should you use?

what-input: Choose what-input when you need to track the current mouse, keyboard or touch input method across the page and write your own styling rules. That is a broader input-tracking task than applying this polyfill's per-element class. Keep focus-visible for a browser requirement or an existing class-based integration that still needs it.

When should you not use focus-visible?

Your supported browsers already implement :focus-visible. The project recommends using native CSS where you can, which avoids loading this script.

API stability3/5API rubric: 1 means an unclear integration contract, 3 means an inspectable contract in the reviewed version, and 5 requires demonstrated compatibility across upgrades. The 5.2.1 source exposes the browser helper and readiness event, adds the documented classes and attributes, and initializes the document. That is enough to explain this version's integration. It does not establish upgrade stability or working behavior across your browser matrix. No compatibility trial contributes to this score.
Docs4/5Documentation rubric: 1 gives only a purpose, 3 supports basic setup, and 5 would explain the relevant edge behavior consistently without needing source inspection. The pinned README covers installation, guarded CSS, attribute selectors, manual classes, shadow roots, late loading and defensive native CSS. It also gives a clear reason to skip the dependency. The source was still needed to clarify the broader keydown rule and the readOnly exception to its simplified text-field explanation, so this is short of 5.
Maintenance2/5Maintenance rubric: 1 lacks a traceable release or support position, 2 has a clear limited-support position, 3 has evidence of planned continuing work, and 5 would require a demonstrated record of sustained fixes. The version-matched README explicitly says updates and new releases are not planned, with emergency discussion still possible. The exact 5.2.1 release note records a demo-and-docs reference cleanup. These sources support a limited-support judgment, not a promise of future browser fixes or a claim that the runtime changed in this release.
Ecosystem3/5Ecosystem rubric here assesses documented integration choices: 1 is a narrow unexplained environment, 3 supports several concrete integration paths, and 5 would also need verified breadth across consumers and environments. The README and source cover ordinary documents, explicit shadow-root application and attribute selectors for frameworks that replace classes. Those are useful choices for an existing site. Native CSS is the maintainers' preferred route where available, which limits the case for adding this dependency. Download counts, consumer adoption and browser execution were not used to award points.

Use it if

  • Your browser requirements include an environment without native :focus-visible, and you need focus-ring styling that distinguishes pointer use from keyboard use.
  • An existing site already uses .js-focus-visible and .focus-visible selectors, and you need to understand those rules before removing the script.
  • Your framework replaces element classes, so you want the documented data-attribute selectors instead of depending on the polyfill's class names.
  • A web component needs the same polyfill behavior inside a shadow root, and you can own that root's initialization and CSS.
Skip it if

Setup reality

Install the npm package as focus-visible@5.2.1. The published manifest points main and module at dist/focus-visible.js. For a site that serves public/ as its static root, the first two recipes copy the minified browser script into public/vendor/ and load that local URL near the end of body. Adapt the destination to your build system. Installing an npm dependency does not make a node_modules URL publicly reachable. The remaining recipes are independent illustrations, not steps to paste together. We inspected the npm package archive, version-matched README and source. We did not run these examples in a browser or measure installation speed, bundle output or accessibility outcomes.

Keep an ordinary :focus style before adding any suppression rule. The class recipe removes an outline only beneath .js-focus-visible, which the script adds to the document element. If JavaScript never runs, that guarded rule does not match and the basic indicator remains. For a framework that overwrites classes, use the documented data-js-focus-visible and data-focus-visible-added selectors instead. Choose one convention consistently. A manually supplied focus-visible class is a separate way to keep an indicator on a particular control.

The details are in the 5.2.1 implementation. Its keydown handler enters keyboard mode unless Alt, Ctrl or Meta is pressed, rather than checking only for Tab. Writable allowlisted input types, writable textareas and contenteditable elements also request the class when focused. A read-only field skips the text-entry shortcut, but it can still get a ring through keyboard mode. Do not mistake this heuristic for a complete account of someone's accessibility needs.

Shadow roots need both an explicit applyFocusVisiblePolyfill(root) call and styles inside the root. The helper marks the host with data-js-focus-visible, so a document-level .js-focus-visible selector alone is not the component's styling solution. If the script loads late, check whether the helper already exists before waiting for focus-visible-polyfill-ready. The event is dispatched once when the script loads, and waiting only for a future event can miss it. Assign one initialization path per root.

The version-matched README recommends native CSS and says routine polyfill releases are not planned. The 5.2.1 release note describes removing a malicious polyfill-service reference from the demo and docs. That release note does not announce a new focus algorithm. If your task is keeping focus inside a React dialog, the react-focus-lock guide covers that separate decision.

Before shipping either the polyfill or the native-CSS recipe, check Tab traversal, pointer clicks on buttons and writable fields, JavaScript disabled, and the controls inside each shadow root. Inspect the actual indicator against your page backgrounds. The scores below are editorial judgments from documentation and source, using the rubric in each rationale. They are not browser test results or adoption measurements.

Patterns

Install and copy the browser file install-and-copy-browser-script

npm install --save-exact focus-visible@5.2.1
mkdir -p public/vendor
cp node_modules/focus-visible/dist/focus-visible.min.js public/vendor/focus-visible.min.js

All recipes on this page are unexecuted documentation-based illustrations for 5.2.1. These shell commands assume public/ is your static document root. They copy a browser asset, not a stylesheet. Match the served URL to your deployment and include the file-copy step in your build.

Load the copied script near the end of body load-local-browser-script

<button type="button">Save settings</button>
<script src="/vendor/focus-visible.min.js"></script>

Place this HTML inside body after the page controls. The URL assumes the copy destination in the install recipe. The script initializes the document automatically, but shadow roots need their own helper calls. A script tag alone does not add your chosen focus-ring CSS.

Keep a fallback ring when JavaScript is absent guard-class-focus-rules

button:focus,
a[href]:focus {
  outline: 2px solid #0757b8;
  outline-offset: 3px;
}

.js-focus-visible button:focus:not(.focus-visible),
.js-focus-visible a[href]:focus:not(.focus-visible) {
  outline: none;
}

The script adds .js-focus-visible to the document element. Keeping suppression beneath that marker preserves the basic :focus rule if the script never starts. This example targets buttons and links only. Check the chosen outline color against your actual backgrounds.

Use attributes when a framework replaces classes use-attribute-focus-rules

.action:focus {
  outline: 2px solid #0757b8;
  outline-offset: 3px;
}

[data-js-focus-visible] .action:focus:not([data-focus-visible-added]) {
  outline: none;
}

Use this instead of the class-based suppression rule for controls with class="action". The README documents these attributes for frameworks that overwrite classes. The per-element attribute identifies a class added by the polyfill. A manually supplied .focus-visible class does not receive that ownership attribute, so do not combine this rule with the manual-class recipe.

Keep an indicator on a chosen button keep-a-control-ring

<style>
  button:focus {
    outline: 2px solid #0757b8;
    outline-offset: 3px;
  }
  .js-focus-visible button:focus:not(.focus-visible) {
    outline: none;
  }
</style>
<button type="button" class="focus-visible">Review payment</button>

In the class-based approach, supply .focus-visible yourself before the script handles focus. The implementation leaves an existing class alone and removes classes on blur only when its data-focus-visible-added ownership marker is present. The persistent class does not move focus or display this outline on an unfocused button.

Keep writable text fields visible after a click style-text-entry-focus

<style>
  input:focus {
    outline: 2px solid #0757b8;
    outline-offset: 3px;
  }
  .js-focus-visible input:focus:not(.focus-visible) {
    outline: none;
  }
</style>
<label>
  Email
  <input type="email" name="email" autocomplete="email">
</label>

The 5.2.1 allowlist includes email inputs, so a writable focused field requests the class even after pointer use. A readOnly input skips that shortcut, though keyboard mode can still give it the class. Do not use the presence of a ring as proof that the last interaction was a key press.

Apply the polyfill and CSS inside a shadow root initialize-shadow-root

var host = document.createElement('div');
var root = host.attachShadow({ mode: 'open' });
root.innerHTML = 
  '<style>' +
  'button:focus { outline: 2px solid #0757b8; outline-offset: 3px; }' +
  ':host([data-js-focus-visible]) button:focus:not(.focus-visible) { outline: none; }' +
  '</style><button type="button">Component action</button>';
document.body.appendChild(host);

if (typeof window.applyFocusVisiblePolyfill === 'function') {
  window.applyFocusVisiblePolyfill(root);
}

Run after body exists and, for polyfill behavior, after the script loads. This recipe requires a browser with attachShadow. The helper marks the host with data-js-focus-visible, and the component's own :host selector guards suppression. Without the helper, its basic focus style remains. Use the late-loading recipe if script order is uncertain.

Handle a polyfill that may already have loaded wait-for-late-polyfill

var host = document.querySelector('focus-card');
var root = host && host.shadowRoot;

if (root) {
  if (typeof window.applyFocusVisiblePolyfill === 'function') {
    window.applyFocusVisiblePolyfill(root);
  } else {
    window.addEventListener('focus-visible-polyfill-ready', function () {
      window.applyFocusVisiblePolyfill(root);
    }, { once: true });
  }
}

This assumes an existing focus-card element with an open shadow root and its own focus styles. Run this initialization path once for that root instead of also calling the helper in another recipe. The global event fires at script load, so first checking the helper avoids waiting for an event that already happened. This code does not create a later-mounted component.

Use native CSS while preserving an older-browser fallback replace-with-defensive-native-css

button:focus {
  outline: 2px solid #0757b8;
  outline-offset: 3px;
}

button:focus:not(:focus-visible) {
  outline: none;
}

button:focus-visible {
  outline: 3px solid #0757b8;
  outline-offset: 3px;
}

This is the README's defensive native-CSS structure, without the polyfill. Keep the rules separate: a browser that rejects the :focus-visible selectors can retain the first :focus rule. Adapt it to the other focusable controls you own. Check the browser requirements and the resulting indicators before removing the script and its old CSS.

Alternatives

PackageRegistryPick it when
what-inputnpmChoose what-input when you need to track the current mouse, keyboard or touch input method across the page and write your own styling rules. That is a broader input-tracking task than applying this polyfill's per-element class.
@react-aria/focusnpmChoose @react-aria/focus when a React component needs useFocusRing() state and props to decide its own ring styling. It is a component-level approach rather than a document-wide class polyfill.

More web frontend guides

postcss · react · react-dom · tailwindcss · htmlparser2 · tailwind-merge · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.