GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

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 focus

Auto-plays · click a tile to jump to its section · all nine in one zip

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 that turns the box into a css balloon tooltip, 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. For click responses in the wider sense, the micro interactions CSS set gathers them, and turning the pointer's whereabouts itself into the effect is what the custom cursor CSS effects do.

01Basic top tooltip

How this css tooltip animation runs

A small bubble appears above the target with a short line of text and disappears when the pointer leaves. This round layers a one-shot entry spring on top: scale overshoots to 1.12 before settling through a damped rotation wobble of 2.6°→-1.5°→0.6° around the arrow-side edge.

Where it fits

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.

Basic top tooltip
Open in a new tab

01 · Basic top tooltip — Measured area 4.959% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

상단 8px 간격opacity + translate 4px200ms
.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

How it works

The tooltip starts slightly scaled down and pops up to full size as it fades in. The pop now overshoots to 1.14 before a damped rotation wobble of -2.4°→1.4°→-0.5° around the top edge settles it.

Where it fits

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.

Fade + scale pop
Open in a new tab

02 · Fade + scale pop — Measured area 5.799% · moving frames 11/23Original by GODRICH · SCSS and React ship in the zip · See the password

scale(.9)→1cubic-bezier 팝180ms
.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

How it works

With anchor-name and position-anchor set once, the browser flips the tooltip to the other side on its own near a screen edge. Both the fallback and the anchored placement now share the same entry spring — scale overshoots to 1.1, then a damped rotation wobble of 2.2°→-1.3°→0.5° settles it.

Where it fits

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.

Auto-flip via anchor positioning
Open in a new tab

03 · Auto-flip via anchor positioning — Measured area 6.054% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

anchor-nameposition-try-fallbacks자동 뒤집힘
.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

How it works

The Popover API lifts the tooltip into the top layer to show an image-and-text preview card. Its entry now overshoots to 1.08 before a damped rotation wobble of 1.8°→-1°→0.4° around the top edge settles it.

Where it fits

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.

Rich card preview tooltip
Open in a new tab

04 · Rich card preview tooltip — Measured area 23.918% · moving frames 13/23Original by GODRICH · SCSS and React ship in the zip · See the password

popover=manualtop layer280px 카드
.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

How it works

A small triangular tail under the bubble makes it obvious which element it points to. The entry spring pivots around that same tail — scale overshoots to 1.12, then a damped rotation wobble of 2.6°→-1.5°→0.6° settles it.

Where it fits

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.

Arrow tail
Open in a new tab

05 · Arrow tail — Measured area 4.799% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

clip-path 삼각형4px 간격opacity
.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

How it works

The tooltip waits a beat before appearing, so it stays hidden during a quick pass of the pointer. Once it does appear, scale overshoots to 1.12 before a damped rotation wobble of 2.4°→-1.4°→0.5° around the bottom edge settles it.

Where it fits

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.

Delayed show on hover
Open in a new tab

06 · Delayed show on hover — Measured area 6.83% · moving frames 13/23Original by GODRICH · SCSS and React ship in the zip · See the password

@keyframes delay-loop짧은 등장긴 유지
.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

How it works

The same tooltip also appears when focus moves in with the Tab key, not only on mouse hover. Either trigger now plays the same one-shot spring — scale overshoots to 1.12, then a damped rotation wobble of 2.6°→-1.5°→0.6° settles it.

Where it fits

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.

Show on keyboard focus
Open in a new tab

07 · Show on keyboard focus — Measured area 5.953% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

:focus-visible:hover 동시 대응aria-describedby
.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

How it works

A rounded chat-bubble shape with a tail makes the tooltip feel like a message. It now overshoots to scale 1.14 before a damped rotation wobble of 3°→-1.7°→0.7° around that tail corner settles it.

Where it fits

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.

Chat-app style bubble
Open in a new tab

08 · Chat-app style bubble — Measured area 9.751% · moving frames 11/23Original by GODRICH · SCSS and React ship in the zip · See the password

border-radius 18px꼬리 pseudo등장 바운스
.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

How it works

A small dot gently pulses to signal there is more information worth checking. The bubble's own entry overshoots to scale 1.12 before a damped rotation wobble of 2.6°→-1.5°→0.6° around the bottom edge settles it.

Where it fits

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.

Info pulse dot
Open in a new tab

09 · Info pulse dot — Measured area 7.869% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password

@keyframes pulse-ring2초 주기opacity 감쇠
.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.

The top-layer trap

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.

The quiet fallback

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.

Both fixes in the zip

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. When the backdrop behind the bubble should do as much talking as the bubble itself, the layered colors of the aurora mesh gradient kit fill that ground.

Accessibility

Reduced motion

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.

Screen readers

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. Growing item 08's chat-style bubble into a conversation that keeps going is the move the CSS chat bubble animations take next.

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.

Enter the archive password

The password is inside this article. You will find it as you read.