Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Mon, 10 Nov 2025 19:45:55 +0000 en-US hourly 1 https://wordpress.org/?v=6.8.3 225069128 Perfectly Pointed Tooltips: To The Corners https://frontendmasters.com/blog/perfectly-pointed-tooltips-to-the-corners/ https://frontendmasters.com/blog/perfectly-pointed-tooltips-to-the-corners/#respond Mon, 10 Nov 2025 15:04:33 +0000 https://frontendmasters.com/blog/?p=7714 Ready for the last challenge?

We are still creating tooltips that follow their anchors, and this time we will work with new positions and learn new tricks. I will assume you have read and understood the first two parts, as I will skip the things I already explained there. Fair warning, if you haven’t read those first two you might get a little lost.

Article Series

At the time of writing, only Chrome and Edge have full support of the features we will be using.

As usual, a demo of what we are making:

This time, instead of considering the sides, I am considering the corners. This is another common pattern in the tooltip world. The code structure and the initial configuration remain the same as in the previous examples, so let’s jump straight into the new stuff.

Defining The Positions

If you took the time to explore my interactive demo, you already know the position we will start with:

position-area: top left;

The other positions will logically be top rightbottom left, and bottom right. We already learned that defining explicit positions is not the ideal choice, so let’s flip!

The flipped values are:

position-try-fallbacks: flip-inline, flip-block, flip-block flip-inline; 

The advantage of this configuration is that we are not using flip-start, so we can safely define min-width (or max-height) without issue. The drawback is that adding the tail is complex. It needs to be placed on the corners, and the margin trick won’t work. We need another hack.

Notice how I am using margin instead of inset to control the gap between the tooltip and the anchor. Both are correct, but you will see later why margin is slightly better in my use case.

Adding The Tail

In the previous examples, the logic is to draw a shape with all the tails, then hide the non-needed parts. The tail has the same color as the tooltip and is placed behind its content, so we can only see what is outside the boundary of the tooltip.

This time, I will use a slightly different idea. I am still drawing a shape with all the tails, but the hiding technique will be different.

First, we place the pseudo-element of the tooltip above the anchor. Not on the top of it, but both will overlap each other.

#tooltip::before {
  content: "";
  position: fixed;
  position-anchor: --anchor;
  position-area: center;
  width:  anchor-size(width);
  height: anchor-size(height);
}

I am using a fixed position to be able to “see” the anchor (we talked about this quirk in the first article). Then, I place the element in the center area, which means above the anchor element (or below it depending on the z-index).

I am introducing a new function, anchor-size(), which is part of the Anchor Positioning API. We saw the anchor() function, which allows us to query the position from an anchor element. anchor-size()does the same but with the sizes. I am using it to make the pseudo-element have the same size as the anchor. It’s like using width: 100% where 100% refers to the anchor.

Nothing fancy so far. We have a square behind the anchor.

Let’s increase the size a little so it also touches the tooltip. We add twice the gap defined by the variable --d plus the value of --s, which controls both the radius and the size of the tooltip.

#tooltip {
  --d: .5em; /* distance between anchor and tooltip */
  --s: .8em; /* tail size & border-radius */ 
}

#tooltip:before {
  width:  calc(anchor-size(width) +  2*(var(--d) + var(--s)));
  height: calc(anchor-size(height) + 2*(var(--d) + var(--s)));
}

It seems we are going nowhere with this idea but, believe me, we are almost there.

Now we sculpt the pseudo-element to have the shape of a tail on each corner, like illustrated below:

Illustration showing a blue square transitioning into a tooltip design with four symmetrical tails around a centered anchor icon.

I am using a somewhat verbose clip-path value to create the final shape but the method used is not particularly important. You can consider gradients, SVG background, the new shape() function, etc. Perhaps you would also like to have a different design for the tails. The main idea is to have four tails around the anchor.

Do you start to see the tricks? We have the correct position for the tails (you can drag the anchor and see the result), but we still have to hide the extra ones.

All we need is to add one line of code to the tooltip:

clip-path: inset(0) margin-box;

I know it’s not very intuitive but the explanation is fairly simple. Even if the pseudo-element is using a fixed position and has lost its relation with the tooltip, it remains part of its content, so clipping the tooltip will also affect the pseudo-element.

In our case, the clip-path will consider the margin box as its reference to create a basic rectangle using inset(0) that will show only what is inside it. In other words, anything outside the margin area is hidden.

Toggle the “debug mode” in the demo below and you will see a black rectangle that illustrates the clip-path area.

Only one tail can fit that rectangle, which is perfect for us!

This trick sounds cool! Can’t we apply it to the previous demo as well?

We can! This series of articles could have been one article detailing this trick that I apply to the three examples, but I wanted to explore different ideas and, more importantly, learn about anchor positioning through many examples. Plus, it’s always good to have various methods to achieve the same result.

What about trying to redo the previous example using this technique? Take it as homework to practice what you have learned through this series. You will find my implementation in the next section.

More Examples

Let’s start with the previous demos using the new technique. As usual, you have the debug mode to see what’s going on behind the scenes.

I will conclude with one final example for you to study. You can also try to implement it before checking my code if you want another challenge.

And a version with a curved tail:

Conclusion

I hope you enjoyed this article series. Our goal was to leverage modern CSS to create common tooltip patterns, while also exploring the powerful Anchor Positioning API. It’s one of those modern features that introduce new mechanisms into the CSS world. We are far from the era where we simply define properties and see a static result. Now we can link different elements across the page, create conditional positioning, define a dynamic behavior that adjusts to each situation, and more!

This feature is only at its Level 1. The Level 2 will introduce even more ideas, one of which is the ability to query the fallback positions and apply a custom CSS. Here is one of the previous demos using this future technique:

The code is probably more verbose, but it feels less hacky and more intuitive. I let you imagine all the possibilities you can do with this technique.

Article Series

]]>
https://frontendmasters.com/blog/perfectly-pointed-tooltips-to-the-corners/feed/ 0 7714
Perfectly Pointed Tooltips: All Four Sides https://frontendmasters.com/blog/perfectly-pointed-tooltips-all-four-sides/ https://frontendmasters.com/blog/perfectly-pointed-tooltips-all-four-sides/#comments Mon, 03 Nov 2025 16:15:35 +0000 https://frontendmasters.com/blog/?p=7543 Time for part two! We’ve got really nice functional positioned tooltips already, but they were mostly concerned with “pointing” up or down and shifting at the edges to avoid overflow. Now we’re going to take it further, considering four positions without shifts.

Article Series

At the time of writing, only Chrome and Edge have full support of the features we will be using.

Here is a demo of what we are making:

Drag the anchor and see how the tooltip switches between the four positions and how it remains centered relatively to the anchor.

The Initial Configuration

We are going to use the same code structure as in the first part. We start with the tooltip placed above the anchor (the “top”).

<div id='anchor'></div>
<div id='tooltip'></div>
#anchor {
  anchor-name: --anchor;
}
#tooltip {
  --d: 1em; /* distance between tooltip and anchor */

  position: absolute; 
  position-anchor: --anchor;
  position-area: top;
  bottom: var(--d);
}

From here on, things will be different from the previous example.

Defining Multiple Positions

The position-try-fallbacks property allows us to define multiple positions. Let’s try the following:

position-try-fallbacks: bottom, left, right;

Let’s not forget that the placement is related to the containing block, which is the body in our example (illustrated with the dashed border):

We almost have the same behavior as the first example; however if you are close to the right or left edges, you get the new positions. Instead of overflowing, the browser will swap to the right or left position.

Illustration showing a tooltip following an anchor, with a crossed-out example on the left and a correct behavior on the right, displaying the text 'Drag the anchor and I should follow...'

Similar to the first example, the gap disappears when switching to the fallback positions. We know how to fix it! Instead of explicitly defining the positions, we can rely on the “flip” feature.

To move from top to bottom, we use flip-block:

position-try-fallbacks: flip-block, left, right;

From top to left, we use flip-start:

position-try-fallbacks: flip-block, flip-start, right; 

The flip-block value mirrors the position across the horizontal axis, and flip-start does the same across the diagonal. With this value, we can move from top to left and from bottom to right. And logically, we also have a flip-inline that considers the vertical axis to move from left to right.

But how do we move from top to right? We are missing another value, right?

No, we have all the necessary values. To move from top to right, we combine two flips: flip-block to move to the bottom, then flip-start to move to the right:

position-try-fallbacks: flip-block, flip-start, flip-block flip-start;

Or flip-start to move to the left, and then flip-inline to move to the right:

