Creating a pure CSS tooltip

Tooltip example sketch

In this post we'll be creating our very own custom tooltip with pure CSS. Lots of sites use Javascript to achieve this effect, and perhaps if extra functionality is required this may be the right choice, but often, you don't need Javascript and will be much better off without it.

The HTML

Of course, we want semantic and clean HTML. So ideally, we would want something like:

<button data-tooltip="with tooltip!">
	I'm a button
</button>

So let's just work with that! After all, you don't want to be writing extra HTML every time you need a tooltip somewhere.

The CSS magic

When do we want our tooltip CSS to apply? Well, only to elements that have the tooltip attribute set. So, the appropriate selector would simply be [data-tooltip] (see the CSS3 specifications for more info). Now to create the tooltip, we would need a psuedoelement. Let's just pick ::after. Then, we want the content of that psuedoelement to be the text from the data-tooltip attribute. Let's see what that does:

<button data-tooltip="with tooltip!"> I'm a button </button>
[data-tooltip]::after { content: attr(data-tooltip); } /* Some styles for the button itself */ body { display: flex; align-items: center; justify-content: center; margin: 0; height: 100vh; } button { background-color: orange; border-radius: 6px; padding: .5em 1em; border-width: 0; cursor: pointer; font: 20px / 1.5 sans-serif; outline: none; }

Now, the tooltip text just gets appended after the text that was already in the button, because we didn't do any styling. We only want the tooltip to appear when we mouse over the button, and so it needs to be seperate of the button. Let's not worry about the hover for now, but just build and style the element first. We will position it absolutely, and to make that work properly, we should give the button itself a positioning other than static (the default). We also want the tooltip to function like an inline-block (since we want the width to fit the text contents). So, let's try that:

<button data-tooltip="with tooltip!"> I'm a button </button>
[data-tooltip]{ position: relative; } [data-tooltip]::after { content: attr(data-tooltip); position: absolute; bottom: 100%; left: 0; display: inline-block; background-color: dimgray; color: white; } /* Some styles for the button itself */ body { display: flex; align-items: center; justify-content: center; margin: 0; height: 100vh; } button { background-color: orange; border-radius: 6px; padding: .5em 1em; border-width: 0; cursor: pointer; font: 20px / 1.5 sans-serif; outline: none; }

That looks a lot like it already! Now we just center the element relative to the button by setting left: 50%; and transform: translateX(-50%);. Then add some padding to the box, because who doesn't want some extra space. Slap on a border-radius to match the button, and offset it slightly so that there's some space between the pseudo-element and its parent. And, we only want the text to get linebreaks when we tell it to, so get a white-space: nowrap; in there as well.

Now, we want the tooltip to have that little arrow pointing at the thing it's a tooltip for, so let's make that as well. We will use the ::before pseudo-element, and we will create the arrow using borders of different colors and different widths. Then, we just position it right under the tooltip box.

<button data-tooltip="with tooltip!"> I'm a button </button>
[data-tooltip]{ position: relative; } [data-tooltip]::before { content: ""; position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); display: block; border-top: 8px solid dimgray; border-right: 8px solid transparent; border-bottom: 0 solid transparent; border-left: 8px solid transparent; margin-bottom: 10px; } [data-tooltip]::after { content: attr(data-tooltip); position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); display: inline-block; background-color: dimgray; color: white; padding: .5em 1em; white-space: nowrap; margin-bottom: 18px; border-radius: 4px; } /* Some styles for the button itself */ body { display: flex; align-items: center; justify-content: center; margin: 0; height: 100vh; } button { background-color: orange; border-radius: 6px; padding: .5em 1em; border-width: 0; cursor: pointer; font: 20px / 1.5 sans-serif; outline: none; }

If you don't see how the borders create that little arrow, try giving each border side (top, right, bottom, left) a different color. All that's left to do now is hide it when it's not being hovered and boom! We got ourselves a working pure CSS tooltip! Since we don't want the actual tooltip hoverable (after all, it is a child of the button), we will set pointer-events: none;. Now we simply set the opacity to zero whenever the element is not being hovered. We can also move the hidden tooltip down a little bit to let the tooltip look like it is coming from the button. This is what we get:

<button data-tooltip="with tooltip!"> I'm a button </button>
[data-tooltip]{ position: relative; } [data-tooltip]::before { content: ""; position: absolute; bottom: 100%; left: 50%; transform: translate(-50%, 10px); display: block; border-top: 8px solid dimgray; border-right: 8px solid transparent; border-bottom: 0 solid transparent; border-left: 8px solid transparent; margin-bottom: 10px; pointer-events: none; transition: .3s; opacity: 0; } [data-tooltip]::after { content: attr(data-tooltip); position: absolute; bottom: 100%; left: 50%; transform: translate(-50%, 10px); display: inline-block; background-color: dimgray; color: white; padding: .5em 1em; white-space: nowrap; margin-bottom: 18px; border-radius: 4px; pointer-events: none; transition: .3s; opacity: 0; } [data-tooltip]:hover::before { opacity: 1; transform: translateX(-50%); } [data-tooltip]:hover::after { opacity: 1; transform: translateX(-50%); } /* Some styles for the button itself */ body { display: flex; align-items: center; justify-content: center; margin: 0; height: 100vh; } button { background-color: orange; border-radius: 6px; padding: .5em 1em; border-width: 0; cursor: pointer; font: 20px / 1.5 sans-serif; outline: none; }

So, that's that done! Of course, feel free to throw any styles you want at this - it's your party!

Thoughts

One of the points I'm trying to convey in this post is to only use Javascript if you really have to. It makes your code more semantic, creates less garbage in your JS files, animations (can) get smoother, and works for users that have Javascript turned off. However, CSS can't do everything. So sometimes you need it - even with this tooltip, some extra functionality may require it. For examle, one issue this tooltip has is that when the button is at the very top of the screen, the tooltip renders off-screen. You would need Javascript to make sure the tooltip moves to the other side of the button in this case. However, if you can get away with not using Javascript, do it.