Ability to explicitly set :focus-visible from focus()

Currently when setting focus programmatically to an element with focus(), there is no way to instruct the browser to apply :focus-visible styling to it.

This can be problematic when using HTML-over-the-wire technologies like Hotwire or Htmx, where the focused element may be replaced with a new one, and the new one is given focus. If the initial element was showing a focus ring via :focus-visible styles, that styling will go away for the new element, because the browser has decided that it wasn’t necessary.

focus() currently supports passing an object with preventScroll:true to prevent the browser from scrolling to the focused element. I propose that the object should support an additional forceVisible key, which can be set to true to force :focus-visible styling on the element.

2 Likes

Here’s a related discussion from the :focus-visible polyfill: Should :focus-visible match when returning focus or programmatically focusing? · Issue #88 · WICG/focus-visible · GitHub

I think it kinda shows that it’s not possible for the browser/polyfill to always know the right answer on what to do about :focus-visible, and there are cases where we need to be able to explicitly enable it.

This would be a great addition to the focus() API.

Since a commonly desired behavior in the thread you linked to was to match :focus-visible only if the last user action was not a pointer event, what would you think about adding an easy option for that? Like propagateVisibility: true or changing the new option to visible: 'force'|'propagate'?

By the time focus() needs to be called, the prior element probably doesn’t exist in the DOM anymore so its focus has been lost. So ideally there should also be a way to detect whether :focus-visible is active on the activeElement before the swap takes place. Then apply it to focus() later.

const activeElement = document.activeElement;
const focusVisible = document.focusVisible; // (something like this maybe?)

// do the swap
// ...

// if the active element is no longer in the DOM and a new one exists with the same ID, set focus to it
if (activeElement && activeElement.id && !document.body.contains(activeElement)) {
  const newElement = document.getElementById(activeElement.id);
  if (newElement) {
    newElement.focus({forceVisible: focusVisible});
  }
}

Hm, when I focus an input via the focus method, the :focus-visible style applies. Try my demo.

unless i’m mistaken, text entry <input> elements always show their focus even if you activate them with mouse or touch (i.e. focus-visible always evaluates to true for them regardless). to be more representative, you probably want to use a link or button (where the browser’s focus-visible heuristic kicks in).

just checking in Chrome at least, :focus-visible kicks in for a <button> when focused programmatically.

For links as well: demo

Huh, yeah I’m seeing that too. I’ll need to investigate why it’s not happening in my case…