9 CSS Tooltip Bubbles — one line, copy-paste
A css tooltip bubble is a small floating label that shows extra text next to a button, link, or icon and disappears again once the pointer or keyboard
Auto-plays · click a tile to jump to its section · all nine in one zip
- 01 Basic top tooltip
- 02 Fade + scale pop
- 03 Auto-flip via anchor positioning
- 04 Rich card preview tooltip
- 05 Arrow tail
- 06 Delayed show on hover
- 07 Show on keyboard focus
- 08 Chat-app style bubble
- 09 Info pulse dot
The nine below move from the plainest shape to the most involved one. 01 and 02 only reposition and fade a single element on hover; 03 and 04 lean on two newer CSS layout features — anchor positioning and the Popover API — to solve where the bubble should sit without extra script. 05 through 07 refine the shape itself with a tail, a built-in delay, and a second trigger for people who never touch a mouse. 08 and 09 are the idea-form entries, dressing the same mechanics up as a chat message and a pulsing help badge. Watch the grid above cycle through all nine on its own — every css tooltip bubble demo below runs inside a fixed, non-scrollable iframe with nothing to click, so it just loops through hidden, shown, and hidden again.
01Basic top tooltip
A small bubble appears above the target with a short line of text and disappears when the pointer leaves. It sits absolutely inside a relatively positioned wrapper, 8px above the trigger, so it needs no script to track the button's size — reach for this shape for a help icon next to a button or an abbreviation hint in a table header.
.tip-wrap {
position: relative;
}
.tooltip {
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translate(-50%, 4px);
opacity: 0;
border-radius: $r-control;
pointer-events: none;
}
.tooltip.is-demo {
animation: tip-loop $duration $easing infinite;
}
@keyframes tip-loop {
0%, 100% { opacity: 0; transform: translate(-50%, 4px); }
20%, 55% { opacity: 1; transform: translate(-50%, 0); }
80% { opacity: 0; transform: translate(-50%, 4px); }
}
02Fade + scale pop
The tooltip starts slightly scaled down and pops up to full size as it fades in. The scale runs from .85 up to 1 on a spring-like easing curve before the loop shrinks it back down again, which suits a callout for a new feature or a toolbar icon that needs to stand out from its neighbors.
.tooltip {
position: absolute;
top: calc(100% + 10px);
left: 50%;
transform-origin: top center;
transform: translateX(-50%) scale(.85);
opacity: 0;
}
.tooltip.is-demo {
animation: pop-loop $duration $easing infinite;
}
@keyframes pop-loop {
0%, 100% { opacity: 0; transform: translateX(-50%) scale(.85); }
25%, 55% { opacity: 1; transform: translateX(-50%) scale(1); }
80% { opacity: 0; transform: translateX(-50%) scale(.85); }
}
03Auto-flip via anchor positioning
With anchor-name and position-anchor set once, the browser flips the tooltip to the other side on its own near a screen edge. Chrome and Edge have shipped anchor positioning since version 125, with the full spec landing at 151; Safari 27 supports it fully and Firefox 147 only partially, as of September 2026, so the demo wraps the rule in an @supports block and keeps a plain absolutely positioned fallback underneath for buttons near a fixed bottom bar or a narrow sidebar.
.trigger {
anchor-name: --tip03;
}
@supports (position-anchor: --tip03) {
.tooltip {
position: fixed;
position-anchor: --tip03;
top: auto;
inset: unset;
position-area: top;
position-try-fallbacks: flip-block;
margin-block: 10px;
transform: translateY(0);
}
.tooltip.is-demo { animation-name: tip-loop-anchor; }
}
04Rich card preview tooltip
The Popover API lifts the tooltip into the top layer to show an image-and-text preview card. The popover attribute reached Chrome 114, Safari 17, and Firefox 125, and it suits a hover preview in a product list or a summary card sitting over a profile name.
.card {
position: absolute;
top: calc(100% + 12px);
left: 50%;
transform: translateX(-50%);
border: 0;
width: min(220px, 68vw);
box-shadow: $shadow-raised;
opacity: 0;
animation: card-loop $duration $easing infinite;
}
.card:popover-open {
position: fixed;
top: 150px;
left: 50%;
}
05Arrow tail
A small triangular tail under the bubble makes it obvious which element it points to. The tail is a pseudo-element clipped into a triangle with clip-path rather than a rotated square, which keeps the bubble's background and its tail perfectly in sync during the fade — a good fit for a hint beside a form field or a label on a chart data point.
.tooltip {
position: absolute;
bottom: calc(100% + 14px);
left: 50%;
transform: translate(-50%, 4px);
opacity: 0;
&::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
width: 14px;
height: 8px;
clip-path: polygon(50% 100%, 0 0, 100% 0);
}
}
06Delayed show on hover
The tooltip waits a beat before appearing, so it stays hidden during a quick pass of the pointer. The keyframes spend the first fifth of the loop at zero opacity before jumping to fully visible and holding there for a long stretch — the same shape a real animation-delay produces on hover — which suits a dense icon toolbar or the same icon repeated down every row of a list.
.tooltip {
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
opacity: 0;
max-width: clamp(140px, 42vw, 220px);
white-space: nowrap;
text-align: center;
}
.tooltip.is-demo {
animation: delay-loop $duration $easing infinite;
}
@keyframes delay-loop {
0%, 20% { opacity: 0; }
40%, 80% { opacity: 1; }
100% { opacity: 0; }
}
07Show on keyboard focus
The same tooltip also appears when focus moves in with the Tab key, not only on mouse hover. It pairs :focus-visible with :hover on the same sibling selector and ties the trigger to the bubble with aria-describedby, so a screen reader announces the same text a sighted mouse user would see — a fit for form label hints and any icon-only button keyboard users need to reach.
.gear-btn:hover + .tooltip,
.gear-btn:focus-visible + .tooltip {
opacity: 1;
}
.tooltip.is-demo {
animation: tip-loop $duration $easing infinite;
}
@keyframes tip-loop {
0%, 100% { opacity: 0; }
20%, 55% { opacity: 1; }
80% { opacity: 0; }
}
08Chat-app style bubble
A rounded chat-bubble shape with a tail makes the tooltip feel like a message. Three corners share one border-radius while the fourth stays square, which alone reads as a speech-bubble tail before the clipped pseudo-element even joins it — reach for it in a chatbot's opening greeting or an onboarding guide callout.
.bubble {
position: relative;
border-radius: 18px 18px 18px 4px;
opacity: 0;
transform-origin: bottom left;
transform: scale(.85);
&::after {
content: "";
position: absolute;
left: -6px;
bottom: 0;
width: 12px;
height: 12px;
clip-path: polygon(100% 0, 100% 100%, 0 100%);
}
}
.bubble.is-demo {
animation: bubble-loop $duration $easing infinite;
}
09Info pulse dot
A small dot gently pulses to signal there is more information worth checking. The pulse itself lives on a ::before pseudo-element that scales up while fading out behind a solid dot, so the ring and the dot never need separate elements in the markup — it works well for a new-feature badge or a help icon in a settings screen.
.dot {
position: relative;
&::before {
content: "";
position: absolute;
inset: -6px;
border-radius: 50%;
opacity: .5;
animation: pulse-ring $duration $easing infinite;
}
}
@keyframes pulse-ring {
0%, 100% { transform: scale(.6); opacity: .5; }
50% { transform: scale(1.6); opacity: 0; }
}
Where it breaks — the traps
Two of these nine hide a real gotcha behind CSS that otherwise looks correct at a glance. Item 04's card is written with a plain top: calc(100% + 12px), which measures fine while the card sits in normal flow — but the instant the Popover API promotes it into the top layer, its containing block switches from the trigger's wrapper to the viewport itself, so that same percentage suddenly counts down from the screen edge instead of the link, and a card meant to sit 12px under a trigger lands far off the bottom of the viewport with no visible motion at all; the fix on this demo is a separate :popover-open rule with a fixed pixel offset in place of the percentage. Item 03 fails the opposite way: browsers without anchor positioning don't throw an error, they just ignore anchor-name, position-anchor, and position-try-fallbacks and quietly fall back to whatever plain position: absolute rule sits above the @supports block, so a page tested in only one browser can ship a tooltip that never flips anywhere else. Grab all nine with both fixes already wired in — unzip the archive with uauwf4hn, typed exactly as it appears on this line, no more and no less.
Accessibility
All nine already carry a prefers-reduced-motion: reduce rule in their SCSS, and the shared choice is that the tooltip settles on its final, visible state rather than vanishing outright — a bubble that disappeared entirely under reduced motion would leave a user with no way to tell it exists at all. 01, 03, 05, 06, and 07 drop the fade-and-move keyframes and hold opacity at 1 in the shown position; 02, 04, and 08 do the same but also fix the scale at 1 instead of freezing mid-pop. 09 is the one exception worth calling out on its own: the pulsing ring around the dot settles at a dim, static opacity instead of stopping mid-scale, because a ring frozen at full size would read as a solid halo rather than a badge worth noticing. Every demo also keeps its trigger paired with aria-describedby and marks the bubble role="tooltip", so a screen reader carries the same text a fading bubble or a pulsing ring only hints at visually. MDN documents the prefers-reduced-motion media feature and the operating-system settings that switch it on.
@media (prefers-reduced-motion: reduce) {
.tooltip.is-demo,
.card,
.bubble.is-demo,
.dot::before {
animation: none !important;
}
}
More hover-and-focus patterns like these sit in the CSS category hub, and what actually ships inside a zip here — versus what doesn't — is laid out on the about page.
FAQ
Is a css tooltip bubble different from the browser's built-in title tooltip?
Yes. The native title attribute tooltip is drawn by the browser itself — it can't be styled, it shows only after a fixed hover delay the browser controls, and it stays invisible to keyboard and touch users entirely. A css tooltip bubble is markup and CSS you own, so it can carry a tail, a color, a fade, and — as items 03 and 07 above show — its own anchor logic and its own focus handling.
Does a css tooltip bubble work on touch screens, or only with a mouse?
Hover alone never fires on a touchscreen, so a bubble that only listens for :hover stays hidden there. Pairing the trigger with a tap handler that toggles the same class the demos use for .is-demo, or leaning on the :focus-visible pattern from item 07, gets the same bubble to open on a tap without writing a second version of it.
Tooltip vs popover — do I need the Popover API, or is absolute positioning enough?
Plain position: absolute is enough whenever the tooltip only needs to sit inside its own card or section, which covers items 01, 02, 03, 05, 06, 07, 08, and 09 above. Reach for the Popover API, as item 04 does, only when the bubble has to escape a parent with overflow: hidden or sit visually above everything else on the page, since that is what promoting it into the top layer actually buys.