Put a (Focus) Ring Around it

How to overcome the differences between React and Vue for adding Focus Rings to your project!
4 min read
Anthony Antoine
Put a (Focus) Ring Around it

If you have visited the Mirahi website before, you may have noticed a ring displayed around the element in focus. That is thanks to a cool open-source React library called Focus Rings, made by the fine folks at Discord.
They created that library to improve the experience of users navigating with their keyboard, and we used that library on our website for the exact same reason.
Frankly, we like it so much, and its API is so easy to use, that we think it should be used on every single one of our projects.

The problem is, of course, that the library was made for React, and not every project we work on is made with that technology. Since we tend to use Vue and Nuxt a lot, it only made sense for me to fill the void and build my own version of Focus Rings, this time in Vue.

Even if the code to make it work would obviously be different, I wanted to follow the same core API and offer the same components as its React counterpart.

How it works

Wrap your main content with RingScope, ideally as high in the chain as possible. This is the component in charge of drawing the focus ring.

Wrap the component you want highlighted with a FocusRing, while in the scope of RingScope, and when it gets focus, it will let RingScope know and we will have a nice ring around it.

But now what about this way smarter, more useful component?

Wrrrong!

The issue

That will not work as is. You see, just like in React with the concept of React.Children, Vue has a way to access the children of a given component with the useSlots() composable.

This would give us the underlying HTML element of the first child of a default/unnamed slot. Quite practical, with one tiny caveat: the el property on a child will be undefined if any of its own children uses its own slot, or contains text interpolation. Which, if we are honest, is around 95% of our components.

So it appears I could not have the same API after all...
Lucky for me, the React FocusRing component has one of its props I can use: focusTarget. It was initially made to be used with its sibling, ringTarget, for use cases where the HTML element you want highlighted is not the same as the one triggering the focus. Just pass it the correct refs, and voilà! I could use that same prop to let my version of the FocusRing know which element is its target in case of dynamic content, while still having the "pure", prop-less component target its first child by default. That would look like this:

Corrrect!

Now of course, it can be used in conjunction with another prop if we have that same use case I wrote about earlier:

The input triggers the focus, the whole component is highlighted.

But now, when in its original version, you simply had to wrap your desired element/component with FocusRing and be done with it, in the Vue version, you would also have to create a ref, place it on the target element, and pass it as prop to FocusRing.

That's three whole additional steps, four steps in total. That doesn't sit right with me.

Enter useFocusRing.

With its composition API, Vue now allows us to have even more flexibility than before! With this, I could write a composable that will give us a ref that we can simply place on the desired element, like this:

So now that's only two steps! Much better. And it can also give us a container ref if we need one, or we could pass it our own refs, in case they are needed for something else and you don't want ref duplication (why would you?!).

The Vue Focus Ring library is still a work in progress and can be checked out here.