A comic-book speech bubble with a thick outline, a hard offset shadow and a hand-drawn tail that pops out beside a chunky help button. The tail is a tiny inline SVG that covers the outline.
Also known as: css speech bubble, comic tooltip css, neo brutalist tooltip.
<span class="tip-speech-bubble"> <button type="button" aria-label="Help" aria-describedby="tip-speech-bubble-tip">?</button> <span class="tip-speech-bubble-tip" id="tip-speech-bubble-tip" role="tooltip">Psst! Hints live here.<svg viewBox="0 0 28 24" aria-hidden="true"><path class="tip-speech-bubble-fill" d="M25 1.5 1.5 20.5 25 14.5z"/><path class="tip-speech-bubble-line" d="M21.4 1.5 1.5 20.5l19.9-6"/></svg></span> </span>
.tip-speech-bubble {
--c: var(--tip-color, #3b82f6);
font-size: var(--tip-size, 16px);
position: relative;
display: inline-flex;
--ink: #0f172a;
--shade: color-mix(in srgb, var(--c) 55%, #000);
margin-right: 12.5em; /* room for the bubble in this preview */
}
.tip-speech-bubble button {
display: grid;
place-items: center;
width: 2.6em;
height: 2.6em;
padding: 0;
border: 0.14em solid var(--ink);
border-radius: 0.6em;
background: var(--c);
box-shadow: 0.22em 0.22em 0 var(--shade);
color: #fff;
font: 900 1.1em/1 system-ui, sans-serif;
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
/* pressed into its shadow */
.tip-speech-bubble:hover button {
box-shadow: 0.08em 0.08em 0 var(--shade);
transform: translate(0.14em, 0.14em);
}
.tip-speech-bubble button:focus-visible {
outline: 2px solid var(--c);
outline-offset: 2px;
}
.tip-speech-bubble-tip {
position: absolute;
top: 50%;
left: calc(100% + 1.4em);
z-index: 10;
padding: 0.6em 0.95em;
border: 0.14em solid var(--ink);
border-radius: 1em;
background: #fff;
box-shadow: 0.25em 0.25em 0 var(--shade);
color: var(--ink);
font: 800 0.85em/1.2 system-ui, sans-serif;
letter-spacing: 0.02em;
text-transform: uppercase;
white-space: nowrap;
opacity: 0;
visibility: hidden;
pointer-events: none;
/* grows out of the tail's point */
transform-origin: -1em 80%;
translate: 0 -60%;
transform: scale(0.2) rotate(-14deg);
transition: opacity 0.15s ease, visibility 0.15s;
}
/* the tail overlaps the bubble by 0.4em, and its fill hides that bit of outline */
.tip-speech-bubble-tip svg {
position: absolute;
top: calc(50% - 0.35em);
right: calc(100% - 0.4em);
width: 1.4em;
height: 1.2em;
overflow: visible;
}
.tip-speech-bubble-fill { fill: #fff; }
.tip-speech-bubble-line {
fill: none;
stroke: var(--ink);
stroke-width: 2.8;
stroke-linejoin: round;
}
.tip-speech-bubble:hover .tip-speech-bubble-tip,
.tip-speech-bubble:focus-within .tip-speech-bubble-tip {
opacity: 1;
visibility: visible;
animation: tip-speech-bubble-pop 0.6s cubic-bezier(0.3, 0.7, 0.4, 1) both;
}
@keyframes tip-speech-bubble-pop {
0% { opacity: 0; transform: scale(0.2) rotate(-14deg); }
45% { opacity: 1; transform: scale(1.1) rotate(4deg); }
70% { transform: scale(0.96) rotate(-3deg); }
100% { opacity: 1; transform: scale(1) rotate(-2deg); }
}
@media (prefers-reduced-motion: reduce) {
.tip-speech-bubble, .tip-speech-bubble *, .tip-speech-bubble::before, .tip-speech-bubble::after, .tip-speech-bubble *::before, .tip-speech-bubble *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}--tip-color on the element or a parent.
Currently previewing #3b82f6.--tip-size. Every other dimension is in
em, so the whole thing scales together. Currently previewing 16px.transition or animation
shorthand. Staggered delays are relative, so they keep their rhythm.