Front-End solution: Eyebrow heading dots

Learn how anchor positioning is really useful for a solution other than for positioning popovers.


In the challenge I was stuck with a suboptimal solution to little decorative dots. If you haven’t already read that, do that first. I’ll wait.

Right, you’re up to speed, let’s dig in.

A call to arms permalink

I was really impressed with the efforts folks contributed to this challenge, so I’ll list a couple of my favourites.

Thank you, as always, to the folks who contributed.

The secret is in the newest CSS permalink

Roma linked me up with an article about the shrink-wrap problem they wrote. The solution was right there, in these further examples.

It’s a really clever solution because the anchor positioning system allows us to stick the start or the end of the dots to each side of the text, providing that much sought-after effect that I was after in the original challenge.

I’ve not read up much anchor position stuff quite yet because in my mind, it was a popover thing. How wrong was I?!

I riffed off Roma’s approach for the solution to this Front-End Challenges Club permalink

We’re using slightly different HTML this time. I wanted no extra elements, but that seems like an impossibility at this point. That’s cool because you gotta let go of these things to progress. It’s better to be flexible when working with the web.

Code language
html
<h2 class="eyebrow">
  <span>Some reasonably long text goes here</span>
</h2>

One less <span> at least and most importantly, no empty elements, which I did have in my last attempt in the challenge.

Let’s now move on to CSS and start on the .eyebrow block.

Code language
css
.eyebrow {
  --eyebrow-decoration-gutter: 1ch;

  font-size: var(--size-step-0);
  font-weight: 500;
  text-transform: uppercase;
  text-align: center;
  text-wrap: balance;
  font-feature-settings: "cpsp" on;
  max-width: unset;

  /* This is the relative parent for the dots */
  position: relative;
}

We start by determining what the gap between dots and text should be. I’ve opted for a nice 1ch but it can be whatever you like.

Skipping past the visual typography settings, we set the position as relative. The dots will use this as their relative parent where anchor support is available later.

Code language
css
.eyebrow > span {
  position: relative;
  display: inline-block;
}

To handle browsers with no anchor support, we are building with progressive enhancement. By setting the <span> as a relative parent, we can fake anchoring with absolute positioning later.

Code language
css
.eyebrow > span::before,
.eyebrow > span::after {
  content: "";
  display: block;
  aspect-ratio: 1;
  height: 0.7ex;
  background-color: currentColor;
  border-radius: 100%;
  position: absolute;

  /* Attach to the anchor */
  position-anchor: --eyebrow-target;
  
  inset-block-start: 50%;
  transform: translateY(-50%);
}

This is very similar to my original shot at this pattern. We’re creating dots using aspect ratio and border radius, making them the same colour as the rendered text.

The next part, we create an anchor for our dots. We’re not doing this in a @supports because there’s no point. The browser will ignore and carry on because CSS is a declarative programming language.

The last bit I’m not overly enthused about. I wish translate had some logical versions because this pattern would work regardless of writing mode. A refactoring target, I guess.

Code language
css
.eyebrow > span::before {
  inset-inline-start: calc(var(--eyebrow-decoration-gutter) * -2);
}

.eyebrow > span::after {
  inset-inline-end: calc(var(--eyebrow-decoration-gutter) * -2);
}

Again, for browsers with no anchor support, we’re using only absolute positioning to position our anchors. There’s not enough space with the --eyebrow-decoration-gutter, so I’ve multiplied it by minus two to get a double negative version of our custom property. I can live with that, but you might instead prefer to create an --eyebrow-fallback-decoration-gutter or something instead.

Code language
css
@supports (position-anchor: --eyebrow-target) {
  /* Not a relative parent, but an anchoring parent instead */
  .eyebrow > span {
    anchor-name: --eyebrow-target;

    /* Undo the default experience */
    display: revert;
    position: revert;
  }

  /* Anchor the end of the dot to the anchor. Margin inline provides the gutter */
  .eyebrow > span::before {
    inset-inline-start: auto;
    inset-inline-end: anchor(start);
    margin-inline-end: var(--eyebrow-decoration-gutter);
  }

  /* Anchor the start of the dot to the anchor. Margin inline provides the gutter */
  .eyebrow > span::after {
    inset-inline-end: auto;
    inset-inline-start: anchor(end);
    margin-inline-start: var(--eyebrow-decoration-gutter);
  }
}

This is where the fun starts. We first check to see if anchor positioning is available to us. A quick note on @supports: I like to test the actual value we are trying to apply. Feels like a good idea and improves readability as I see it.

The next thing we do is target the span and attach it to the anchor we created earlier with anchor-name: --eyebrow-target;. Following this, we revert the positioning and display rules to the user agent stylesheet value.

Finally, because we have anchor support, we can now utilise it. For the ::before element, we change the positioning values to stitch the end of the dot to the start of the anchor. We do the opposite for the ::after element and stitch the start to the end of the anchor. We re-apply the gutter value with margin too, as it was set with positioning before.

See the Pen My final solution to this challenge by piccalilli (@piccalilli) on CodePen.

Wrapping up permalink

It’s been a bit of a different challenge this time around on Front-End Challenges Club. I hope you’ve enjoyed it! Next time, I promise it’ll be a simpler one.

Again, thank you to everyone who had a go at it!