16

I'm writing a custom web component that is meant to be interactive. How can I tell the browser that this custom component should receive focus?

I wish that my custom element…

  • could be focused (by tab-navigation);
  • could receive keypresses when focused;
  • could be matched by :focus pseudo-selector.

I'm not using any external library, just plain HTML5 APIs.

Denilson Sá Maia
  • 47,466
  • 33
  • 109
  • 111

4 Answers4

13

Based on this demo that I found in this question, I have this answer:

Just add the tabindex attribute to the elements you want to be focusable.

// Add this to createdCallback function:
if (!this.hasAttribute('tabindex')) {
    // Choose one of the following lines (but not both):
    this.setAttribute('tabindex', 0);
    this.tabIndex = 0;
}
// The browser automatically syncs tabindex attribute with .tabIndex property.

Clicking on the element will give it focus. Pressing tab will work. Using :focus in CSS will also work. keydown and keyup events work, although keypress doesn't (but it's deprecated anyway). Tested on Chrome 44 and Firefox 40.

Also note that this.tabIndex returns -1 even if the HTML attribute is missing, but this has a different behavior than setting tabindex="1":

  • <foo></foo>: No tabindex attribute, the element is not focusable.
  • <foo tabindex="-1"></foo>: The element is not reachable through tab-navigation, but it is still focusable by clicking.

References:

Denilson Sá Maia
  • 47,466
  • 33
  • 109
  • 111
9

@Denilson, I would like to provide you with some more information.

As you said, this.tabIndex = 0 works when your webcomponent contains no focusable elements. If it does, it gets more complicated.

For example, if your component contains one or more inputs, then first the "whole" component gets focus, and only later, when tabbing, each inner inputs get focus, one by one. This is usually not what you want. Usually, when the component gets focus this should mean its first input gets focus immediately.

Also, there is a reverse tabbing problem. If your first input has focus and you press SHIFT-TAB, then the "whole" component gets focus, and you are forced to press SHIFT-TAB twice to move to the previous element.

I found this to solve all focus and tabbing problems:

// At first, the component may get focus and accept tabbing.
createdCallback = function () { this.tabIndex = 0; }

// When the component gets focus, pass focus to the first inner element.
// Then make tabindex -1 so that the component may still get focus, but does NOT accept tabbing.
focus = function (e) { firstFocusableInnerElement.focus(); this.tabIndex = -1; }

// When we completely left the component, then component may accept tabbing again.
blur = function (e) { this.tabIndex = 0; }

Note: As of now (Sep 2015) if an inner element gets focus, then the "whole" element is not matched by the :focus pseudo-selector (tested only in Chrome). If find this behavior to be just plain wrong. The focus event was fired, and the blur event was not. So the element should have focus, right? I hope they change this in the future.

Marcelo Glasberg
  • 29,013
  • 23
  • 109
  • 133
  • I suppose this works because when an inner element is focused (or loses focus), the event itself bubbles up, calling your handler. And… What is the behavior if the outer element is not focusable? Will the inner ones also be focusable? Do you have any live demo available anywhere? – Denilson Sá Maia Sep 15 '15 at 10:42
  • Shadow DOM has a feature called “event retargeting” which changes an event’s target as it bubbles up, such that target is always in the receiving element’s light DOM. I.e., events originating from nodes inside of the shadow DOM are retargeted so they appear to come from the shadow host. The focus event of an inner element does not cross the boundary if the element already has focus. – Marcelo Glasberg Sep 15 '15 at 18:29
  • And this Chromium bug: https://code.google.com/p/chromium/issues/detail?id=531790 – Marcelo Glasberg Sep 15 '15 at 18:37
  • Thanks for the comments! To answer my own question: the outer (light DOM) tabindex does not affect the inner (shadow DOM) tabindex. Which means the inner elements are focusable even if the outer one is not. (Now that I think about it, it feels kinda obvious.) – Denilson Sá Maia Sep 23 '15 at 01:33
  • Though this seems vebose and hacky, it's the only thing I've found that works. Thanks – junvar Mar 24 '20 at 14:46
  • Anyone using this, beware that when the focus goes to the child element and you set the web component's tabindex to -1, on firefox, if you press tab, the focus goes (crazy) to a random location. My workaround was to remove the attribute instead. Also make sure to make the child element's tabindex to -1, otherwise, both the web component and the child element will be focusable. – Ian Jun 07 '21 at 15:33
  • Since a while there's been [`delegatesFocus`](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/delegatesFocus) as an additional parameter to `attachShadow` which (in chrome at least) causes the component to keep `:focus` when its children are focused. – LJᛃ Oct 26 '21 at 08:50
3

Short answer: delegatesFocus is what you need here, not tabindex.

Details: Assuming that you have interactive elements inside the shadow DOM, there is no satisfying way to make the component programmatically focusable with tabindex:

  • if you set it to 0 you add the host element to the tab sequence ("sequential keyboard navigation") and you have an extra tab stop
  • if you set it to -1 you remove not only the host element but any interactive element inside its shadow DOM from the tab sequence, so the whole thing becomes inaccessible for keyboard users

There's a web component API just for this: ShadowRoot.delegatesFocus, see here. Set this to true and you'll get:

  • calling .focus() on the host or clicking on any non focusable part of the component focuses the first focusable element in the shadow DOM
  • :focus styles are applied to the host in addition to the focused element within
  • tab sequence is unchanged (it should already work the way you want)

It's supported since shadow DOM v1.

mrtnmgs
  • 1,402
  • 14
  • 26
  • That sounds like you have something like one or multiple `` or buttons inside the shadow DOM, and then you can use `delegatesFocus` to make those descendant elements focusable. If that's what you mean, if that's what this option does… Then it sounds like a great solution. (In my case, however, I wanted my own custom component to be focusable, not any children.) Can you please clarify? Can you also add a demo example? That MDN page is very sparse. – Denilson Sá Maia Sep 29 '22 at 09:37
  • Yes, in the `delegatesFocus` you have interactive element in the shadow DOM. I assumed that's what you were doing. So you want the host element itself to be focusable, but there is no focusable element in the shadow DOM? What were you building exactly? Because in that case you would probably need to give your component an ARIA role, so that assistive technology devices know what to do with it. – mrtnmgs Oct 03 '22 at 13:13
  • I wrote https://github.com/denilsonsa/html5-knob many years ago, and haven't touched it since. Indeed, it's missing ARIA attributes, as I wasn't familiar with them. – Denilson Sá Maia Oct 05 '22 at 13:14
-5

One very pragmatic approach I use, if possible and suitable, is just to put a <button type='button'> around my custom element.
This maybe does not fit as solution for you, I mention it anyway for others stepping into this question / problem.

It handles all focus matters, including a focus rectangle an so on.

To tame a <button> is less work than it seems (think especially about the line-height the button changes)

halfbit
  • 3,773
  • 2
  • 34
  • 47