position-try-fallbacks: flip-block, flip-start, flip-start flip-inline;

It should be noted that all the flips consider the initial position defined on the element and not the previous position defined on position-try-fallbacks or the current position. If we first perform a flip-block to move to the bottom, the flip-start of the second position will not consider the bottom position but the top position (the initial one). This can be confusing, especially when you have many positions.

Said differently, the browser will first transform all the flips into positions (considering the initial position) and then pick the suitable one when needed.

Disabling the Shift Behavior

What we have is actually good and might work perfectly for some use-cases, but we’re aiming for slightly more advanced functionality. What we want is to flip to the left or right position as soon as the tooltip touches the edges. We don’t want to have the “shift” behavior. I want the tooltip to remain always centered relatively to the anchor.

Image showing four tooltip positions in relation to an anchor, with text indicating interaction.

For this, we can use:

justify-self: unsafe anchor-center;

What is this strange value!?

After defining the position of an element using position-area we can also control its alignment using justify-self and align-self (or the shorthand place-self). However, we get a default alignment that you rarely need to change.

For position-area: top, the default alignment is equivalent to justify-self: anchor-center and align-self: end.

Don’t we have a center value? Why is it called anchor-center?

The center value exists, but its behavior is different from anchor-center. The center value considers the center of the area, while anchor-center considers the center of the anchor in the relevant axis.

Here is a screenshot taken from my interactive demo, where you can see the difference:

Comparison of element alignment in CSS, showing the difference between centering in the top area versus centering at the anchor point.

In addition to that, anchor-center follows the logic of safe alignment which cause the shift behavior. When there is not enough room for centering, the element will shift to remain within the containing block area. To disable this, we tell the browser to consider an “unsafe” behavior hence the use of:

justify-self: unsafe anchor-center;

Here is a demo with only the top and bottom positions. Notice how the tooltip will overflow from the left and right sides instead of shifting.

And if we add back the left and right positions to the fallbacks, the browser will use them instead of overflowing!

It should be noted that justify-self is also included in the flip. It’s one of those properties that the browser changes when flipping. When the position is top or bottom, it remains justify-self, but when the position is left or right, it becomes align-self. Another reason why it’s better to consider the flip feature instead of explicitly defining a position.

Adding min-width

The position of the tooltip is now good, but in some particular cases, it’s too narrow.

A tooltip with a blue background displaying the text 'Drag the anchor and I should follow...' is positioned above a gray anchor icon.

That’s a logical behavior since the text inside can wrap to make the tooltip fit that position. You probably want to keep that behavior, but in our case, we’d like to add min-width to force it to flip to another position before shrinking too much. It can also be a max-height as well.

Oops, min-width is not preventing wrapping, but it is increasing the height! What?!

Can you guess what the issue is? Think a moment about it.

It’s the flip behavior.

The min-width and all the sizing properties are also affected by the flip. The initial configuration is top, so defining min-width means that when we perform a flip-start to move to the left or the right position, the min-width becomes min-height, which is not good.

So we define min-height instead, when flipped it becomes min-width!

Yes, but the min-height will apply to the top and bottom positions, which is not ideal either.

We can fix this by using custom positions where we define all the properties manually.

#tooltip {
  min-width: 10em;

  position-area: top;
  justify-self: unsafe anchor-center;
  bottom: var(--d);
  position-try-fallbacks: flip-block,--left,--right;
}
@position-try --left {
  position-area: left;
  justify-self: normal;
  align-self: unsafe anchor-center;
  right: var(--d);
}
@position-try --right {
  position-area: right;
  justify-self: normal;
  align-self: unsafe anchor-center;
  left: var(--d);
}

We use @position-try to create a custom position with a given name, and inside it we define all the properties. Instead of using flip-start to set the left position, I define a custom --left position with all the necessary properties to correctly place the tooltip on the left. Same for the right position. In this situation, min-width is preserved for all positions, as we are no longer using flip-start.

It is worth noting that when using a custom position, you need to ensure that you override all the properties of the initial position defined on the element otherwise they still apply. For this reason, I am defining justify-self: normal to override justify-self: unsafe anchor-centernormal being the default value of justify-self.

While this solution works fine, it’s a bit verbose, so I was wondering if we can do better. It turns out we can!

We can combine the flip feature and custom positions to get a shorter code:

#tooltip {
  position-area: top;
  justify-self: unsafe anchor-center;
  bottom: var(--d);
  position-try: flip-block,--size flip-start,--size flip-start flip-inline;
}
@position-try --size {
  min-height: 12em; /* this is min-width! */
}

When we define a custom position with a flip, the browser selects the properties within the custom position, as well as the properties already defined on the element, and then performs the flip. So --size flip-start will flip the properties defined on the element and the one defined in the custom position --sizemin-height becomes a min-width! Clever, right?

But you said we cannot use min-height?

We cannot use it on the main element as it will apply to the top and bottom positions. However, within a custom position, I can select where it applies, and I want it to apply only to the left and right positions. Plus, I don’t need any min-width or min-height constraint when the position is top or bottom.

Now our tooltip position is perfect! Let’s add the tail.

Adding The Tail

First, we create a shape that contains the 4 tails.

Comparison of tooltip shapes demonstrating the transition from a red diamond shape to a blue rounded shape with the text 'Drag the anchor and I should follow...'
#tooltip:before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: calc(-1*var(--d));
  clip-path: polygon(
    calc(50% - var(--s)) var(--d),50% .2em,calc(50% + var(--s)) var(--d),
    calc(100% - var(--d)) calc(50% - var(--s)), calc(100% - .2em) 50%,calc(100% - var(--d)) calc(50% + var(--s)),
    calc(50% + var(--s)) calc(100% - var(--d)),50% calc(100% - .2em),calc(50% - var(--s)) calc(100% - var(--d)),
    var(--d) calc(50% + var(--s)), .2em 50%,var(--d) calc(50% - var(--s))
  );
}

Then we control it using margin on the tooltip element, just as we did in the first part. When the position is top, we add a margin to all the sides except for the bottom one:

margin: var(--d);
margin-bottom: 0;
Comparison of tooltip designs showing a red diamond-shaped tooltip on the left and a blue rectangular tooltip on the right, both displaying the text 'Drag the anchor and I should follow...'.

And for the other sides, we do nothing! The flip will do the job for us.

Toggle the “debug mode” to see how the shape behaves in each position.

Conclusion

We have completed the second part. Now, you should be comfortable working with fallbacks, the flip feature, and custom positions. If you are still struggling, give the article another read. We still have one final challenge, so make sure everything is clear before moving to the next article.

Article Series

]]>
https://frontendmasters.com/blog/perfectly-pointed-tooltips-all-four-sides/feed/ 2 7543
Perfectly Pointed Tooltips: A Foundation https://frontendmasters.com/blog/perfectly-pointed-tooltips-a-foundation/ https://frontendmasters.com/blog/perfectly-pointed-tooltips-a-foundation/#comments Tue, 28 Oct 2025 16:51:29 +0000 https://frontendmasters.com/blog/?p=7514 Tooltips are a classic in web development. You click on an element, and a small bubble appears to display additional details. Behind that simple click, there tends to be JavaScript performing calculations to determine the correct position for the tooltip. Let’s try to place it at the top. Nope, not enough space. Let’s try the bottom instead. It’s also touching the right edge so let’s shift it a bit to the left. There is a lot that can go into making sure a tooltip is placed well without any cutoffs losing information.

In this article, I will show you how to write good JavaScript that covers all the possibilities…

Kidding! We’ll be using CSS and I will show how the modern anchor positioning API can help us with all this. None of the weight and performance concerns of JavaScript here.

Article Series

At the time of writing, only Chrome and Edge have full support of the features we will be using.

Let’s start with a demo:

Click-and-drag the anchor and see how the tooltip behaves. It will try to position itself in a way to remain visible and avoid any overflow. Cool, right? No JavaScript is used to position the tooltip (except the one for dragging the anchor, which is irrelevant to the trick).

This is made possible thanks to the new Anchor Positioning API and a few other tricks we will dissect together. We will also study more examples, so if you are new to anchor positioning, you are in the right place.

The Initial Configuration

Let’s start with the markup: An anchor element and its tooltip:

<div id='anchor'></div>
<div id='tooltip'></div>

This isn’t interesting HTML, but it does showcase how the anchor and the tooltip are different elements that don’t need to be parent/child. They can be anywhere in the DOM and the CSS can handle that (though, for practical and accessibility reasons, you may want to keep them close together and associate them).

