v145 · DOM · Keyboard vs. Mouse Pattern
Keyboard vs. Mouse Pattern
A navigation menu that moves focus programmatically using arrow keys. When the keyboard triggers the focus movement, focusVisible: true ensures the focus ring is shown. When the mouse clicks a different item, focusVisible: false suppresses the ring — exactly matching what a user expects from each input modality.
Chrome 145 required for the
focusVisible option. This page checks the observable result by calling focus() and then reading whether the newly focused element matches :focus-visible.
Live focusVisible option probe
Checking focusVisible support…
- activeElement: pending
- :focus-visible: pending
- last option: pending
Run a focus option to see the exact result.
navigation menu demo
Click an item to select it (no focus ring), or use arrow keys to move between items (ring appears). The log shows which input method triggered each focus call.
Menu (click or use ↑↓ arrow keys)
Focus log
Interact with the menu…
code
const items = [...menu.querySelectorAll('.nav-link')];
let currentIndex = 0;
// ↑↓ key navigation: focusVisible: true (keyboard)
menu.addEventListener('keydown', e => {
if (e.key === 'ArrowDown') {
e.preventDefault();
currentIndex = (currentIndex + 1) % items.length;
items[currentIndex].focus({ focusVisible: true });
// ^^^^^^ always show ring
}
if (e.key === 'ArrowUp') {
e.preventDefault();
currentIndex = (currentIndex - 1 + items.length) % items.length;
items[currentIndex].focus({ focusVisible: true });
}
});
// Click: focusVisible: false (mouse — no ring needed)
items.forEach((item, i) => {
item.addEventListener('mousedown', e => {
e.preventDefault(); // prevent browser default focus before our call
currentIndex = i;
item.focus({ focusVisible: false });
// ^^^^^ suppress ring for mouse
});
});
before Chrome 145
// Without focusVisible, developers had to track pointer type manually:
let lastInput = 'keyboard';
window.addEventListener('pointerdown', () => { lastInput = 'mouse'; });
window.addEventListener('keydown', () => { lastInput = 'keyboard'; });
function moveFocus(el) {
el.focus(); // browser guesses from lastInput — often wrong
// Then add/remove a class to control outline via CSS:
if (lastInput === 'keyboard') {
el.classList.add('show-ring');
} else {
el.classList.remove('show-ring');
}
}
// With Chrome 145: just pass focusVisible
function moveFocus(el, fromKeyboard) {
el.focus({ focusVisible: fromKeyboard });
// No extra class, no input tracking, no CSS workaround
}
see also
- Focus Ring Control — direct comparison of all three options
- ChromeStatus entry
- MDN — HTMLElement.focus()