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.

4 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…

It’s working on the init of the page, but not with an action:

In the spec and Firefox, there’s a FocusOptions.focusVisible to control this, see:

1 Like