The HTML structure you use will depend on your use case and your type of content, so choose it carefully. In all cases, it’s mainly one element for the anchor and another one for the tooltip.

Here is a demo taken from another article where the anchor is the thumb of a slider and the tooltip is an <output> element:

The CSS:

#anchor {
  anchor-name: --anchor;
}
#tooltip {
  position: absolute; 
  position-anchor: --anchor;
  position-area: top;
}

We define the anchor using anchor-name, link the tooltip to the anchor using position-anchor (and a custom ident, the --anchor bit that looks like a custom property but is really just a unique name), and then we place it at the top using position-area. The tooltip needs to be absolutely positioned (which includes fixed position as well).

Nothing fancy so far. The tooltip is “always” placed at the top, whatever the anchor’s position. You can drag the anchor to see the result.

In this article we’ll use simple values for position-area, but this property can be very tricky.

A grid layout demo showing various cell configurations and an anchor icon with the text 'CSS Is Awesome' positioned at the top left.
I’ve created an interactive demo if you want to explore all the different values and understand how alignment works in the context of Anchor Positioning.

Now that our tooltip is placed, let’s add a small offset at the bottom to prepare the space for the tail. Using bottom will do the job.

#anchor {
  anchor-name: --anchor;
}
#tooltip {
  --d: 1em; /* distance between tooltip and anchor */

  position: absolute; 
  position-anchor: --anchor;
  position-area: top;
  bottom: var(--d);
}

Making the Position Dynamic

Let’s move to the interesting part, where we will adjust the position of the tooltip to make it always visible and avoid any overflow. Anchor Positioning has native mechanisms to do this, and all we need to do is understand how to use them correctly.

The first thing is to identify the containing block of the absolutely positioned element. We may intuitively think that the logic is to avoid a screen overflow, but that’s not the case. It’s related to the containing block. This can be very confusing if you don’t understand this part, so let’s look closely.

The specification:

Anchor positioning, while powerful, can also be unpredictable. The anchor box might be anywhere on the page, so positioning a box in any particular fashion might result in the positioned box overflowing its containing block or being positioned partially off screen.

To ameliorate this, an absolutely positioned box can use the position-try-fallbacks property to refer to several variant sets of positioning/alignment properties that the UA can try if the box overflows its initial position. Each is applied to the box, one by one, and the first that doesn’t cause the box to overflow its containing block is taken as the winner.

As you can read, it’s all about the containing block, and the containing block of an absolutely positioned element is the first ancestor with a position different from static (the default). If such element doesn’t exist we consider the initial containing block.

In our example, I am going to use the body as the containing block, and I will add a border and an offset from each side to better illustrate:

Drag the anchor to the left or the right and see what happens. When the tooltip touches the edges, it stops, even if you can still move the anchor. It overflows the body only when the anchor is getting outside.

The browser will initially place the tooltip at the top and centered. The priority is to remain withing the containing block, so if there isn’t enough space to keep the center behavior, the tooltip is shifted. The second priority is to keep the anchor behavior, and in this case, the browser will allow the overflow if the anchor element is outside.

A three-part interactive demo showing a tooltip following an anchor element as it is dragged. The tooltip displays the message 'Drag the anchor and I should follow...' with an anchor icon below.

Assuming the anchor will remain within the body area, we already have what we want without too much effort. The tooltip will never overflow from the right, left, or bottom side. What remains is the top.

By default, the browser can shift the element within the area defined by position-area, but cannot do more than that. We need to instruct the browser on how to handle the other cases. For this, we use position-try-fallbacks where we define different positions for the browser to “try” in case the element doesn’t fit its containing block.

Let’s define a bottom position:

position-try-fallbacks: bottom;

Drag the anchor to the top and see what happens:

Now, when the tooltip overflows the body from the top, the position becomes “bottom”. It will also remain bottom until the tooltip overflows again from the bottom. In other words, when the browser picks a position after an overflow, it keeps it until a new overflow happens.

That’s all, we are done! Now our tooltip is perfectly placed, whatever the anchor position.

But we no longer have the gap when the position is at the bottom (for the future arrow). How do we fix that?

We told the browser to only change the value of position-area to bottom, but we can do better by using:

position-try-fallbacks: flip-block;

“Block” refers to the block axis (the vertical axis in our default writing mode), and this instruction means flip the position across the vertical axis. The logic is to mirror the initial position on the other side. To do this the browser needs to update different properties in addition to position-area.

In the example we’ve defined position-area: top and bottom: var(--d). With position-try-fallbacks: flip-block in place, when that flip happens, it’s as if we defined position-area: bottom and top: var(--d). We keep the gap!

If you are a bit lost and confused, don’t worry. We are dealing with new mechanisms not common to the CSS world so it may take time to click for you.

To sum up, we can either instruct the browser to update only the position-area by defining a new position or to “flip” the actual position across one axis which will update different properties.

Adding The Tail

Adding a tail to a tooltip is pretty straightforward (I even have a collection of 100 different designs), but changing the direction of the tail based on the position is a bit tricky.

Three tooltip examples illustrating text that says 'Drag the anchor and I should follow...' with an anchor icon, showcasing dynamic positioning.

For now, Anchor Positioning doesn’t offer a way to update the CSS based on the position, but we can still use the existing features to “hack” it. Hacking with CSS can be fun!

I am going to rely on the “flip” feature and the fact that it can update the margin to achieve the final result.

First, I will consider a pseudo-element to create the tail shape:

#tooltip {
  --d: 1em; /* distance between tooltip and anchor */
  --s: 1.2em; /* tail size */
}
#tooltip::before {
  content: "";
  position: absolute;
  z-index: -1;
  width: var(--s);
  background: inherit;
  inset: calc(-1*var(--d)) 0;
  left: calc(50% - var(--s)/2);
  clip-path: polygon(50% .2em,100% var(--d),100% calc(100% - var(--d)),50% calc(100% - .2em),0 calc(100% - var(--d)),0 var(--d));
}

Both tails are visible by default. Click “debug mode” to better understand the shape and how it’s placed.

When the tooltip is at the top, we need to hide the top part. For this, we can use a margin-top on the pseudo-element equal to variable --d. And when the tooltip is at the bottom, we need margin-bottom.

I am going to define the margin on the tooltip element and then inherit it on pseudo-element:

#tooltip {
  --d: 1em; /* distance between tooltip and anchor */
  --s: 1.2em; /* tail size */

  margin-top: var(--d);
}
#tooltip::before {
  margin: inherit;
}

Tada. Our tooltip is now perfect! The use of margin will hide one side keeping one tail visible at a time.

But we didn’t define margin-bottom. How does it work for the bottom position?

That’s the “flip” feature. Remember what we did with the gap where we only defined top and flip-block changed it to bottom? The same logic applies here with margin: the margin-top automatically becomes a margin-bottom when the position is flipped! Cool, right?

Note that using margin will cause the tooltip to flip a bit earlier since margin is part of the element, and the logic is to prevent the overflow of the “margin box”. It’s not a big deal in our example; it’s nicer to flip the position before it touches the edges.

Moving The Tail

The top and bottom parts are good, but we still need to fix the cases where the tooltip shifts when it’s close to the left and right edges. The tail needs to follow the anchor. For this, we have to update the left value and make it follow the anchor position.

Instead of:

left: calc(50% - var(--s));

We use:

left: calc(anchor(--anchor center) - var(--s)/2); 

I replace 50%, which refers to the center of the tooltip element, with anchor(--anchor center), which is the center of the anchor element.

The anchor() function is another cool feature of Anchor Positioning. It allows us to query a position from any anchor element and use it to place an absolutely positioned element.

Uh oh — that doesn’t work. I’ve left this in though, as it’s a educational moment we need to look at.

We hit one of the trickiest issues of Anchor Positioning. In theory, any element on the page can be an anchor using anchor-name and any other element can position itself relative to that anchor. That’s the main purpose of the feature but there are exceptions where an element cannot reference an anchor.

I won’t detail all the cases, but in our example, the pseudo-element (the tail) is a child of the tooltip, which is an absolutely positioned element. This makes the tooltip the containing block of the pseudo-element and prevents it from seeing any anchor defined outside it. (If you think z-index and stacking context are hard, get ready for this)

To overcome this, I will update the position of the pseudo-element to fixed. This changes its containing block (the viewport at the moment) and makes it able to see the anchor element.

Yes, the demo is broken, but drag the anchor close to the edges and see how the tail is correctly placed horizontally as it’s now able to “see” the anchor element. However, the pseudo-element now has a fixed position so it can no longer be placed relatively to its parent element, the tooltip. To fix this we can make the tooltip an anchor element as well, so the pseudo-element can reference it.

