In short
CSS tooltips are small text bubbles that appear when you hover or focus an element. Learn to build them with ::after and attr(), position them, add arrows and fade them in. What is a tooltip?
CSS tooltips are small text bubbles that appear when you hover or focus an element. Learn to build them with ::after and attr(), position them, add arrows and fade them in.
What is a tooltip?
A tooltip is a small label that pops up next to something when you point at it, like a sticky note that appears when you need it. It gives a short hint: "Copy link", "Delete forever", "Your password needs 8 characters".
Browsers already show a plain tooltip for the title attribute, but you can't style it, it appears slowly, and touch and keyboard users never see it. So let's build our own with CSS. There are two common ways:
- A pseudo-element tooltip: the text lives in a
data-tipattribute and CSS shows it with::after. Quick and neat for simple hints. - A real element tooltip: the text lives in a
<span>inside the trigger. A little more HTML, but better for accessibility.
A CSS tooltip with ::after and attr()
The attr() function reads the value of an HTML attribute. Inside content, attr(data-tip) copies the text from data-tip into the pseudo-element. You met ::after in the Pseudo-elements lesson.
<button class="tip" type="button" data-tip="Copy link to clipboard">Copy</button>
.tip {
position: relative;
}
.tip::after {
content: attr(data-tip);
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
z-index: 10;
width: max-content;
max-width: 220px;
padding: 6px 10px;
border-radius: 6px;
background: #111827;
color: white;
font-size: 0.85rem;
line-height: 1.3;
pointer-events: none;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
}
.tip:hover::after,
.tip:focus-visible::after {
opacity: 1;
visibility: visible;
}
Hover the "Copy" button, or Tab to it, and a dark bubble with "Copy link to clipboard" fades in above it, centered. Here is what the key lines do:
position: relativeon the trigger makes it the anchor for the bubble.bottom: calc(100% + 8px)puts the bubble 8px above the trigger.left: 50%plustranslateX(-50%)centers the bubble over the trigger.width: max-contentstops the text from squeezing into a thin column;max-widthmakes long hints wrap.opacityandvisibilitytogether give a smooth fade.display: nonecan't fade, butvisibilitycan be transitioned, and it also makes the hidden bubble invisible to the mouse.pointer-events: nonestops the bubble from blocking clicks on things behind it.
How to add an arrow to a tooltip
The little triangle arrow is a classic CSS trick. An element with zero width and height and thick borders draws four triangles that meet in the middle. Make three borders transparent, and only one triangle is left. We draw it with ::before.
.tip::before {
content: "";
position: absolute;
bottom: calc(100% - 4px);
left: 50%;
transform: translateX(-50%);
z-index: 10;
border: 6px solid transparent;
border-top-color: #111827;
pointer-events: none;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
}
.tip:hover::before,
.tip:focus-visible::before {
opacity: 1;
visibility: visible;
}
Now a small dark triangle points down from the bubble to the button, like a speech bubble in a comic. Only the top border has a color, so we see a triangle pointing down. To point the arrow another way, color a different side: border-bottom-color points up, border-left-color points right, and border-right-color points left.
Tip: The arrow must use the exact same color as the bubble. Change both together, or store the color in one CSS variable like
--tip-bgand use it in both places.
Tooltip positions: top, right, bottom and left
Let's add a data-pos attribute to choose the side. inset: auto resets the top, right, bottom and left values from the base rule, so each position starts clean.
<button class="tip" type="button" data-tip="Shown on the right" data-pos="right">Right</button>
<button class="tip" type="button" data-tip="Shown below" data-pos="bottom">Bottom</button>
<button class="tip" type="button" data-tip="Shown on the left" data-pos="left">Left</button>
/* Right */
.tip[data-pos="right"]::after {
inset: auto;
left: calc(100% + 8px);
top: 50%;
transform: translateY(-50%);
}
.tip[data-pos="right"]::before {
inset: auto;
left: calc(100% - 4px);
top: 50%;
transform: translateY(-50%);
border-color: transparent;
border-right-color: #111827;
}
/* Bottom */
.tip[data-pos="bottom"]::after {
inset: auto;
top: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
}
.tip[data-pos="bottom"]::before {
inset: auto;
top: calc(100% - 4px);
left: 50%;
transform: translateX(-50%);
border-color: transparent;
border-bottom-color: #111827;
}
/* Left */
.tip[data-pos="left"]::after {
inset: auto;
right: calc(100% + 8px);
top: 50%;
transform: translateY(-50%);
}
.tip[data-pos="left"]::before {
inset: auto;
right: calc(100% - 4px);
top: 50%;
transform: translateY(-50%);
border-color: transparent;
border-left-color: #111827;
}
Each button now shows its bubble on a different side, with the arrow pointing back at the button. The pattern is always the same: push the bubble just past one edge with calc(100% + 8px), center it on the other axis with 50% and a translate, and color the arrow border that faces the button.
| Position | Bubble | Center with | Arrow color side |
|---|---|---|---|
| Top | bottom: calc(100% + 8px) | left: 50% + translateX(-50%) | border-top-color |
| Right | left: calc(100% + 8px) | top: 50% + translateY(-50%) | border-right-color |
| Bottom | top: calc(100% + 8px) | left: 50% + translateX(-50%) | border-bottom-color |
| Left | right: calc(100% + 8px) | top: 50% + translateY(-50%) | border-left-color |
Accessible tooltips with aria-describedby
Screen readers do not handle text in pseudo-elements in a reliable way. For tooltips that really matter, put the text in a real element and connect it with aria-describedby. That attribute says "this element is described by the element with this id", so a screen reader reads the button name and then the hint.
<span class="tip-wrap">
<button type="button" aria-describedby="del-tip">Delete</button>
<span class="tip-text" id="del-tip" role="tooltip">Deletes the file forever</span>
</span>
.tip-wrap {
position: relative;
display: inline-block;
}
.tip-text {
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
width: max-content;
max-width: 220px;
padding: 6px 10px;
border-radius: 6px;
background: #111827;
color: white;
font-size: 0.85rem;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s 0.1s, visibility 0.2s 0.1s;
}
.tip-wrap:hover .tip-text,
.tip-wrap:focus-within .tip-text {
opacity: 1;
visibility: visible;
}
It looks the same as before, but now a screen reader says "Delete, button, Deletes the file forever". The 0.1s in the transition is a delay, so the bubble does not flash when the mouse just passes over the button. The tooltip sits next to the button (not inside it), so it does not become part of the button's name. Both live in a wrapper, so :focus-within shows the bubble when the button is focused, and the bubble stays open while the pointer is over the wrapper.
Common mistake: Putting important information only in a tooltip, like the rules for a password or the price of an option. Touch users can't hover, and many people never discover tooltips at all. A tooltip should only add a short extra hint. Anything people need belongs in normal visible text on the page.
A good tooltip follows three simple rules: it appears on hover and keyboard focus, it stays visible while the pointer is over it, and it is short (a few words). For icon-only buttons, the button still needs its own aria-label; the tooltip is an extra, not the name.
Practice exercise
Create a toolbar with three icon buttons: Bold, Italic and Underline (you can use the letters B, I and U). Give each one a tooltip below the button with an arrow pointing up, for example "Bold (Ctrl+B)". Make the tooltips fade in after a short delay.
Hint: use the data-pos="bottom" rules, and add a delay as the second time value in the transition, like transition: opacity 0.2s 0.3s, visibility 0.2s 0.3s;.
Key takeaways
- A tooltip is an absolutely positioned bubble inside a
position: relativetrigger. content: attr(data-tip)on::aftergives a quick tooltip with no extra HTML.- Arrows are zero-size boxes with one colored border, drawn with
::before. - Fade tooltips with
opacityandvisibility, and show them on both:hoverand:focus-visible. - For accessibility, use a real element with
role="tooltip"andaria-describedby, and never hide essential information in a tooltip.