I've been working on composite widgets for quite a while now ( https://reakit.io/docs/composite ).

Components that can be built with it include Combobox, Toolbar, Calendar etc.

It has lots of interesting implementation details that I'd like to share in this thread. 👇
A composite widget is a collection of elements that behave as a single unit. It exists as a single tab stop on the page, but may contain several focusable items, which are usually navigable with arrow keys.
The Google Docs toolbar is a composite widget.

One great benefit of this approach is having fewer tab stops on the page, so keyboard users can navigate through the elements more quickly.

Imagine having to tab through all the toolbar items to reach the next element on the page.
The Google search field is another composite widget, more specifically a combobox. It has a text box with focusable suggestions. You can use Tab to navigate in and out of the whole widget, and arrow keys to navigate through the focusable elements inside it.
Arrow keys are usually commands that perform actions on screen readers. On NVDA, for example, up/down arrow keys navigate between elements on the page, and left/right arrow keys navigate between characters in a text.
By using a composite role, you're telling screen readers that keystrokes shouldn't be interpreted as commands. Instead, when a composite widget gets focus, the screen reader will enter focus mode and transfer all keystrokes to JavaScript.
This means that the developer is responsible for implementing keyboard navigation. You can implement it using one of these two strategies: roving tabindex and aria-activedescendant.

A toolbar normally uses roving tabindex. A combobox uses aria-activedescendant.
You need to listen to keydown events so you can programmatically move focus to the next/previous element in the composite widget in response to arrow keystrokes.
The element that currently has focus will always have tabindex="0", whereas others will have tabindex="-1". This will ensure that the last focused item will be remembered when tabbing back into the composite widget.
Moving this secondary "virtual" focus consists of changing the value of the aria-activedescendant attribute on the element that has DOM focus. This is basically telling assistive technologies that there's an additional element with focus.
Screen readers will announce the active descendant element while you can still interact with the document.activeElement.

For example, in a combobox, you can still type on the text box while you navigate through the suggestions.
When using aria-activedescendant, as you only need to update the attribute, the navigation is usually implemented without actually focusing on the descendant elements.

In fact, we can’t call element.focus() on them as this would remove DOM focus from the composite element.
Because the active descendants don't get real focus, things you would get for free, such as scroll into view and focus/blur events on the items, won't work out of the box.

Scrolling into view is especially important if descendants are rendered within a scrollable container.
There's the element.scrollIntoView method ( https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView), but it alone doesn't help us in this case. We need something like scrollIntoViewIfNeeded, for which implementation is rather complex.

And this is where we start talking about the Reakit Composite module.
By default, it uses the roving tabindex method. Changing it to aria-activedescendant is as simple as setting the (still experimental) `virtual` state on the state hook.

```
useCompositeState({ unstable_virtual: true });
```

The rest of the API remains exactly the same.
The Composite component doesn't necessarily need to be the container. For example, to create a combobox, you would render it as a separate element.

https://carbon.now.sh/?bg=rgba(171%2C%20184%2C%20195%2C%201)&t=dracula-pro&wt=none&l=jsx&ds=true&dsyoff=20px&dsblur=68px&wc=true&wa=true&pv=56px&ph=56px&ln=false&fl=1&fm=Fira%20Code&fs=14px&lh=133%25&si=false&es=2x&wm=false&code=import%2520%257B%2520useCompositeState%252C%2520Composite%252C%2520CompositeItem%2520%257D%2520from%2520%2522reakit%2522%253B%250A%250A%252F%252F%2520Simplified%2520version%2520of%2520a%2520combobox%2520component%250Afunction%2520Combobox()%2520%257B%250A%2520%2520const%2520composite%2520%253D%2520useCompositeState(%257B%2520unstable_virtual%253A%2520true%2520%257D)%253B%250A%2520%2520return%2520(%250A%2520%2520%2520%2520%253Cdiv%253E%250A%2520%2520%2520%2520%2520%2520%253CComposite%2520%257B...composite%257D%2520role%253D%2522combobox%2522%2520%252F%253E%250A%2520%2520%2520%2520%2520%2520%253Cdiv%2520role%253D%2522listbox%2522%253E%250A%2520%2520%2520%2520%2520%2520%2520%2520%253CCompositeItem%2520%257B...composite%257D%2520role%253D%2522option%2522%253EApple%253C%252FCompositeItem%253E%250A%2520%2520%2520%2520%2520%2520%2520%2520%253CCompositeItem%2520%257B...composite%257D%2520role%253D%2522option%2522%253EOrange%253C%252FCompositeItem%253E%250A%2520%2520%2520%2520%2520%2520%2520%2520%253CCompositeItem%2520%257B...composite%257D%2520role%253D%2522option%2522%253EBanana%253C%252FCompositeItem%253E%250A%2520%2520%2520%2520%2520%2520%253C%252Fdiv%253E%250A%2520%2520%2520%2520%253C%252Fdiv%253E%250A%2520%2520)%253B%250A%257D
The state object exposes methods so you can programmatically control the state of the UI. Here's an example (both Toolbar and Menu components use Composite underneath): https://twitter.com/diegohaz/status/1288087268222214144
One of the premises of the library is to avoid introducing custom event props (eg. onSelect) whenever possible.