In the end we need two anchors: #anchor and #tooltip. The tooltip is positioned relatively to the anchor, and the tail is positioned relatively to both the anchor and the tooltip.

#anchor {
  position: absolute;
  anchor-name: --anchor;
}
#tooltip {
  --d: 1em;  /* distance between anchor and tooltip  */
  --s: 1.2em; /* tail size */
  
  position: absolute; 
  position-anchor: --anchor;
  anchor-name: --tooltip;
}
/* the tail */
#tooltip:before {
  content: "";
  position: fixed;
  z-index: -1;
  width: var(--s);
  /* vertical position from tooltip  */
  top:    calc(anchor(--tooltip top   ) - var(--d));
  bottom: calc(anchor(--tooltip bottom) - var(--d));
  /* horizontal position from anchor */
  left: calc(anchor(--anchor center) - var(--s)/2);
}

Thanks to anchor(), I can retrieve the top and bottom edges of the tooltip element and the center of the anchor element to correctly position the pseudo-element.

Our tooltip is now perfect! As I mentioned in the introduction, this CSS is not particularly complex. We barely used 20 declarations.

What if we want to start with the bottom position?

Easy! You simply change the initial configuration to consider the bottom position and flip-block will switch to the top one when there is an overflow.

#tooltip {
  position-area: bottom; /* instead of position-area: top; */
  top: var(--d); /* instead of bottom: var(--d); */
  margin-bottom: var(--d); /* margin-top: var(--d) */
}

Conclusion

That’s all for this first part. We learned how to place a tooltip using position-area and how to defined a fallback position when an overflow occurs. Not to mention the flip feature and the use of the anchor() function.

In the second part, we will increase the difficulty by working with more than two positions. Take the time to digest this first part before moving to the next one I also invite you to spend a few minutes on my interactive demo of position-area to familiarize yourself with it.

Article Series

]]>
https://frontendmasters.com/blog/perfectly-pointed-tooltips-a-foundation/feed/ 3 7514
Examples of Why The Web Needs Anchored Popovers https://frontendmasters.com/blog/examples-of-why-the-web-needs-anchored-popovers/ https://frontendmasters.com/blog/examples-of-why-the-web-needs-anchored-popovers/#respond Thu, 27 Feb 2025 01:18:44 +0000 https://frontendmasters.com/blog/?p=5243 With popovers, you click a <button>, it opens a <whatever>. No JavaScript is required. It opens on the “top layer” so it will always be visible no matter what. You can click away and it closes and/or offer your own close mechanics. The two elements can be wherever makes the most sense in the DOM without restriction. With CSS anchor positioning, the popover that opens can be positioned perfectly and safely next to whatever it needs to be again without DOM restrictions. It’s the best.

What might use this potent combo? Everything.

These type of menus on GitHub would be great.

A similar type of header dropdown menu on our new version of CodePen we’re working on would love it, which sometimes have nested tooltips I’m crossing my fingers work well.

For flyout menus like this it will be nice to have particularly for the save positioning. Wouldn’t it be nice to have that menu open more “upwards” instead if there wasn’t room before the bottom of the browser window below?

Maybe the DOM positioning matters less above as perhaps it actually makes sense that the submenu is, say, a nested list, which already helps with positioning. That’s OK, sometimes you need extra help and sometimes you don’t, it still nice to use a unified system.

There is about a dozen popups like this in the Riverside.fm interface that would all benefit from positioned popovers. Wouldn’t it be nice to move elements around, even dramatically in response to screen sizes and aspect ratios, and never worry that menu popup positioning would get donked up?

Really anything app-like, like Figma, would benefit from being able to show menus without additional JavaScript having to run calculations and always be watching for window size changes in order to position things correctly, to say nothing of z-index battles that, when lost, can render menus entirely hidden.

Designers will surely enjoy having being able to think in terms of alignment. Where should this popup hang off of? Straight off the right side? Pushed down then to the right? Centered below it? We’ll have lots of positioning options that work great with pretty simple keywords that have smart side effects.

What’s inside a popup can be complex sometimes, involving forms, multiple lists, lots of interactive elements, etc. Not being forced to put that in the same exact DOM area as the button that triggers is might be beneficial for accessibility, not to mention the help handling moving focus in and out of that area properly.

There are a lot of menu examples above, but I think there are just as many examples out there of “styled tooltips” which is something we’ve never properly been able to do without lots of fancy dancing JavaScript.

It’s telling you, the web is chock-a-block with these things.

]]>
https://frontendmasters.com/blog/examples-of-why-the-web-needs-anchored-popovers/feed/ 0 5243
Anchoreum https://frontendmasters.com/blog/anchoreum/ https://frontendmasters.com/blog/anchoreum/#respond Fri, 13 Dec 2024 17:57:31 +0000 https://frontendmasters.com/blog/?p=4802 A “game” where you enter the right CSS to move to the next level, teaching you CSS anchor positioning along the way.

In the vein of Flexbox Froggy, which I know clicked with a ton of people. Made by Thomas Park, who’s company Codepip actually makes a ton of these experiences.

]]>
https://frontendmasters.com/blog/anchoreum/feed/ 0 4802
Custom Range Slider Using Anchor Positioning & Scroll-Driven Animations https://frontendmasters.com/blog/custom-range-slider-using-anchor-positioning-scroll-driven-animations/ https://frontendmasters.com/blog/custom-range-slider-using-anchor-positioning-scroll-driven-animations/#comments Wed, 21 Aug 2024 14:20:29 +0000 https://frontendmasters.com/blog/?p=3569 Anchor positioning and scroll-driven animations are among of the most popular and exciting CSS features of 2024. They unlock a lot of possibilities, and will continue to do so as browser support improves and developers get to know them.

Here is a demo of a custom range slider where I am relying on such features.

This whole UI is a semantic HTML <input type="range">, with another semantic <output> element showing off the current value, along with quite fancy CSS.

Intuitively, you may think there is a JavaScript code somewhere gathering the value of the input “on change” and updating the position/content of the tooltip. As for the motion, it’s probably a kind of JavaScript library that calculates the speed of the mouse movement to apply a rotation and create that traction illusion.

Actually, there is no JavaScript at all.

It’s hard to believe but CSS has evolved in a way that we can achieve such magic without any scripts or library. You will also see that the code is not that complex. It’s a combination of small CSS tricks that we will dissect together so follow along!

At the time of writing, only Chrome (and Edge) have the full support of the features we will be using.

Prerequisites

First, let’s start with the HTML structure:

<label>
  Label
  <input type="range" id="one" min="0" max="120" value="20">
  <output for="one" style="--min: 0;--max: 120"></output>
</label>  

An input element and an output element are all that we need here. The label part is not mandatory for the functionality, but form elements should always be labelled and you need a wrapper element anyway.

I won’t detail the attributes of the input element but note the use of two CSS variables on the output element that should have the same values as the min and max attributes.

In addition to the HTML code, I am going to consider the styling of the range slider and the tooltip as prerequisites as well. I will mainly focus on the new features and skip most of the aesthetic parts, although I have covered some of those aspects in other articles, like here where I detail the styling of the range slider.

As for the tooltip, I have a big collection of 100 different tooltip shapes and I am going to use the #41 and #42. I also have a two-part article detailing the creation of most of the tooltips.

You don’t need the fancy styled tooltip output, nor do you need the custom styling of the range slider itself, it’s just fun and offers some visual control you might want. Here’s a “naked” demo without all that:

The Tooltip Position

The first thing we are going to do is to correctly place the tooltip above (or below) the thumb element of the slider. This will be the job of Anchor positioning and here is the code:

input[type="range" i]::-webkit-slider-thumb {
  anchor-name: --thumb;
}
output {
  position-anchor: --thumb;
  position: absolute;
  position-area: top; /* or bottom */
}

That’s all! No more than four CSS declarations and our tooltip is correctly placed and will follow the movement of the slider thumb.

Anchor positioning is an upgrade of position: absolute here. Instead of positioning the element relatively to an ancestor having position: relative we can consider any element on the page called an “anchor”. To define an anchor we use anchor-name with whatever value you want. It’s mandatory to use the dashed indent notation like with custom properties. That same value can later be used within the absolute element to link it with the “anchor” using position-anchor.

Defining the anchor is not enough, we also need to correctly position the element. For this, we have the position-area.

The position-area CSS property enables an anchor-positioned element to be positioned relative to the edges of its associated anchor element by placing the positioned element on one or more tiles of an implicit 3×3 grid, where the anchoring element is the center cell.

ref

Here is an online tool to visualize the different values.

We’re using position-area: top on the <output>, and a bottom class flips that to position-area: bottom to re-position it and make the design work below.

Here is the demo so far:

Hmmmm, there is an issue! Both tooltips are linked to the same thumb. This is understandable, because I used the same anchor name so the first one will get ignored.

Use a different name, you say, and that’s correct but it’s not the optimal solution. We can still keep the same name and instead, limit the scope using anchor-scope.

label {
  anchor-scope: --thumb;
}

The above code should limit the scope of the anchor --thumb to the label element and its descendant. In other words, the anchor cannot be seen outside the label element.

Another fix is to add position: relative to label. I won’t detail how it works but it has to do with the creation of a containing block.

Hmmmmm. We have fixed the scoping problem but the position of the tooltip is still not good. If you move the thumb to the edges, the tooltip is no longer following. It’s limited to the boundary of the slider. It’s kind of strange, but it’s by design.

By adding position: relative we create a containing block for the tooltip and we trigger the following behavior described by the specification:

If the box overflows its inset-modified containing block, but would still fit within its original containing block, by default it will “shift” to stay within its original containing block, even if that violates its normal alignment. This behavior makes it more likely that positioned boxes remain visible and within their intended bounds, even when their containing block ends up smaller than anticipated.

To fix this, we can use justify-self: unsafe anchor-center;.

When using position-area: top (or bottom), the browser applies a default alignment in the horizontal axis equivalent to justify-self: anchor-center. By adding the unsafe keyword, we allow it to overflow the containing block instead of shifting inside it.

The Tooltip Content

Now that our tooltip is correctly positioned, let’s move to the content. This is where scroll-driven animations enter the story. I know what you are thinking: “We have nothing to scroll, so how are we going to use scroll-driven animations?”

If you read the MDN page you will find something called a “view progress timeline”:

You progress this timeline based on the change in visibility of an element (known as the subject) inside a scroller. The visibility of the subject inside the scroller is tracked as a percentage of progress — by default, the timeline is at 0% when the subject is first visible at one edge of the scroller, and 100% when it reaches the opposite edge.

This is perfect for us since we have a thumb (the subject) that moves inside the input (the scroller) so we don’t really need to have anything else to scroll.

We start by defining the timeline as follows:

input {
  overflow: hidden; /* or `auto` */
}
input[type="range" i]::-webkit-slider-thumb {
  view-timeline: --thumb-view inline;
}

Similar to what we did with the anchor, we give a name and the axis (inline) which is the horizontal one in our default writing mode. Then, we define overflow: hidden on the input element. This will make the input our scroller while the thumb is the subject.

If you forget about the overflow (so easy to forget!), another element will get used as the scroller, and won’t really know which one, and nothing will work as expected. Always remember that you need to define the subject using view-timeline and the scroller using overflow. I will repeat it again: don’t forget to define overflow on the scroller element!

Next, we define the animation:

@property --val {
  syntax: '<integer>';
  inherits: true;
  initial-value: 0; 
}
label {
  timeline-scope: --thumb-view;
}
output {
  animation: range linear both;
  animation-timeline: --thumb-view;
}
@keyframes range {
  0%   { --val: var(--max) }
  100% { --val: var(--min) }
}

Let’s start with timeline-scope. This is yet another scoping issue that will give you a lot of headaches. With anchor positioning, we saw that an anchor is by default available everywhere on the page and we have to limit its scope. With scroll-driven animations, the scope is limited to the element where it’s defined (the subject) and its descendant so we have to increase the scope to make it available to other elements. Two different implementations but the same issue.

Never ever forget about scoping when working with both features. Sometimes, everything is correctly defined and you are only missing timeline-scope or position: relative somewhere.

Next we define an animation that animates an integer between the min and max variables, then link that animation with the timeline we previously defined using animation-timeline.

Why the max is at 0% and the min at 100%? Isn’t that backwards, you ask?

Intuitively, we tend to think “from left to right” but this looks like it’s “from right to left”. To understand this, we need to consider the “scroll” part of the feature.

I know that we don’t have scrolling in our case but consider the following example to better understand.

When you scroll the container “from left to right” you have a red circle that moves “from right to left”. We still have the “from left to right” behavior but it’s linked to the scroll. As for the content, it will logically move in the opposite direction “from right to left”.

When the scroll is at the left, the element is at the right and when the scroll is at the right, the element is at the left. The same logic applies to our thumb even if there is nothing to scroll. When the thumb is at the right edge, this is our 0% state and we need to have the max value there. The left edge will be the 100% state and it’s the min value.

The last step is to show the value using a pseudo-element and counter()

output::before {
  content: counter(num);
  counter-reset: num var(--val);
}

And we are done!

Wait a minute, the values aren’t good! We are not reaching the min and max values. For the first slider, we are supposed to go from 0 to 120 but instead, we have 9 and 111.

Another trick related to the scroll part of the feature and here is a figure to illustrate what is happening:

The movement of the thumb is limited to the input container (the scroller) but the 0% and 100% state are defined to be outside the scroller. In our case, the subject cannot reach the 0% and 100% since it cannot go outside but luckily we can update the 0% and 100% state:

We can either use animation-range to make both states inside the container:

output {
  animation: range linear both;
  animation-timeline: --thumb-view;
  animation-range: entry 100% exit 0%;
}

Or we consider view-timeline-inset with a value equal to the width of the thumb.

input[type="range" i]::-webkit-slider-thumb{
  anchor-name: --thumb;
  view-timeline: --thumb-view inline;
  view-timeline-inset: var(--s); /* --s is defined on an upper element and is used to define the size of the thumb */
}

The first method seems better as we don’t have to know the size of the thumb (the subject) but keep in mind both methods. The view-timeline-inset property may be more suitable in some situations.

Now our slider is perfect!

A lot of stuff to remember, right? Between the scoping issues, the range we have to correct, the overflow we should not forget, the min that should be at 100% and max that should be at 0%, etc. Don’t worry, I feel the same. They are new features with new mechanisms so it requires a lot of practice to get used to them and build a clear mental model. If you are a bit lost, that’s fine! No need to understand everything at once. Take the time to play with the different demos, read the doc of each property, and try things on your own.

Adding Motion

Now let’s move to the fun part, those silly wobbly animations. A tooltip that follows the thumb with dynamic content is good but it’s even better if we add some motion to it.

You may think this is gonna be the hardest part but actually it’s the easiest one, and here is the relevant code:

@property --val {
  syntax: '<integer>';
  inherits: true;
  initial-value: 0; 
}
@property --e {
  syntax: '<number>';
  inherits: true;
  initial-value: 0; 
}
output {
  animation: range linear both;
  animation-timeline: --thumb-view;
  animation-range: entry 100% exit 0%;
}
output:before {
  content: counter(num);
  counter-reset: num var(--val);
  --e: var(--val);
  transition: --e .1s ease-out;
  rotate: calc((var(--e) - var(--val))*2deg);
}
@keyframes range {
  0%   { --val: var(--max) }
  100% { --val: var(--min) }
}

We add a new CSS variable --e with a number type. This variable will be equal to the --val variable. Until now, nothing fancy. We have two variables having the same value but one of them has a transition. Here comes the magic.

When you move the thumb, the animation will update the --val variable inside the output element. The pseudo-element will then inherit that value to update the content and also update --e. But since we are applying a transition to --e, it will not have an instant update but a smooth one (well, you know how transitions work!). This means that for a brief moment, both --e and --val will not be equal thus their difference is different from 0. We use that difference inside the rotation!

In addition to this, the difference can get bigger if you move the thumb fast or slow. Let’s suppose the current value is equal to 5. If you move the thumb rapidly to the value 50, the difference will be equal to 45 hence we get a big rotation. If you move to the value 7, the difference will be equal to 2 and the rotation won’t be that big.

Here is the full demo again so you can play with it. Try different speeds of movement and see how the rotation is different each time.

If you want to dig more into this technique and see more examples I advise you to read this article by Bramus.

Another Example

Let’s try a different idea.

This time, I am adjusting the tooltip position (and its tail) to remain within the horizontal boundary of the input element. Can you figure out how it’s done? This will be your homework!

For the tooltip part, I already did the job for you. I will redirect you again to my online collection where you can get the code of the tooltip shape. Within that code, I am already defining one variable that controls the tail position.

Conclusion

CSS is cool. A few years ago, doing such stuff with CSS would have been impossible. You would probably need one or two JavaScript libraries to handle the position of the tooltip, the dynamic content, the motion, etc. Now, all it takes is a few lines of CSS.

It’s still early to adopt those features and include them in real projects but I think it’s a good time to explore them and get an overview of what could be done in the near future. If you want more “futuristic” experimentation make sure to check my CSS Tip website where I regularly share cool demos!

]]>
https://frontendmasters.com/blog/custom-range-slider-using-anchor-positioning-scroll-driven-animations/feed/ 3 3569
Popover API is Here https://frontendmasters.com/blog/popover-api-is-here/ https://frontendmasters.com/blog/popover-api-is-here/#comments Tue, 30 Apr 2024 18:12:46 +0000 https://frontendmasters.com/blog/?p=1948 The Popover API has support across browsers now, and apparently the new way to say that is “landed in Baseline”, albeit as “newly available” rather than “widely available”, which may be an important distinction for you.

A popover is very much like a <dialog> in that you don’t have to worry about where it exists in the DOM. When it’s open, it will be “on top”. That alone is a sick feature. I dislike having to put modals as children of the <body> just so they don’t have other elements inescapable stacking contexts to worry about.

So then how is it different than a <dialog>? Popups are not “modal”. The web-world definition of modal is that it forces a user to deal with it (the rest of the page cannot receive focus, it is inert). Popups just… pop… up. The user is free to do anything else while a popup is open. Dialogs, not so much.

Robin showed off how extremely very easy it is to use:

<button popovertarget="mypopover">Toggle the popover</button>

<div id="mypopover" popover>Popover content</div>

The fact that that’s possible in HTML alone is awfully nice. But of course you can style it with CSS if you like. Plus you get freebies like the fact that they ESC key closes it. Taking it a bit further, here’s an only slightly more fleshed out example using other cool freebies like popovertargetaction="hide":

(p.s. I dig the trick of getting a white ❌ with the filter property as shown above. Golf clap.)

Robin goes on to say that the logic for using it is something like this:

  1. Use title if the content in my popover is just text.
  2. Use the popover attribute if the content is more than plain text and like a menu of options or something.
  3. Use <dialog> if you need to force the user to make a decision or block all other interactions on the page.

I only disagree on the first one. I think title is pretty useless and I only tend to include one if some accessibility tool warns me to add one, like on an <iframe>. The title attribute does nothing on mobile/touch devices or screen readers. You can’t control anything about it, like how it looks, where it goes, or how long it takes to show up if it does at all. If we get more control over it (which, who knows, we might) then I’d be happy to take another look, but it seems likely it’ll still be a “only use if it’s a just text” situation.

I’m certainly a fan of this API existing, but I do think it’s very hampered right now without the Anchor Position API being nearly as well supported. Massive use cases like tooltips or flyout menus aren’t really possible without it. Una has written about this as well. And good news, it can also be HTML powered.

<button id="menu-toggle" popovertarget="menu-items">
  Open Menu
</button>

<ul id="menu-items" popover anchor="menu-toggle">
  <li class="item">...</li>
  <li class="item">...</li>
</ul>

Note the anchor attribute there connecting the two elements. So cool. And again, you can take finer grained control with CSS if you’d like. “Open right next to this other element” is clutch. Heads up though, the anchor attribute is not ready yet.

Taking it futher, here’s another idea from Una that uses an anchor element as a central position and then moves individual items in a circle around it.

I’ll tell ya, Una is ready with all this! Particularly with the Anchor Positioning API, she’s got a bunch of demos and a little helper website to make sure you can declare the positioning CSS you intend:

I now will await patiently the Anchor Positioning API to drop everwhere. Alas, Popover was part of interop 2024 where Anchor was not.

]]>
https://frontendmasters.com/blog/popover-api-is-here/feed/ 2 1948
Drawing a Line to Connect Elements with CSS Anchor Positioning https://frontendmasters.com/blog/drawing-a-line-to-connect-elements-with-css-anchor-positioning/ https://frontendmasters.com/blog/drawing-a-line-to-connect-elements-with-css-anchor-positioning/#respond Tue, 02 Apr 2024 18:02:50 +0000 https://frontendmasters.com/blog/?p=1511 The World Wide Web Consortium (W3C) published a First Public Working Draft of CSS Anchor Positioning last year, so I thought I would give it a try. I already had a perfect candidate to try it on: a component on my other site, adedicated.dev, which showcase my services by linking different words together.

To link different elements in columns, my component relies on heavy JavaScript calculation. Here’s that example. While I love solving a math problem here and there, I prefer browsers doing these kinds of calculations for me!

Let’s take a look at CSS Anchor Positioning and see how it might have a solution for us.

A Bit about CSS Anchor Positioning

CSS Anchor Positioning provides a better way to position an element in relation to another element. Think of a tooltip and how it is positioned related to the element that triggers it. The perfect tooltip usually “knows” if it overflows outside of the containing block. For example, if the tooltip doesn’t fit above its trigger element, it should go below it. CSS Anchor Positioning solves this problem for us, and that mean less JavaScript calculation.

It is worth noting that CSS Anchor Positioning is quite new API and it is prone to changes. At the time of this writing, the only browser that supports this feature is Chrome Canary, and it is behind the “Experimental Web Platform Features” flag.

The Demo

Back to my component. I have a three columns, and in each one I have a set of words which, when linked, form a new term. When you hover over any word, a random word in three different columns is highlighted and the final term is created. For example, “Creating WordPress Websites” or “Developing HubSpot Pages” or “Updating Shopify Layouts”. I thought it would be fun to showcase my skills in such a way. Here’s how the component works:

To solve the problem of linking different words, we need to prepare the HTML for that. I am using two <div>s for two links, first one for link between first and second column, and the other one for linking second and third column.

<div class="link link--alpha"></div>
<div class="link link--beta"></div>

First thing we need to do is to position our <div>s. For each level links, I had to set up the min-block-size (the logical equivalent of width in a left-to-right or right-to-left language — we’ll be using more of these logical properties as this article goes on):

.link {
  position: absolute;
  min-block-size: 2px;
}

Then we need a grid of all words. I am using unordered list and CSS Grid to achieve this.

<ul>
  <li>Creating</li>
  <li>WordPress</li>
  <li>Websites</li>

  <li>Developing</li>
  <li>HubSpot</li>
  <li>Pages</li>

  <li>Updating</li>
  <li>Shopify</li>
  <li>Layouts</li>

  <li>Implementing</li>
  <li>Jekyll</li>
  <li>Templates</li>

  <li>Optimizing</li>
  <li>Hugo</li>
  <li>Components</li>
</ul>
:root {
  --color-alpha: lightcyan;
  --color-beta: cyan;
  --color-gamma: indigo;
}

ul {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 32px;
  cursor: pointer;
}

li {
  color: var(--color-gamma);
  background-color: var(--color-alpha);
  border-radius: var(--space-alpha);
  position: relative;
  padding: 32px;
  transition: background-color 110ms, color 110ms;
}

To highlight the word, I am using data attributes on unordered list element, like so:

<ul data-col1="2" data-col2="3" data-col3="4">
  ...
</ul>

[data-col1="1"] li:nth-child(1),
[data-col1="2"] li:nth-child(4),
[data-col1="3"] li:nth-child(7),
[data-col1="4"] li:nth-child(10),
[data-col1="5"] li:nth-child(13),
[data-col2="1"] li:nth-child(2),
[data-col2="2"] li:nth-child(5),
[data-col2="3"] li:nth-child(8),
[data-col2="4"] li:nth-child(11),
[data-col2="5"] li:nth-child(14),
[data-col3="1"] li:nth-child(3),
[data-col3="2"] li:nth-child(6),
[data-col3="3"] li:nth-child(9),
[data-col3="4"] li:nth-child(12),
[data-col3="5"] li:nth-child(15) {
  background-color: var(--color-beta);
  transition: background-color var(--trd-beta), color var(--trd-beta);
}

Now for the fun stuff — let’s anchor some elements! We are going to define three anchor-names, each for a single column. These elements will be defined by the word element. That way our line elements will be able to use the position of linked word elements and link each other.

[data-col1="1"] li:nth-child(1),
[data-col1="2"] li:nth-child(4),
[data-col1="3"] li:nth-child(7),
[data-col1="4"] li:nth-child(10),
[data-col1="5"] li:nth-child(13) {
  anchor-name: --link-col1;
}

[data-col2="1"] li:nth-child(2),
[data-col2="2"] li:nth-child(5),
[data-col2="3"] li:nth-child(8),
[data-col2="4"] li:nth-child(11),
[data-col2="5"] li:nth-child(14) {
  anchor-name: --link-col2;
}

