Multi-Line Exclusion Tabs

Exclusion tabs inspired by Paco using WAAPI and Clip-path. I extend it to support wrapped content with flex-wrap on the parent container.

The typical way of creating exclusive tabs is by using motion with a span element. You move that based on the active item and that creates the smooth effect.

Motion is an extra dependency and CSS is powerful which means we can solve the problem with the extra dep

The trick is to use clip-path to create a mask that reveals only the active tab.


How it works

To animate the clip-path across the horizontal axis, you have to measure how much you have to clip on the left and how much you have to clip on the right.

We add a ref to the activeTabElement and use that to get it's offsets.

const { offsetLeft, offsetWidth } = activeTabElement;
const clipLeft = offsetLeft;
const clipRight = offsetLeft + offsetWidth;

Once you've gotten that, you slot the values into the clip-path. We are basically saying clip everything from the left of the element and everything after the right of the element.

container.style.clipPath = `inset(0 ${Number(100 - (clipRight / container.offsetWidth) * 100).toFixed()}% 0 ${Number((clipLeft / container.offsetWidth) * 100).toFixed()}% round 17px)`;

Emil has a great introduction to the feature.

To make this work for flex-wrap containers, we need to figure out the top and bottom positions as well. With that we can by what percentage we have to clip.

In this case:

const { offsetLeft, offsetWidth, offsetTop, offsetHeight } = activeTabElement
const clipLeft = offsetLeft
const clipRight = offsetLeft + offsetWidth
const clipBottom = offsetTop + offsetHeight
const clipTop = offsetTop

With that, our clip-path looks like this:

const clipBottomValue = Number(
  100 - (clipBottom / container.offsetHeight) * 100).toFixed()

const clipTopValue = Number(
  (clipTop / container.offsetHeight) * 100).toFixed()

const clipRightValue = Number(
  100 - (clipRight / container.offsetWidth) * 100).toFixed()

container.style.clipPath = `inset(${clipTopValue}% ${clipRightValue}% ${clipBottomValue}% ${Number(
  (clipLeft / container.offsetWidth) * 100).toFixed()}% round 17px)`

It wasn't intuitive at first but getting a pen and paper and drawing out a 4/4 rectangle helped simplify it.

Thanks for Emil for the initial prototype.

You can view the full code here.