Selecting an item is essentially a click action, whether it's triggered by mouse, keyboard or touch. You should be able to use the onClick prop.
If you want to respond to focus/blur events on composite items, whether it's triggered by arrow keys or as a result of mouse/touch down events, you should be able to use onFocus/onBlur props just like you would with a native HTML element.
You get all those behaviors for free when using roving tabindex since items will get DOM focus.

But on Reakit, you also get that with aria-activedescendant. The component API is exactly the same.
This API consistency is important. If you have used any composite component from Reakit before, like Toolbar or Menu, you'll find other components quite familiar.
How would you respond to keystrokes on a combobox option?

If you were using roving tabindex, the suggestion element would get DOM focus and, therefore, any keystroke would trigger a keydown event on it. So, you could just pass an onKeyDown prop to it.
Since the combobox uses aria-activedescendant, the suggestion elements don't receive real DOM focus. So, onKeyDown on them wouldn't work.

You would have to learn a different way to do something that you might have already done on other composite components, like Toolbar or Menu.
That's why Reakit allows you to pass event props to composite items even when they're using aria-activedescendant.

This is possible because the composite component will forward the events to the composite items using element.dispatchEvent.

https://carbon.now.sh/?bg=rgba(171%2C%20184%2C%20195%2C%201)&t=dracula-pro&wt=none&l=jsx&ds=true&dsyoff=20px&dsblur=68px&wc=true&wa=true&pv=56px&ph=56px&ln=false&fl=1&fm=Fira%20Code&fs=14px&lh=133%25&si=false&es=2x&wm=false&code=%253CCompositeItem%2520onKeyDown%253D%257BremoveItemIfMetaD%257D%2520%252F%253E
What about scroll into view?

As you can see on the GIF, moving virtual focus to an option outside of the container viewport area properly handles scroll position.

And I assure you that no complex scrollIntoViewIfNeeded polyfill was used in this process.
The "secret" here is that element.focus() is actually called on the composite item. When it receives DOM focus, it immediately moves focus back to the composite container.

The item is focused for a fraction of time; it's imperceptible but enough to handle scroll position.
This is a rather simple technique that gives us not only scroll handling for free, but also allows us to pass an onFocus prop to the composite item and it'll get properly called.
What about onBlur?

By immediately returning focus to the composite container, the onBlur prop you passed to the composite item would be called right after onFocus, even though the element is still virtually focused.
These intermediate events are suppressed internally using event.preventDefault and event.stopPropagation.

When the virtual focus leaves the item (eg. by moving to another item), the blur event is programmatically dispatched so you get it at the right time.
Composite widgets are very powerful when used properly. They can drastically reduce the number of tab stops on the page and improve accessibility on complex web apps.
In this thread I talked only about technical aspects and implementation details. But it's also worth looking into the semantics of each composite role so you don't end up making mistakes.

Start with the WAI-ARIA docs and check the subclass roles: https://www.w3.org/TR/wai-aria-1.1/#composite
You can follow @diegohaz.
Tip: mention @twtextapp on a Twitter thread with the keyword “unroll” to get a link to it.

Latest Threads Unrolled: