What is a CSS speech bubble made of?
Mostly a rounded box and a pointer. The box is a p or div with a background, padding and border-radius. The pointer, called the tail, is a ::before or ::after pseudo-element positioned absolutely against it. The box also needs position: relative, otherwise the tail’s offsets get measured from some ancestor instead of the bubble’s own edges.
Nicolas Gallagher’s 2010 tutorial on pure CSS speech bubbles already kept most bubbles to a single element and pushed every visual detail into the stylesheet. That approach still holds.
Border-radius sets the curve (CSS Backgrounds and Borders Module Level 3 defines it), and padding keeps text off the corners. Add a max-width in ch so the bubble doesn’t stretch across the whole page.
Start with one radius on all four corners. A border radius generator helps once the corners need to differ, which happens as soon as a tail sits near one of them.
The tail needs content: "". Without it the pseudo-element is not generated at all.
.bubble {
position: relative;
max-width: 28ch;
padding: 0.75em 1em;
border-radius: 12px;
background: #e8f0fe;
}
.bubble::after {
content: "";
position: absolute;
}
Put together, the build goes in this order.
- Write the body with background, padding, border-radius and a max-width in ch.
- Set position: relative on the body.
- Add ::after with an empty content string and position: absolute.
- Give the tail its shape (border triangle, clip-path or mask) and offset it with top, bottom, left or right.
How does the border triangle method build a tail?
A zero-size element with one colored border and three transparent ones renders as a triangle, and that triangle is the tail. It needs only border, position and content properties.
The colored border is the one facing the bubble. So border-top-color gives a tail that hangs below the bubble and points down.
.bubble::after {
content: "";
position: absolute;
top: 100%;
left: 24px;
border: 10px solid transparent;
border-top-color: #e8f0fe;
}
The other directions follow the same logic, each with an offset on the free axis:
- Tail above, pointing up: bottom: 100% and border-bottom-color
- Tail on the left, pointing left: right: 100% and border-right-color
- Tail on the right, pointing right: left: 100% and border-left-color
Code Boxx’s responsive bubble tutorial runs this trick through four modifier classes (up, down, left, right). A CSS triangle generator produces the border values for any size, which saves the trial and error, and the same border trick draws most CSS arrows.
The appeal is how little it takes. One pseudo-element, a handful of properties that matter, and no support checks in current browsers, since it relies on basic border and positioning rules. The tail size changes with a single border-width value, in px or em.
The downside is the tip. It’s sharp, the edges are straight, and the triangle can’t follow border-radius, so a tail placed at a rounded corner looks detached. An outline needs a second triangle (covered below).
How to build a tail with clip-path or mask
Clip-path and mask draw the tail on the same element as the body, so the basic shape needs no pseudo-element. Clip-path suits straight-edged tails, and mask suits rounded ones.
Using clip-path and border-image fill
Temani Afif’s two-property tooltip, published in Smashing Magazine in March 2024, sets the color with border-image and the fill keyword. The border-image outset paints an overflowing color past the box, covering the area where the tail goes, and clip-path with polygon() cuts out the rectangle plus the pointer.
The same series moves the tail with a –p custom property and reshapes it with –x. A clip-path generator is quicker for sketching the polygon points before converting them to percentages.
Everything lives on one element, with no pseudo-element, and tail size and position sit in custom properties. It has been available across browsers since January 2020 (MDN).
The catch is that clip-path clips everything outside the shape, box-shadow included, and it creates a new stacking context (MDN). The border-radius also needs adjusting to the tail position (Smashing Magazine, 2024).
Using mask and conic-gradient for rounded corners
Rounded corners are where clip-path polygons stop being practical, and the mask method takes over. In Afif’s version for Verpex, the tail is set by a height and an angle (a 20px tail at 60deg in the demo), where the clip-path version uses a width and a height.
His CSS-Tip bubble rounds the body with border-radius and carves a curved tail with a radial-gradient mask, driven by two variables: –r for the radius and –t for the tail size.
One rule gives a rounded body and a rounded tail, and the tail angle is adjustable. The stacked gradients are hard to read, though, and some published snippets still pair mask with -webkit-mask.
The unprefixed mask shorthand has been available across browsers since December 2023, and conic-gradient since November 2020 (both per MDN). A mask generator saves the manual gradient math.
Resizable tails with the shape() function
The older path() function takes SVG path data in fixed CSS pixels, so a bubble drawn with it does not grow with its text. MDN’s example shows the failure: on a larger element the shape stays the same size, and the top and left borders end up partly visible while the right and bottom borders are clipped away.
The shape() function accepts percentages, calc() and custom properties, so the clipped shape scales with the element. It became Baseline Newly Available in February 2026, when Firefox 148 shipped.
| Browser | First version with shape() | Release date |
|---|
| Safari | 18.4 | 31 March 2025 |
| Chrome | 135 | 1 April 2025 |
| Firefox | 148 | 24 February 2026 |
Source: Web Platform DX web-features data, as of October 2026.
Browsers older than those versions drop the declaration, so write a polygon() clip-path first and the shape() rule after it.
How to add an outline and shadow to a bubble with a tail
A border on the bubble body stops at the body, and neither tail method carries it around the tail. Both get fixed the same way, by drawing a slightly smaller layer on top of a first layer in the border color.
On border triangle bubbles that means stacking two triangles. The back one is 1px larger and carries the border color, and the front one carries the background color and sits 1px to the side (30secondsofcode’s example uses an 11px triangle behind a 10px one).
Clip-path bubbles get a pseudo-element inset by the border thickness, with clip-path: inherit so it reuses the body’s shape. Part 2 of the Smashing Magazine series (March 2024) reports that the tail’s border comes out thinner than the body’s until the tail is defined by an angle and a height instead of a base and a height.
Shadows split the two methods further. Box-shadow works on the body of a border triangle bubble, and a box shadow generator speeds up the tuning, but the tail gets no shadow from it.
Clip-path and mask-image clip a box-shadow along with the element, so any shadow outside the shape disappears (CSS-IRL). The fix is filter: drop-shadow() on a parent wrapper around the clipped element. On a border triangle bubble, drop-shadow on the bubble itself covers body and tail together.
Drop-shadow follows the alpha shape, so the tail casts a shadow too. CSS-IRL also notes that box-shadow renders darker and heavier than drop-shadow at the same values, so lower the alpha when switching.
If the two layers drift apart, untick the clip-path declaration in Chrome DevTools. The unclipped boxes show exactly where the misalignment starts.
Which tail method should you pick?
Use the border triangle for flat-color bubbles with square corners, clip-path for single-element components with a variable-driven tail, and mask only when the body and tail both need rounding. The rotated square sits between them: a small square turned 45 degrees behind the body, which gives the tail its own shadow.
| Method | Rounded tail | Shadow route | Setup cost |
|---|
| Border triangle | No | drop-shadow on the bubble | Low |
| Rotated square | Optional, via border-radius | Its own box-shadow | Low |
| clip-path polygon() | No | drop-shadow on a wrapper | Medium |
| mask with gradients | Yes | drop-shadow on a wrapper | High |
Skip mask when a border triangle will do. A notification bubble with a flat color and straight corners gains nothing from stacked gradients, and the extra rule is one more thing to debug.
Browser support and failure conditions
The border triangle only works with a flat color. Give the body a gradient and you get a visible color jump at the tail, because the triangle cannot continue the gradient.
The rotated square has half of itself sitting inside the body, so it needs the same background and a z-index behind the text. H5P’s JoubelUI stylesheet does exactly this with a 12px square rotated 45deg, a box-shadow of its own and z-index: -1.
Clip-path means a wrapper if you want the shadow, so the component carries two nodes in the DOM.
Mask hides the shape inside gradient stops, so a misaligned tail means reading layered gradients. FreeFrontend’s listing of a single-element version highlights a gradient that flows across both body and tail, which a triangle cannot do.
Support rarely decides the choice. Every method above sits at Widely available status in Baseline except shape(), which keeps its polygon() fallback, and Can I use gives the exact version floor before a fallback is dropped.
How to position the tail and align chat bubbles
Pick the side with top, bottom, left or right, set the offset with one value or variable, and mirror the rule for the opposite side. Chat lists add one step: the tail side flips between sent and received messages.
Setting the tail side and offset
A fixed offset like left: 24px holds the tail at a constant distance from the corner. Use left: 20% and it follows the bubble as its width changes. Centering takes left: 50% with transform: translateX(-50%), and if you want the whole thing variable-driven, a –p custom property can be read by left or by the clip-path polygon.
Offsets near the end of an edge fail first. In Temani Afif’s Smashing Magazine tutorial, pushing the position variable to its extremes slides the tail off the end of the bubble.
Clamp the offset instead. Keep it at least one corner radius plus half the tail base away from either end, so the tail stays on the straight part of the edge.
Sent and received messages in a chat list
Bottom-aligning a message list with justify-content: flex-end makes the earliest messages unreachable once the list overflows, according to Chen Hui Jing’s box alignment write-up. Her fix is margin-top: auto on the first child, which keeps the list pinned to the bottom and still scrollable.
Sent bubbles take align-self: flex-end, and received ones keep the default start alignment. The tail moves with them, right side for sent and left side for received.
- Flex alignment follows dir=”rtl” without extra rules, while margin-based alignment needs manual overrides (Alex Chan, 2026)
- Facebook Messenger varies corner rounding across consecutive bubbles, with three states that differ for sender and recipient (Ahmad Shadeed’s component breakdown)
- Showing the tail only on the last bubble of a run keeps a long thread from looking spiky
An iMessage-style corner tail has a catch. freeCodeCamp’s React Native build overlaps two elements at the bubble corner and colors one to match the chat screen, so the cut-out only works on a solid chat background.
Bubbles are one piece of a CSS chat box, which also needs a scrolling message list, a header and an input row.
How to make a speech bubble responsive
Size the body in ch or % so the text wraps, size the tail in em so it scales with the font, and offset the tail in % so it holds its place as the width changes.
A max-width in ch counts characters, so line length stays readable at any font size. Padding derived from the radius keeps text clear of the curve. And if the bubble width isn’t fixed, use % for the tail offset, not px.
Pick em when the tail should follow the bubble’s own text size, and rem when it should follow the root. The em versus rem trade-off is the same one that applies everywhere else in a stylesheet.
Container queries became Baseline Widely available in August 2025 (web.dev), after landing in all major engines in 2023.
A bubble in a narrow chat column and the same bubble in a wide card can switch tail side or font size from the container’s width. Media queries react to the screen instead, which is the wrong signal for a component dropped into different slots.
Container units go further. One cqi equals 1% of the container’s inline size, so the tail size can follow the container directly.
How to keep a speech bubble accessible
Most of the accessibility work sits in the text, not the tail. The tail needs no treatment while its generated content stays an empty string.
Test the text against the bubble background, not the page behind it. WCAG 2.2 Success Criterion 1.4.3 sets the thresholds, and the W3C’s Understanding document says computed ratios are not rounded, so 4.499:1 fails. Normal text needs at least 4.5:1. Large-scale text gets by with 3:1, which means 18pt or 14pt bold (about 24px and 18.5px).
A pastel bubble with white text is the usual offender. Run the pair through a color contrast check before the shape work starts.
Pick the element to match the content. A single message fits a p. Quotes and testimonials belong in a blockquote (Nicolas Gallagher’s demo styles blockquote elements as bubbles), and a message list is a ul or ol with li items.
For a live chat list, put role=”log” on the container. MDN names chat logs as the example, and the role announces new messages when the user is idle. It carries an implicit aria-live of polite, and an accessible name is required, so name the log with aria-label or aria-labelledby. See the ARIA role basics if the attribute set is new.
Generated content is exposed to assistive tools, per MDN’s breadcrumb recipe. An empty string has nothing to read, so the tail stays silent.
If the tail uses an image, add an empty alternative: content: url(tail.svg) / "";. And do not let bubble color be the only sign of who sent a message, since a visible name or a visually hidden label covers readers who cannot tell the colors apart.
CSS Speech Bubble FAQ
Is a speech bubble the same thing as a tooltip?
No, but they share one design idea: a text box with a pointing tail, according to Temani Afif’s tutorial for Verpex. A tooltip adds behavior, appearing on hover or focus and anchoring to a trigger element.
How do you turn a speech bubble into a thought bubble?
Replace the pointed tail with a trail of shrinking circles. FreeFrontend’s Pure CSS Thought Bubbles example builds the trail from shadows that inherit the background color, so the circles always match the body.
Is a generator faster than writing the CSS by hand?
Yes, for a known shape. Afif’s CSS Generators site lists more than 70 tooltip and speech bubble shapes, each built from one element and each with copy-ready CSS.
Hand-written CSS pays off once the bubble needs behavior a preset lacks.
Does Bootstrap or Tailwind CSS include a speech bubble component?
No. Bootstrap ships tooltips and popovers with an arrow, positioned by Popper, which is a hover pattern rather than a static bubble. Tailwind CSS has no bubble component either, but its after: variant reproduces the pseudo-element tail.
Can a speech bubble animate in without causing layout shift?
Yes, when the animation changes only transform and opacity. web.dev’s animation guidance says to stick to those two properties, because animating layout properties forces layout work.
CSS keyframes handle the timing, and a transform-origin at the tail tip makes the bubble grow out of the speaker.
Can the same bubble work as a popover?
Yes. The HTML popover attribute, Baseline Newly available since January 2025 (web-features data), renders the bubble in the top layer, so a parent’s overflow or position rules cannot clip it.
MDN’s examples pair it with anchor positioning to place the element beside its trigger. For hover-only cases, a plain CSS tooltip pattern is enough.
When an Inline SVG Bubble Beats CSS
A CSS speech bubble stops being the right build when the tail must curve along an arbitrary outline. An inline SVG bubble draws any curve exactly.
The switch has a cost: the W3C’s SVG 1.1 specification states that SVG performs no automatic line breaking or word wrapping. Stuff & Nonsense placed a foreignObject paragraph inside each of the eight speech bubbles on its about page to get text that wraps.
The trade-off is exact curves in exchange for owning the text flow, which HTML handles for free.
The shape() function draws curves with percentage units and is expected to reach Baseline Widely available on 24 August 2028 (web-features data, checked October 2026). From that date a curved tail can stay in CSS without a polygon() fallback.
Until then, embedding SVG in HTML covers the cases CSS cannot.