Modern CSS Tooltips And Speech Bubbles (Part 1) — Smashing Magazine

Modern CSS Tooltips And Speech Bubbles (Part 1) — Smashing Magazine

In a previous article, we explored ribbon shapes and different ways to approach them using clever combinations of CSS gradients and clip-path(). This time, I’d like to explore another shape, one that you’ve likely had to tackle at least once in your front-end life: tooltips. You know what we’re talking about, those little things that look like speech bubbles from comic books. They’re everywhere in the wild, from a hover effect for buttons to the text messaging app on your phone.

The shapes may look easy to make in CSS at first glance, but it always ends with a lot of struggles. For example, how do you adjust the position of the tail to indicate whether the tooltip is coming from a left, right, or center position? There are plenty of considerations to take into account when making tooltips — including overflowage, collision detection, and semantics — but it’s the shape and direction of the tail that I want to focus on because I often see inflexible fixed units used to position them.

Forget what you already know about tooltips because in this article, we will start from zero, and you will learn how to build a tooltip with minimal markup powered by modern CSS that provides flexibility to configure the component by adjusting CSS variables. We are not going to build one or two shapes, but… 100 different shapes!

That may sound like we’re getting into a super-long article, but actually, we can easily get there by adjusting a few values. In the end, you will have a back pocket full of CSS tricks that can be combined to create any shape you want.

And guess what? I’ve already created an online collection of 100 different tooltip shapes where you can easily copy and paste the code for your own use, but stay with me. You’re going to want to know the secret to unlocking hundreds of possibilities with the least possible code.

We’ll start with the shapes themselves, discussing how we can cut out the bubble and tail by combining CSS gradients and clipping. Then, we’ll pick things back up in a second article dedicated to improving another common approach to tooltips using borders and custom shapes.

The HTML

We’re only working with a single element:

<div class="tooltip">Your text content goes here</div>

That’s the challenge: Create hundreds of tooltip variations in CSS with only a single element to hook into in the HTML.

I’m going to skip right over the basic rectangular shape; you know how to set a width and height (or aspect-ratio) on elements. Let’s start with the simplest shape for the tooltip’s tail, one that can be accomplished with only two CSS properties:

.tooltip {
  /* tail dimension */
  --b: 2em; /* base */
  --h: 1em; /* height*/

  border-image: fill 0 // var(--h)
    conic-gradient(#CC333F 0 0); /* the color  */
  clip-path: 
    polygon(0 100%, 0 0, 100% 0, 100% 100%,
      calc(50% + var(--b) / 2) 100%,
      50% calc(100% + var(--h)),
      calc(50% - var(--b) / 2) 100%);
}

The border-image property creates an “overflowing color” while clip-path defines the shape of the tooltip with polygon() coordinates. (Speaking of border-image, I wrote a deep-dive on it and explain how it might be the only CSS property that supports double slashes in the syntax!)

The tooltip’s tail is placed at the bottom center, and we have two variables to control its dimensions:

Illustrating the border-image coloration and the clip-path polygon()
(Large preview)

We can do the exact same thing in more intuitive ways, like defining a background and then border (or padding) to create space for the tail:

background: #CC333F;
border-bottom: var(--h) solid #0000;

…or using box-shadow (or outline) for the outside color:

background: #CC333F;
box-shadow: 0 0 0 var(--h) #CC333F;

While these approaches are indeed easier, they require an extra declaration compared to the single border-image declaration we used. Plus, we’ll see later that border-image is really useful for accomplishing more complex shapes.

Here is a demo with the different directions so you can see how easy it is to adjust the above code to change the tail’s position.

See the Pen (A simple Tooltip using 2 CSS properties)(https://codepen.io/smashingmag/pen/ExrEXoO) by Temani Afif.

See the Pen A simple Tooltip using 2 CSS properties by Temani Afif.

Next, we’re going to study shapes that include the tail at the bottom, but you can easily find the other variations in my online collection.

Adjusting The Tail Position

Let’s add a third variable, --p, that we can use to control the tooltip’s tail position. In the last example, we used 50% in the clip-path, which positions the tail directly in the horizontal center along the bottom of the tooltip’s rectangular shape. If we assign a variable to it, we can easily change the direction of the tooltip to face left or right by updating 50% to a smaller or larger value, respectively.

.tooltip {
  /* tail dimension */
  --b: 2em; /* base */
  --h: 1em; /* height*/
  --p: 50%; /* tail position */   

  border-image: fill 0 // var(--h)
    conic-gradient(#CC333F 0 0); /* the color  */
  clip-path: 
    polygon(0 100%, 0 0, 100% 0, 100% 100%,
      calc(var(--p) + var(--b) / 2) 100%,
      var(--p) calc(100% + var(--h)),
      calc(var(--p) - var(--b) / 2) 100%);
}

The --p variable can go from 0% to 100%, where 0% is aligned with the left side of the tooltip and 100% is aligned with the right side. Here is an interactive demo where you can update the variable using a range slider:

See the Pen (Updating the tail position)(https://codepen.io/smashingmag/pen/mdoLOGJ) by Temani Afif.

See the Pen Updating the tail position by Temani Afif.

Nice, right?! It’s definitely cool, but there’s a glitch. When the tail’s position is set to the extremes, it appears to slide right off the edge of the bubble. Go ahead and toggle the range slider in the demo between 0% and 100% to see the issue.

Red rectangle tooltip where the tail of the tooltip is overflowing the container.
The tooltip’s tail is allowed to overflow its container at the extremes. (Large preview)

We can fix this by setting limits to some values so the tail never falls outside the container. Two points of the polygon are concerned with the fix.

This:

calc(var(--p) + var(--b) / 2) 100%

…and this:

calc(var(--p) - var(--b) / 2) 100%

The first calc() needs to be clamped to 100% to avoid the overflow from the right side, and the second one needs to be clamped to 0% to avoid the overflow from the left side. We can use the min() and max() functions to establish the range limits:

clip-path: 
  polygon(0 100%, 0 0, 100% 0, 100% 100%,
    min(100%, var(--p) + var(--b) / 2) 100%,
    var(--p) calc(100% + var(--h)),
    max(0%, var(--p) - var(--b) / 2) 100%);

See the Pen (Fixing the overflow issue)(https://codepen.io/smashingmag/pen/mdoLRVr) by Temani Afif.

See the Pen Fixing the overflow issue by Temani Afif.

Tada! We’ve fixed the edge cases, and now the tail gets a different shape instead of overflowing!

Illustrating the tail’s at the left and right edges of the tooltip
(Large preview)

Adjusting The Tail Shape

Let’s integrate another variable, --x, into the clip-path() and use it to adjust the shape of the tail:

.tooltip {
  /* tail dimension */
  --b: 2em; /* base */
  --h: 1em; /* height*/

  --p: 50%;  /* tail position */
  --x: -2em; /* tail shape */

  border-image: fill 0 // 9999px
    conic-gradient(#CC333F 0 0); /* the color  */
  clip-path: 
    polygon(0 100%, 0 0, 100% 0, 100% 100%,
      min(100%, var(--p) + var(--b) / 2) 100%,
      calc(var(--p) + var(--x)) calc(100% + var(--h)),
      max(0%, var(--p) - var(--b) / 2) 100%);
}

The --x variable can be either positive or negative (using whatever unit you want, including percentages). What we’re doing is adding the variable that establishes the tail’s shape, --x, to the tail’s position, --p. In other words, we’ve updated this:

var(--p) calc(100% + var(--h))

…to this:

calc(var(--p) + var(--x)) calc(100% + var(--h))

And here is the outcome:

Two variations of the same red rectangular tooltip, one with a tail in the left direction and one with the tail in the right position.
(Large preview)

The tooltip’s tail points in either the right or left direction, depending on whether --x is a positive or negative value. Go ahead and use the range sliders in the following demo to see how the tooltip’s tail is re-positioned (--p) and re-shaped (--x) when adjusting two variables.

See the Pen (Updating the tail shape)(https://codepen.io/smashingmag/pen/ExMLZZB) by Temani Afif.

See the Pen Updating the tail shape by Temani Afif.

Cool, right? If you’ve ever attempted tooltips on your own, I’m sure you will appreciate the way this approach eliminates the need to use magic numbers to tweak the tooltip’s appearance. That’s one significant headache we no longer have to worry about!

And did you notice how the tail, when stretched, is allowed to overflow the container? That’s perfect! Using min() and max(), we’re correctly fixing the overflow issue while allowing the tail to stretch further away from the container.

Two variations of the same red rectangular tooltip, one with a tail in the left direction and one with the tail in the right position.
(Large preview)

Note that I have updated the border-image outset to an impractically large value (9999px) instead of using the --h variable. The shape of the tail can be any type of triangle and can take a bigger area. Since there’s no way for us to know the exact value of the outset, we use that big value to make sure we have enough room to fill the tail in with color, no matter its shape.

Does the outset concept look strange to you? I know that working with border-image isn’t something many of us do all that often, so if this approach is tough to wrap your head around, definitely go check out my border-image article for a thorough demonstration of how it works.

Working With Gradients

Most of the trouble starts when we want to color the tooltip with a gradient instead of a flat color. Applying one color is simple — even with older techniques — but when it comes to gradients, it’s not easy to make the tail color flow smoothly into the container’s color.

But guess what? That’s no problem for us because we are already using a gradient in our border-image declaration!

border-image: fill 0 // var(--h)
  conic-gradient(#CC333F 0 0);

border-image only accepts gradients or images, so to produce a solid color, I had to use a gradient consisting of just one color. But if you change it into a “real” gradient that transitions between two or more colors, then you get your tooltip gradient. That’s all!

See the Pen (Adding gradient coloration)(https://codepen.io/smashingmag/pen/GRedryE) by Temani Afif.

See the Pen Adding gradient coloration by Temani Afif.

The only thing we need to pay attention to is the outset value. When using one color, we don’t really care what the outset value is; it just needs to be as big as possible to cover the clip-path area, as we did when setting it to 9999px. However, when working with multiple colors, we should not use too big of a value so that we avoid clipping the gradient by accident.

In the last demo, you will notice I am using a value equal to 0 0 var(--h) 0, which means that we are setting only a bottom outset; the tail is at the bottom, and the gradient will not extend in all the directions as it did in the other examples. I don’t want to get into all of the various edge cases that could come up, but if you have trouble working with the gradient’s color, it’s usually the border-image’s outset value that you need to check.

Working With Rounded Corners

If we try to add a border-radius to the previous examples, nothing happens. That’s because the border-radius and border-image properties aren’t really all that good of friends. We need to tweak border-image and combine it with background to get things working right.

Illustrating the three steps needed to create rounded tooltip corners in sequential order.
(Large preview)

We start by declaring a background and border-radius on the .tooltip. Nothing fancy. Then, we move to the border-image property so that we can add a bar (highlighted in red in the last figure) that slightly overflows the container from the bottom. This part is a bit tricky, and here I invite you to read my previous article about border-image to understand this bit of CSS magic. From there, we add the clip-path and get our final shape.

.tooltip {
  /* triangle dimension */
  --b: 2em; /* base */
  --h: 1em; /* height */

  --p: 50%; /* position  */
  --r: 1.2em; /* the radius */
  --c: #4ECDC4;

  border-radius: var(--r);
  clip-path: polygon(0 100%, 0 0, 100% 0, 100% 100%,
    min(100%, var(--p) + var(--b) / 2) 100%,
    var(--p) calc(100% + var(--h)),
    max(0%, var(--p) - var(--b) / 2) 100%);
  background: var(--c);
  border-image: conic-gradient(var(--c) 0 0) fill 0/
    var(--r) calc(100% - var(--p) - var(--b) / 2) 0 calc(var(--p) - var(--b) / 2)/
    0 0 var(--h) 0;
}

See the Pen (Adding border radius)(https://codepen.io/smashingmag/pen/MWxGvYg) by Temani Afif.

See the Pen Adding border radius by Temani Afif.

We are good but still have a tiny issue when the tail gets close to the extreme edges.

Showing two versions of the same tooltip where the tail overflows the container on the left and right edges, respectively, creating a jagged edge between the tail and tooltip.
(Large preview)

This visual glitch happens when the border-image overlaps with the rounded corners. To fix this, we need to adjust the border-radius value based on the tail’s position (--p).

We are not going to update all the radii, only the bottom ones and, more precisely, the horizontal values. I want to remind you that border-radius accepts up to eight values — each corner takes two values that set the horizontal and vertical directions — and in our case, we will update the horizontal value of the bottom-left and bottom-right corners:

border-radius:
  /* horizontal values */
  var(--r) 
  var(--r) 
  min(var(--r),100% - var(--p) - var(--b)/2) /* horizontal bottom-right */
  min(var(--r),var(--p) - var(--b)/2) /* horizontal bottom-left */
  /
  /* vertical values */
  var(--r)
  var(--r)
  var(--r)
  var(--r)

All the corner values are equal to --r, except for the bottom-left and bottom-right corners. Notice the forward slash (/), as it is part of the syntax that separates the horizontal and vertical radii values.

Now, let’s dig in and understand what is happening here. For the bottom-left corner, when the position of the tail is on the right, the position (--p) variable value will be big in order to keep the radius equal to the radius (--r), which serves as the minimum value. But when the position gets closer to the left, the value of --p decreases and, at some point, becomes smaller than the value of --r. The result is the value of the radius slowly decreasing until it reaches 0. It adjusts as the position updates!

I know that’s a lot to process, and a visual aid usually helps. Try slowly updating the tail’s position in the following demo to get a clearer picture of what’s happening.

See the Pen (Fixing the edge cases)(https://codepen.io/smashingmag/pen/ZEPoJpG) by Temani Afif.

See the Pen Fixing the edge cases by Temani Afif.

What about instances when we want a custom shape for the tail? The technique we just used will only work when the tail has two equal sides — you know, an isosceles triangle. We need to adjust the border-image value and consider another trick to get things working correctly again.

Illustrating three steps for correcting the tooltip’s overflow when the tail is a custom shape.
(Large preview)

This time, the border image creates a horizontal bar along the bottom that is positioned directly under the element and extends outside of its boundary so that we have enough color for the tail when it’s closer to the edge.

.tooltip {
  /* tail dimension */
  --b: 2em; /* base */
  --h: 1.5em; /* height */

  --p: 50%; /* position */
  --x: 1.8em; /* tail position */
  --r: 1.2em; /* the radius */
  --c: #4ECDC4;

  border-radius: var(--r) var(--r) min(var(--r), 100% - var(--p) - var(--b) / 2) min(var(--r), var(--p) - var(--b) / 2) / var(--r);
  clip-path: polygon(0 100%, 0 0, 100% 0, 100% 100%,
    min(100%, var(--p) + var(--b) / 2) 100%,
    calc(var(--p) + var(--x)) calc(100% + var(--h)),
    max(0%, var(--p) - var(--b) / 2) 100%);
  background: var(--c);
  border-image: conic-gradient(var(--c) 0 0) 0 0 1 0 / 0 0 var(--h) 0 / 0 999px var(--h) 999px;
}

See the Pen (Custom tail with border radius)(https://codepen.io/smashingmag/pen/MWxGEpv) by Temani Afif.

See the Pen Custom tail with border radius by Temani Afif.

Again, the border-image declaration looks strange and difficult because, well, it is! Please do yourself a favor and check my previous article if you want to dig deeper into this approach — you definitely won’t regret it.

“Why not use this approach for the first example we looked at?” you might ask. You are right that we can use this same approach for the first example, even if we don’t have the --x variable. That said, the reason we’re not going in that direction is that there is a tiny drawback to it in some particular cases, as you can see in the figure below.

Showing visual imperfections in the tooltip, including a gap between the tail and the tooltip, and a slight color bleed at the left and right edges of the container.
(Large preview)

That’s why I do not use this approach when working with a simple isosceles triangle. This said, the method is perfectly fine, and in most cases, you may not see any visual glitches.

Putting Everything Together

We’ve looked at tooltips with tails that have equal sides, ones with tails that change shape, ones where the tail changes position and direction, ones with rounded corners, and ones that are filled in with gradients. What would it look like if we combined all of these examples into one mega-demo?

We can do it, but not by combining the approaches we’ve covered. We need another method, this time using a pseudo-element. No border-image for this one, I promise!

.tooltip {
  /* triangle dimension */
  --b: 2em; /* base */
  --h: 1em; /* height */

  --p: 50%; /* position */
  --r: 1.2em; /* the radius */

  border-radius: var(--r) var(--r) min(var(--r), 100% - var(--p) - var(--b) / 2) min(var(--r), var(--p) - var(--b) / 2) / var(--r);
  background: 0 0 / 100% calc(100% + var(--h)) 
    linear-gradient(60deg, #CC333F, #4ECDC4); /* the gradient */
  position: relative;
  z-index: 0;
}
.tooltip:before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 0 0 calc(-1*var(--h));
  background-image: inherit;
  clip-path: 
    polygon(50% 50%,
      min(100%, var(--p) + var(--b) / 2) calc(100% - var(--h)),
      var(--p) 100%,
      max(0%, var(--p) - var(--b) / 2) calc(100% - var(--h)));
}

The pseudo-element is used to create the tail at the bottom and notice how it inherits the gradient from the main element to simulate a continuous gradient that covers the entire shape.

Showing the tail at bottom that inherits the gradient from the main element
(Large preview)

Another important thing to note is the background-size declared in the .tooltip. The pseudo-element is covering a bigger area due to the negative bottom value, so we have to increase the height of the gradient so it covers the same area.

See the Pen (Gradient and border radius)(https://codepen.io/smashingmag/pen/ZEPoayw) by Temani Afif.

See the Pen Gradient and border radius by Temani Afif.

For the custom tail shape, we have to update the code slightly to consider the overflow on the left and right sides of the tooltip. The idea is to increase the gradient’s area when the tail is about to leave the container.

.tooltip {
  --p: 50%;  /* position  */
  --x: -2em; /* tail shape and direction */

  --_e: max(0%, -1 * var(--x) - var(--p), var(--x) + var(--p) - 100%);
  
  background:
    50% 0 / calc(100% + 2*var(--_e)) calc(100% + var(--h)) 
    linear-gradient(60deg, #CC333F, #4ECDC4); /* the gradient */
}
.tooltip:before {
  inset: 0 calc(-1 * var(--_e)) calc(-1 * var(--h));
  padding-inline: var(--_e);
}

Alongside the --x variable that controls the tail’s shape and direction, I have introduced a new variable, --_e, that defines the gradient’s width for covering the .tooltip as well as the pseudo-element’s inline padding and its left and right values. It may look like a complex configuration, but the idea is that --_e will, in most cases, be equal to 0, which gives us the same code as the last example we made. But when the tail overflows the .tooltip container, the --_e value increases, which increases the area of the gradient as well in order to cover the overflow.

Play with the position and shape of the tail in the following demo and notice how the gradient changes when the tail overflows the sides.

See the Pen (Custom tail with border radius and gradient)(https://codepen.io/smashingmag/pen/RwdyExJ) by Temani Afif.

See the Pen Custom tail with border radius and gradient by Temani Afif.

I know this last code may look complex (same for some of the previous ones), and for this reason, I created an online collection of tooltips from where you can easily grab the code. I’ve tried to cover as many cases as possible, even the ones you will probably never need. That said, it’s good to have an idea of how to build various tooltip shapes.

One Last Thought

If we do the count, we have made 32 different tooltip shapes. That’s two types of color (solid or gradient), two types of corners (sharp or rounded) that produce four more variations, and two types of tail shapes (isosceles triangle and custom) for two additional variations, and four different tail positions (top, bottom, left, and right) which brings the final tally to 32 tooltip variations.

The last example we studied can be used to produce all the shapes simply by adjusting the different variables.

I know what you’re thinking: Why didn’t I simply share the last snippet and call it a day? Did this article really have to be so long when we could have jumped straight into the solution?

Sure, we could have done that, but If you compare the first example with only two CSS properties with the last example, the code for the last example is far too complex to create what can otherwise be accomplished in fewer lines. We started with a basic tooltip shape and embarked on a journey to make it account for more complex types of tooltips. Plus, we have learned a lot of tricks that can be useful in other situations and not necessarily for creating tooltips.

Conclusion

That’s all for Part 1 of this brief two-part series. We still have many more shapes to cover in Part 2, so take the time to digest what we covered in Part 1 before jumping ahead. In fact, here’s a little homework to help prepare you for Part 2: try creating the following tooltips using the CSS tricks you learned from this article.

Showing four different tooltip shapes
(Large preview)

Can you figure it out? The code for all of them is included in my tooltip collection if you need a reference, but do try to make them yourself — it’s good exercise! Maybe you will find a different (or perhaps better) approach than mine.

Smashing Editorial
(gg, yk)

]
منبع: https://smashingmagazine.com/2024/03/modern-css-tooltips-speech-bubbles-part1/