Focus Visible is a polyfill for the CSS :focus-visible pseudo-selector that adds a focus-visible class to focused elements when keyboard navigation is used. It enables developers to create accessible focus indicators that appear only when users navigate with the keyboard, while hiding focus rings for mouse interactions.
npm install focus-visibleSince this is a polyfill, it can be used in multiple ways:
<script src="/node_modules/focus-visible/dist/focus-visible.min.js"></script>import "focus-visible";require("focus-visible");Add the script to your HTML page. The polyfill automatically applies to the document when loaded:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<!-- Your content -->
<button>Click me</button>
<input type="text" placeholder="Type here">
<!-- Include polyfill before closing body tag -->
<script src="/node_modules/focus-visible/dist/focus-visible.min.js"></script>
</body>
</html>Use the .js-focus-visible class to conditionally hide default focus styles for mouse users:
/* Hide focus indicator for mouse users */
.js-focus-visible :focus:not(.focus-visible) {
outline: none;
}
/* Optional: Enhanced focus styles for keyboard users */
.js-focus-visible .focus-visible {
outline: 2px solid #4A90E2;
outline-offset: 2px;
}For frameworks that overwrite classes:
[data-js-focus-visible] :focus:not([data-focus-visible-added]) {
outline: none;
}The polyfill works by:
focus-visible class to elements when keyboard focus is detectedKey behavioral rules:
input[type=text], textarea, etc.) always show focus indicators regardless of interaction methodThe polyfill automatically applies to the main document when the script loads.
/**
* Automatically applies focus-visible polyfill to the document when script loads
* This happens immediately when the script is included
*/
// No function call needed - automatic initialization
// Equivalent to: applyFocusVisiblePolyfill(document)Apply the polyfill to Shadow DOM components manually.
/**
* Applies the focus-visible polyfill to a specific scope
* @param scope - Document or ShadowRoot to apply polyfill to
*/
function applyFocusVisiblePolyfill(scope: Document | ShadowRoot): void;Usage Example:
// Check if polyfill is available
if (window.applyFocusVisiblePolyfill != null) {
// Apply to a shadow root
window.applyFocusVisiblePolyfill(myComponent.shadowRoot);
}Listen for the polyfill ready event when lazy-loading the script.
/**
* Custom event fired when polyfill is loaded and ready
* Event name: 'focus-visible-polyfill-ready'
* Bubbles: false
* Cancelable: false
* Detail: {}
*/
interface FocusVisiblePolyfillReadyEvent extends CustomEvent {
type: 'focus-visible-polyfill-ready';
}Usage Example:
// Listen for polyfill ready event
window.addEventListener('focus-visible-polyfill-ready', () => {
window.applyFocusVisiblePolyfill(myComponent.shadowRoot);
}, { once: true });
// Lazy load the polyfill script
const script = document.createElement('script');
script.src = '/path/to/focus-visible.min.js';
document.head.appendChild(script);/* Added to document.documentElement when polyfill is active */
.js-focus-visible { }
/* Added to focused elements when keyboard focus is detected */
.focus-visible { }Important: If developers manually add the focus-visible class to an element, it will not be removed on blur. Only classes added by the polyfill (marked with data-focus-visible-added) are automatically managed.
<!-- Added to document.documentElement or shadow host when polyfill is active -->
<html data-js-focus-visible>
<!-- Added to focused elements when polyfill adds .focus-visible class -->
<button data-focus-visible-added>Button</button>/* Hide default focus for mouse users only when polyfill is active */
.js-focus-visible :focus:not(.focus-visible) {
outline: none;
}
/* Optional: Enhanced focus styles for keyboard users */
.js-focus-visible .focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
}For frameworks that overwrite CSS classes:
[data-js-focus-visible] :focus:not([data-focus-visible-added]) {
outline: none;
}
[data-focus-visible-added] {
outline: 2px solid #0066cc;
outline-offset: 2px;
}For environments where the polyfill might not be loaded:
/* Provide default focus styles */
button:focus {
outline: 2px solid #0066cc;
}
/* Hide focus for mouse users when polyfill is active */
.js-focus-visible button:focus:not(.focus-visible) {
outline: none;
}The following input types always receive the focus-visible class regardless of interaction method:
input[type="text"]input[type="search"]input[type="url"]input[type="tel"]input[type="email"]input[type="password"]input[type="number"]input[type="date"]input[type="month"]input[type="week"]input[type="time"]input[type="datetime"]input[type="datetime-local"]textarea elementscontenteditable attribute/**
* Global function available when polyfill is loaded
*/
declare global {
interface Window {
applyFocusVisiblePolyfill: (scope: Document | ShadowRoot) => void;
}
}
/**
* Supported scopes for polyfill application
*/
type PolyfillScope = Document | ShadowRoot;
/**
* Input types that always trigger focus-visible class regardless of interaction method
*/
interface InputTypesAllowlist {
text: true;
search: true;
url: true;
tel: true;
email: true;
password: true;
number: true;
date: true;
month: true;
week: true;
time: true;
datetime: true;
'datetime-local': true;
}
/**
* CSS classes added by the polyfill
*/
interface FocusVisibleClasses {
/** Added to document element when polyfill is active */
'js-focus-visible': string;
/** Added to focused elements during keyboard navigation */
'focus-visible': string;
}
/**
* Data attributes added by the polyfill
*/
interface FocusVisibleAttributes {
/** Added to document/host element when polyfill is active */
'data-js-focus-visible': string;
/** Added to elements when polyfill adds focus-visible class */
'data-focus-visible-added': string;
}