Making a Tooltip (with tether arrow) using Popover & Anchor Positioning

We can build a tooltip without any JS thanks to Popover API and Anchor Positioning. We can use clip-path to create a tether arrow that moves with the tooltip.

Demo

Watch on YouTube: "Tooltip with CSS Popover and Anchor Positioning"

Short Version

Here is the codepen of a tooltip built with Popover and Anchor Positioning that moves around an anchor element when it needs more space. It also has a tether arrow that moves with the popover.

Long Version

The Popover API and CSS Anchor Positioning are a great combo for making a tooltip without any JS required (although we can leverage just a little JS as a progressive enhancement, more below).

There are plenty of examples out there of combining the two to make a tooltip. But I want my tooltip to have a little tether arrow to provide a UX hint as to what the tooltip is anchored to.

Tooltip with tether arrow

Unfortunately when the tooltip moves around (thanks to position-try) the tether arrow doesn't move with it... there just isn't any way to mess with the pseudo-elements inside of @position-try.

Tooltip with tether arrow on wrong side

I asked about this on Mastadon and Una replied that it's a known gap in the API (coming someday hopefully).

Fortunately, Temani responded with an idea to use clip-path and margin-box to "hide" the parts of the pseudo-element that we don't want to show. Thankfully the margin can be modified inside of @position-try. I modified his example to add a pseudo-element to the right and left sides as well as top and bottom and now we have a tooltip that moves around with a tether arrow! 💪

You can see the full source code and demo on CodePen. Below is a snippet of the CSS that makes it work along with some comments to explain what's going on.

[popover] {
  --tether-offset: 1px;
  --tether-size: 8px;

  position-anchor: --anchor-btn;
  position: absolute;
  position-area: top;
  position-try: --bottom, --left, --right;

  /* default tooltip is above anchor so we have a bottom margin */
  margin: 0 0 var(--tether-size) 0;

  /* in default position (above anchor) the tether arrows on left, top, and right
   are hidden because we are clipping based on the margin. */
  /* NOTE: the inset here is optional, just a little offest so the arrow
   doesn't touch the anchor*/
  clip-path: inset(var(--tether-offset)) margin-box;

  /* the top and bottom arrows */
  &::before {
    content: "";
    position: absolute;
    z-index: -1;
    inset: calc(-1 * var(--tether-size)) calc(50% - var(--tether-size));
    background: inherit;
    clip-path: polygon(
      0 var(--tether-size),
      50% 0,
      100% var(--tether-size),
      100% calc(100% - var(--tether-size)),
      50% 100%,
      0 calc(100% - var(--tether-size))
    );
  }

  /* the left and right arrows */
  &::after {
    content: "";
    position: absolute;
    z-index: -1;
    inset: calc(50% - var(--tether-size)) calc(-1 * var(--tether-size));
    background: inherit;
    clip-path: polygon(
      0 var(--tether-size),
      var(--tether-size) 0,
      calc(100% - var(--tether-size)) 0,
      100% 50%,
      calc(100% - var(--tether-size)) 100%,
      var(--tether-size) 100%
    );
  }
}

Progressive Enhancement (show on hover)

I also like my tooltip to show/hide when hovered by a mouse, so I add a little extra JS for that.

const popover = document.querySelector("[popover]");
const anchor = document.querySelector("button");

// show popover on mouseenter
anchor.addEventListener("mouseenter", () => {
  popover.showPopover();
});

// hide popover on mouseleave
anchor.addEventListener("mouseleave", () => {
  popover.hidePopover();
});

Hopefully you found this post helpful, if you have any questions you can find me on Twitter.

Conditional Styles with CSS `:has()`