[data-col3="1"] li:nth-child(3),
[data-col3="2"] li:nth-child(6),
[data-col3="3"] li:nth-child(9),
[data-col3="4"] li:nth-child(12),
[data-col3="5"] li:nth-child(15) {
  anchor-name: --link-col3;
}

Here’s the image so you can visualize the link elements more easily.

Next, we need to define the offset for our element by using the anchor function. We want our first line (the left pink rectangle) to start outside and in the middle of the first word element and to end outside and in the middle of the second word element. 

.link--alpha {
  inset-block-start: anchor(--link-col1 center);
  inset-inline-start: anchor(--link-col1 right);
  inset-inline-end: anchor(--link-col2 left);
  inset-block-end: anchor(--link-col2 center);
}

(Editor’s note: I drew this crude diagram that follows to demonstrate how the placement of that pink rectangle works because it’s totally fascinating to me!)

It’s the same setup for the second line, but we are the referencing the second and third word elements instead of the first and second.

.link--beta {
  inset-block-start: anchor(--link-col2 center);
  inset-inline-start: anchor(--link-col2 right);
  inset-inline-end: anchor(--link-col3 left);
  inset-block-end: anchor(--link-col3 center);
}

To make the lines, I am using a linear gradient in the following fashion:

  • The first linear gradient is vertical line that is 100% in height and placed in the center of rectangle
  • The second linear gradient is horizontal line that starts in the top left corner and is 50% of width
  • The third linear gradient is horizontal line that starts in the bottom right corner and is 50% of width
.link {
  background-image: linear-gradient(to bottom, black, black), linear-gradient(to right, black, black), linear-gradient(to bottom, black, black);
  background-size: 2px, 50% 2px, 50% 2px;
  background-position: center, top left, bottom right;
  background-repeat: no-repeat;
}

Here’s how it looks now.

To generate different terms on each hover event and to automatically change the terms when no hover effects occur to make the whole component more appealing and inviting, we need to introduce a bit of JavaScript. Once the timeout expires, JavaScript will update the data-col1, data-col2, and data-col3 attributes.

const highlighter = (timeout = 4000) => {
  const $ul = document.querySelector('ul')
   
  const getRandomNumber = (min, max) => {
    return Math.floor(Math.random() * (max - min + 1)) + min;
  }
  
  const randomHighlighter = ($s, c) => {
    const nth1 = [1, 4, 7, 10, 13]
    const nth2 = [2, 5, 8, 11, 14]
    const nth3 = [3, 6, 9, 12, 15]
    
    let c1 = getRandomNumber(1, 5)
    let c2 = getRandomNumber(1, 5)
    let c3 = getRandomNumber(1, 5)

    if(c && nth1.indexOf(c) !== -1) {
      c1 = nth1.indexOf(c) + 1
    }

    if(c && nth2.indexOf(c) !== -1) {
      c2 = nth2.indexOf(c) + 1
    }

    if(c && nth3.indexOf(c) !== -1) {
      c3 = nth3.indexOf(c) + 1
    }

    if(c2 < c1) {
      document.body.classList.add('link-alpha-inverse')
    } else {
      document.body.classList.remove('link-alpha-inverse')
    }
    
    if(c3 < c2) {
      document.body.classList.add('link-beta-inverse')
    } else {
      document.body.classList.remove('link-beta-inverse')
    }

    $s.setAttribute('data-col1', c1)
    $s.setAttribute('data-col2', c2)
    $s.setAttribute('data-col3', c3)
  }

  if($ul) {
    const $lis = $ul.querySelectorAll('li')

    let hover = false;

    randomHighlighter($ul)

    const si = setInterval(() => {
      if(!hover) {
        randomHighlighter($ul)
      }
    }, timeout)

    $lis.forEach(($li, i) => {
      $li.addEventListener('mouseenter', () => {
        randomHighlighter($ul, i + 1)

        hover = true
      })
      
      $li.addEventListener('click', () => {
        randomHighlighter($ul, i + 1)

        hover = true
      })
    })

    $ul.addEventListener('mouseleave', () => {
      hover = false
    })
  }
}

highlighter()

There is one final problem that we need to resolve. In case when the second word is “higher” than the first word, the positioning will not work. That is because we cannot have “negative” elements, meaning the block end must be bigger or equal to block start property. To solve that problem, we will add another class to the body element.

// ...

if(c2 < c1) {
  document.body.classList.add('link-alpha-inverse')
} else {
  document.body.classList.remove('link-alpha-inverse')
}

if(c3 < c2) {
  document.body.classList.add('link-beta-inverse')
} else {
  document.body.classList.remove('link-beta-inverse')
}

// ...

Now we could adjust our line component’s CSS and fix the background positioning, too.

.link-alpha-inverse .link--alpha {
  inset-block-end: anchor(--link-col1 center);
  inset-block-start: anchor(--link-col2 center);
  background-position: center, bottom left, top right;
}

.link-beta-inverse .link--beta {
  inset-block-end: anchor(--link-col2 center);
  inset-block-start: anchor(--link-col3 center);
  background-position: center, bottom left, top right;
}

Conclusion

The original solution to this kind of problems required a whole lot of JavaScript calculations and clumsy inserting of <style> element to our HTML. With CSS Anchor Positioning, we use JavaScript only to update our data attributes and toggle body classes – all calculations and heavy lifting are done by our browser. I think that is wild and I cannot wait to see other useful places where this could be used.

Final Demo

Remember to see the lines, at the time of publication, you need to be in Chrome Canary with the Experimental Web Features flag turned on. If you want to see more JavaScript calculation heavy fallback, see here.

If you’re really into these ideas, definitely check out the web.dev article Tether elements to each other with CSS anchor positioning

]]>
https://frontendmasters.com/blog/drawing-a-line-to-connect-elements-with-css-anchor-positioning/feed/ 0 1511
Menus, toasts and more with the Popover API, the dialog element, invokers, anchor positioning and @starting-style https://frontendmasters.com/blog/menus-toasts-and-more/ https://frontendmasters.com/blog/menus-toasts-and-more/#respond Mon, 04 Mar 2024 20:21:03 +0000 https://frontendmasters.com/blog/?p=1104 Dropdowns, menus, tooltips, comboboxes, toasts — the popover attribute will make building a large variety of UI components easier. The popover attribute can be used on any HTML element, so you have the flexibility to choose whichever element is most appropriate semantically for each particular use case. Unlike a dialog, a popover is always non-modal — meaning they don’t block interaction with anything else on the page. To toggle a popover open and closed, a button element needs to include an invoketarget attribute with a value that matches the id of the popover.

<button invoketarget="foobar">Toggle popover</button>

<div id="foobar" popover>
  Popover content goes here...
</div>

A <button> with an invoketarget attribute is called an invoker. Invokers might eventually bring all sorts of power to HTML markup, but in its first iteration it’s limited to opening and closing popovers and dialogs. You don’t need onclick= or addEventListener, it’ll just work.

The fact that popovers work without JavaScript is nice, but toggling display: none on an element using JS was never challenging. Popovers do, however, bring far more to the table:

  • Popovers make use of the top layer.
  • Light-dismiss functionality: clicking outside of the popover will close the popover.
  • Hitting the escape key will close the popover.
  • Focus management: when you open a popover, the next tab stop will be the first focusable element inside the popover. If you’ve focused an element within the popover and then close the popover, focus is returned to the correct place (this was tricky to get right with JavaScript).

Browser support

The popover attribute is supported in Chrome, Safari, and Firefox 125. The popovertarget attribute currently has better browser support than invoketarget. popovertarget is popover-specific, offering a declarative way to toggle popovers open and closed. popovertarget will likely eventually be deprecated and replaced by the more flexible invoketarget. After popovers shipped in Chrome, some smart people realised it would also be handy to have a declarative way for buttons to open dialogs and perform other tasks, which is why there are two ways to do the same thing. A polyfill for invokers is available.

Light dismiss

The popover attribute can be set to either auto (the default) or manual. When set to auto, the popover has light dismiss functionality: if the user clicks outside of the popover, the popover is closed. Pressing the escape key will also close the popover. Only one auto popover is ever open at a time.

When set to manual, there is no light dismiss functionality and the escape key does not close the popover. The popover must be explicitly closed by pressing the button again (or by calling hidePopover() in JavaScript). Multiple manual popovers can be open at the same time.

<button invoketarget="foobar">Toggle popover</button>

