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)

↑↓ arrow keys to navigate · Click to select

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