<div id="foobar" popover="manual">
  Popover content goes here...
</div>

Invoker actions

Along with the invoketarget attribute, a button can also optionally include an invokeaction attribute. The different actions are listed below.

ActionDescription
showpopoverShow a popover.
hidepopoverClose a popover.
showmodalOpen a dialog element as modal.
closeClose a dialog element.

If you omit the invokeaction attribute, the default behaviour depends on the context: If the target set by invoketarget is a popover it will call .togglePopover(). If the target is a dialog it will call showModal() if the dialog is closed and will close the dialog if the dialog is open.

Using invokers for the dialog element looks much the same as the popover example:

<button invoketarget="my-dialog">Open Dialog</button>

<dialog id="my-dialog">
  Dialog content goes here.
  <button invoketarget="my-dialog" invokeaction="close">Close dialog</button>
</dialog>

Along with built-in actions, developers can write custom actions. This is outside the scope of this article as a custom action could do anything — it need not be related to dialogs or popovers.

While a selling point of invokers is forgoing JavaScript, they also provide a new JavaScript invoke event should you need more than the default behaviour. This event is fired on the popover or dialog, not the button.

document.querySelector("[popover]").addEventListener("invoke", function(event) {
    console.log(event.action);
    console.log(event.invoker);
    // do something useful here...
  });

Within the event handler you can get a reference to whichever button triggered the invocation with event.invoker and determine the action specified by invokeaction with event.action.

Popover methods and events

For many use cases, the popover API doesn’t require JavaScript. What if we want to display a toast notification without a user first interacting with a button, for example?

There are methods to show, hide, or toggle a popover element: .showPopover(), .hidePopover() and .togglePopover(), respectively.

document.getElementById('toast').showPopover();

There is a toggle event that fires on the popover both when the popover gets shown and when it gets hidden (there are no separate open or close events). This would be useful for a toast alert that automatically disappears after a set amount of time, for example, as there’s no markup or CSS-based way to do that.

Its worth checking that the popover isn’t already hidden before calling hidePopover(). We can do that with either .matches(':popover-open'), .checkVisibility(), or event.newState === 'open', all of which will return true if the popover is open.

toast.addEventListener("toggle", function (event) {
  if (event.target.matches(":popover-open")) {
    setTimeout(function () {
      toast.hidePopover();
    }, 3000);
  }
});

There’s also a beforetoggle method, which is similar but lets you call event.preventDefault() inside the event handler, should you need to — and it might come in useful for animations. The toggle event, by contrast, isn’t cancellable.

Default popover styles

By default a popover is set to position: fixed and displayed in the center of the viewport with a solid black border but you’re free to style it however you like. The styles the browser applies to a popover look something like this:

[popover] {
    position: fixed;
    width: fit-content;
    height: fit-content;
    inset: 0px;
    margin: auto;
    border: solid;
    padding: 0.25em;
}

If I wanted to position a popover in the bottom left, for example, I’d need to set top and right to either auto, initial or unset.

.toast {
    inset: unset;
    bottom: 12px;
    left: 12px;
}

Beyond z-index: The top layer

Some JavaScript frameworks have something called portals for rendering things like tooltips and dialogs. I always found portals difficult to work with. The React docs describe portals like so:

“Portals let your components render some of their children into a different place in the DOM. This lets a part of your component “escape” from whatever containers it may be in. For example, a component can display a modal dialog or a tooltip that appears above and outside of the rest of the page… You can use a portal to create a modal dialog that floats above the rest of the page, even if the component that summons the dialog is inside a container with overflow: hidden.”

When working with either the <dialog> element (rather than crafting one out of divs) or the popover attribute, you can avoid this issue entirely — no portals required. Their location in the DOM doesn’t matter. Its often convenient to collocate the markup for a popover or <dialog> together with the button that opens it. They can appear anywhere in your markup and won’t get cropped by overflow: hidden on a parent element. They make use of the top layer, which is a native web solution for rendering content above the rest of the document. The top layer sits above the document and always trumps z-index. An element in the top layer can also make use of a styleable ::backdrop pseudo-element.

Animate an element into and out of the top layer

By default, when a popover or dialog is opened, it instantly appears. You might want to add an entry animation — perhaps a quick opacity fade-in, for example. @starting-style is used to animate an element into view with a CSS transition (you don’t need @starting-style when working with @keyframes). @starting-style works both when you’re adding a new element to the DOM and when an element is already in the DOM but is being made visible by changing its display value from display: none. When in a closed state, both the popover attribute and the <dialog> element make use of display: none under the hood, so @starting-style can be used to animate them onto the page.

The following transition will fade and spin the popover into view, and scale down the size of the popover for the exit transition.

/*  Transition to these styles on entry, and from these styles on exit   */
[popover]:popover-open {
  opacity: 1;
  rotate: 0turn;
  transition: rotate .5s, opacity .5s, display .5s allow-discrete, overlay .5s allow-discrete;
}

/*   Entry transition starts with these styles  */
@starting-style {
  [popover]:popover-open {
    opacity: 0;
    rotate: 1turn;
  }
}

/*  Exit transition ends with these styles  */
[popover]:not(:popover-open) {
  scale: 0;
  transition: scale .3s, display .3s allow-discrete, overlay .3s allow-discrete;
}

The popover will transition from its @starting-style styles to its [popover]:popover-open styles every time it’s opened.

The overlay transition is necessary boilerplate when transitioning an element in or out of the top layer. The overlay property was added to CSS purely for this use case and has no other practical application. It is an unusual property to the extent that, outside of transitions, it can only be specified by the browser — you can’t set it with your own CSS. By default, a dialog or popover is instantly removed from the top layer when closed. This will lead to the element getting clipped and obscured. By transitioning overlay, the element stays in the top layer until the transition has finished.

transition-behavior is a new CSS property that can be set to either normal or allow-discrete. In the above code example I’m using the shorthand.

Similarly for the display property, by including it in the transition and specifying transition-behavior: allow-discrete we ensure that a change from display: none happens at the very start of the entrance transition and that a change to display: none happens at the very end of the exit transition.

@starting-style has some useful applications outside of working with popovers and dialogs, but that’s a topic for a different article.

You can transition the ::backdrop pseudo-element in a similar way.

e.g.

@starting-style {
  [popover]:popover-open::backdrop {
    opacity: 0;
  }
}

Now let’s look at doing the same transition with a <dialog> element:

/*  Transition to these styles on entry, and from these styles on exit   */
dialog:open {
  opacity: 1;
  rotate: 0turn;
  transition: rotate .5s, opacity .5s, display .5s allow-discrete, overlay .5s allow-discrete;
}

/*   Entry transition starts with these styles  */
@starting-style {
  dialog:open {
    opacity: 0;
    rotate: 1turn;
  }
}

/*  Exit transition ends with these styles.  */
dialog:closed {
  scale: 0;
  transition: scale .3s, display .3s allow-discrete, overlay .3s allow-discrete;
}

The :open and :closed selectors are new pseudo-selectors. They work for details, dialog, and select elements — but not for popovers. You can use dialog[open] and dialog:not([open]) for the time being for better browser support.

These examples all work in Chrome. @starting-style and transition-behavior are part of Interop 2024, meaning they’ll likely be fully supported by the end of the year. Safari 17.4 added support for transition-behavior: allow-discrete. Safari Technology Preview 189 added support for @starting-style. WebKit have yet to declare a position on the overlay property.

Anchor positioning

With a component like a toast or a dialog, we generally want to position the element in relation to the viewport. We typically display a dialog in the center of the screen, and a toast at the bottom. That’s easy to do. There are other times when you need to position an element in relation to another element on the page. For a dropdown menu, for example, we want to place the popover in relation to the button that opened it. This is more challenging.

Screenshot of the ... three dot menu on YouTube opened up showing a menu of three options: Clip, Save, and Report.

This sort of behaviour usually requires JavaScript and led to the creation of the popular JavaScript libraries Popper, Floating UI and Tether. With the addition of anchor positioning to CSS, we’ll no longer need to reach for JavaScript. The anchor() function allows developers to tether an absolutely positioned element to one or more other elements on the page. Unfortunately, it’s a work-in-progress so I’ll revisit the topic when the spec and implementation are more solid.

Conclusion

I covered a lot in this article but there’s more to come. The popover attribute can be useful all by itself but some forthcoming web APIs will help cover more use cases. Anchor positioning looks set to be the most useful CSS feature since grid. Stay tuned.

]]>
https://frontendmasters.com/blog/menus-toasts-and-more/feed/ 0 1104