25 CSS Blockquotes12 / 25

CSS onlyMIT licensed

Speech Bubble CSS Blockquote

Two ways to grow a tail on a quote bubble, side by side in one conversation: the classic border-triangle ::after (works everywhere, back to ancient browsers) and the modern clip-path tail, which can be any shape — here a curved-feeling sliver that border tricks can't draw. Bubbles alternate sides like a chat thread, each anchored to a real avatar, and the tail geometry is annotated in the markup so you can lift whichever technique your support matrix allows.

Published Updated

Live Demo
Try it

The code

<section class="bq-12" aria-label="Speech bubble blockquote demo">
  <div class="bq-12__stage">
    <div class="bq-12__thread">
      <figure class="bq-12__msg">
        <span class="bq-12__ava" aria-hidden="true" style="--img:url('https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?q=80&w=120&auto=format&fit=crop')"></span>
        <div class="bq-12__stack">
          <blockquote class="bq-12__bubble bq-12__bubble--classic">
            <p>The tail on this bubble is the old border trick — a zero-size box where only one border edge is painted. Works in literally every browser.</p>
          </blockquote>
          <figcaption class="bq-12__meta">Devon · border-right-color tail</figcaption>
        </div>
      </figure>
      <figure class="bq-12__msg bq-12__msg--me">
        <div class="bq-12__stack">
          <blockquote class="bq-12__bubble bq-12__bubble--modern">
            <p>And this one is clip-path — the tail carries the bubble's gradient, which borders can't do. Any polygon you can imagine.</p>
          </blockquote>
          <figcaption class="bq-12__meta">Ana · clip-path:polygon() tail</figcaption>
        </div>
        <span class="bq-12__ava" aria-hidden="true" style="--img:url('https://images.unsplash.com/photo-1438761681033-6461ffad8d80?q=80&w=120&auto=format&fit=crop')"></span>
      </figure>
    </div>
    <p class="bq-12__chip" aria-hidden="true">border-triangle vs clip-path tails · drop-shadow() wraps both</p>
  </div>
</section>
.bq-12,
.bq-12 *,
.bq-12 *::before,
.bq-12 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.bq-12 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  --bg: oklch(0.96 0.008 250);
  --ink: oklch(0.25 0.015 260);
  --mut: oklch(0.53 0.015 260);
  --bubble: oklch(1 0 0);
  --b2a: oklch(0.6 0.2 260);
  --b2b: oklch(0.55 0.21 290);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
}

.bq-12__stage {
  width: 100%;
  height: 100vh;
  height: 100svh;
  display: grid;
  place-items: center;
  align-content: center;
  gap: 26px;
  padding: clamp(20px,5vw,56px);
  background: linear-gradient(180deg,oklch(0.97 0.01 230),var(--bg));
  overflow-y: auto;
}

.bq-12__thread {
  width: min(100%,560px);
  display: flex;
  flex-direction: column;
  gap: 22px;
}

.bq-12__msg {
  display: flex;
  gap: 12px;
  align-items: flex-end;
  filter: drop-shadow(0 6px 14px oklch(0.3 0.03 260/.16));
}

.bq-12__msg--me {
  justify-content: flex-end;
}

.bq-12__ava {
  width: 40px;
  height: 40px;
  border-radius: 99px;
  flex: none;
  background: linear-gradient(135deg,oklch(0.7 0.1 250),oklch(0.55 0.1 300));
  background-image: var(--img),linear-gradient(135deg,oklch(0.7 0.1 250),oklch(0.55 0.1 300));
  background-size: cover;
  background-position: center;
}

.bq-12__stack {
  max-width: 76%;
}

.bq-12__bubble {
  position: relative;
  padding: 13px 17px;
  font-size: 14.5px;
  line-height: 1.58;
}

.bq-12__bubble p {
  text-wrap: pretty;
}

.bq-12__bubble--classic {
  background: var(--bubble);
  border-radius: 18px 18px 18px 6px;
}

.bq-12__bubble--classic::after {
  content: '';
  position: absolute;
  left: -10px;
  bottom: 10px;
  border: 11px solid transparent;
  border-right-color: var(--bubble);
  border-left-width: 0;
}

.bq-12__bubble--modern {
  background: linear-gradient(135deg,var(--b2a),var(--b2b));
  color: oklch(0.99 0.005 260);
  border-radius: 18px 18px 6px 18px;
}

.bq-12__bubble--modern::after {
  content: '';
  position: absolute;
  right: -11px;
  bottom: 8px;
  width: 14px;
  height: 18px;
  background: var(--b2b);
  clip-path: polygon(0 0,100% 78%,0 100%);
}

.bq-12__meta {
  margin-top: 7px;
  font-size: 11.5px;
  color: var(--mut);
  font-family: ui-monospace,Menlo,Consolas,monospace;
}

.bq-12__msg--me .bq-12__meta {
  text-align: right;
}

.bq-12__chip {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11.5px;
  color: var(--mut);
  padding: 7px 13px;
  border: 1px solid oklch(0 0 0/.1);
  border-radius: 99px;
  background: oklch(1 0 0);
}

@media (prefers-reduced-motion: reduce) {
  .bq-12 * {
    transition-duration: .01ms !important;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Blockquote from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Speech Bubble CSS Blockquote
Source: https://codefronts.com/snippets/css-blockquotes/speech-bubble/

Two ways to grow a tail on a quote bubble, side by side in one conversation: the classic border-triangle ::after (works everywhere, back to ancient browsers) and the modern clip-path tail, which can be any shape — here a curved-feeling sliver that border tricks can't draw. Bubbles alternate sides like a chat thread, each anchored to a real avatar, and the tail geometry is annotated in the markup so you can lift whichever technique your support matrix allows.
## HTML
```html
<section class="bq-12" aria-label="Speech bubble blockquote demo">
  <div class="bq-12__stage">
    <div class="bq-12__thread">
      <figure class="bq-12__msg">
        <span class="bq-12__ava" aria-hidden="true" style="--img:url('https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?q=80&w=120&auto=format&fit=crop')"></span>
        <div class="bq-12__stack">
          <blockquote class="bq-12__bubble bq-12__bubble--classic">
            <p>The tail on this bubble is the old border trick — a zero-size box where only one border edge is painted. Works in literally every browser.</p>
          </blockquote>
          <figcaption class="bq-12__meta">Devon · border-right-color tail</figcaption>
        </div>
      </figure>
      <figure class="bq-12__msg bq-12__msg--me">
        <div class="bq-12__stack">
          <blockquote class="bq-12__bubble bq-12__bubble--modern">
            <p>And this one is clip-path — the tail carries the bubble's gradient, which borders can't do. Any polygon you can imagine.</p>
          </blockquote>
          <figcaption class="bq-12__meta">Ana · clip-path:polygon() tail</figcaption>
        </div>
        <span class="bq-12__ava" aria-hidden="true" style="--img:url('https://images.unsplash.com/photo-1438761681033-6461ffad8d80?q=80&w=120&auto=format&fit=crop')"></span>
      </figure>
    </div>
    <p class="bq-12__chip" aria-hidden="true">border-triangle vs clip-path tails · drop-shadow() wraps both</p>
  </div>
</section>
```
## CSS
```css
.bq-12,
.bq-12 *,
.bq-12 *::before,
.bq-12 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.bq-12 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  --bg: oklch(0.96 0.008 250);
  --ink: oklch(0.25 0.015 260);
  --mut: oklch(0.53 0.015 260);
  --bubble: oklch(1 0 0);
  --b2a: oklch(0.6 0.2 260);
  --b2b: oklch(0.55 0.21 290);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
}

.bq-12__stage {
  width: 100%;
  height: 100vh;
  height: 100svh;
  display: grid;
  place-items: center;
  align-content: center;
  gap: 26px;
  padding: clamp(20px,5vw,56px);
  background: linear-gradient(180deg,oklch(0.97 0.01 230),var(--bg));
  overflow-y: auto;
}

.bq-12__thread {
  width: min(100%,560px);
  display: flex;
  flex-direction: column;
  gap: 22px;
}

.bq-12__msg {
  display: flex;
  gap: 12px;
  align-items: flex-end;
  filter: drop-shadow(0 6px 14px oklch(0.3 0.03 260/.16));
}

.bq-12__msg--me {
  justify-content: flex-end;
}

.bq-12__ava {
  width: 40px;
  height: 40px;
  border-radius: 99px;
  flex: none;
  background: linear-gradient(135deg,oklch(0.7 0.1 250),oklch(0.55 0.1 300));
  background-image: var(--img),linear-gradient(135deg,oklch(0.7 0.1 250),oklch(0.55 0.1 300));
  background-size: cover;
  background-position: center;
}

.bq-12__stack {
  max-width: 76%;
}

.bq-12__bubble {
  position: relative;
  padding: 13px 17px;
  font-size: 14.5px;
  line-height: 1.58;
}

.bq-12__bubble p {
  text-wrap: pretty;
}

.bq-12__bubble--classic {
  background: var(--bubble);
  border-radius: 18px 18px 18px 6px;
}

.bq-12__bubble--classic::after {
  content: '';
  position: absolute;
  left: -10px;
  bottom: 10px;
  border: 11px solid transparent;
  border-right-color: var(--bubble);
  border-left-width: 0;
}

.bq-12__bubble--modern {
  background: linear-gradient(135deg,var(--b2a),var(--b2b));
  color: oklch(0.99 0.005 260);
  border-radius: 18px 18px 6px 18px;
}

.bq-12__bubble--modern::after {
  content: '';
  position: absolute;
  right: -11px;
  bottom: 8px;
  width: 14px;
  height: 18px;
  background: var(--b2b);
  clip-path: polygon(0 0,100% 78%,0 100%);
}

.bq-12__meta {
  margin-top: 7px;
  font-size: 11.5px;
  color: var(--mut);
  font-family: ui-monospace,Menlo,Consolas,monospace;
}

.bq-12__msg--me .bq-12__meta {
  text-align: right;
}

.bq-12__chip {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11.5px;
  color: var(--mut);
  padding: 7px 13px;
  border: 1px solid oklch(0 0 0/.1);
  border-radius: 99px;
  background: oklch(1 0 0);
}

@media (prefers-reduced-motion: reduce) {
  .bq-12 * {
    transition-duration: .01ms !important;
  }
}
```

How this works

Technique one — the border triangle, the trick that raised a generation of tooltips. A zero-size box with one visible border edge is a triangle:

.bubble::after{
  content:''; position:absolute;
  left:-10px; bottom:14px;
  border:11px solid transparent;
  border-right-color:var(--bubble);   /* only this edge shows */
  border-left-width:0;
}

Technique two — clip-path on a small positioned square, which frees the tail from triangle-only geometry:

.bubble--modern::after{
  content:''; position:absolute;
  right:-12px; bottom:12px; width:14px; height:18px;
  background:var(--bubble2);
  clip-path:polygon(0 0, 100% 78%, 0 100%);   /* any shape */
}

The border version's constraint is that the tail color must be a flat color (it IS a border). The clip-path version paints with background, so it can carry the bubble's gradient — which is why the right-side bubbles here can be gradient-filled tail included, while flat bubbles use the cheaper classic. Rounded-corner logic completes the effect: the corner nearest each tail is tightened (18px → 6px) so bubble and tail read as one drawn shape, the way iMessage and WhatsApp do it.

Markup-wise each message is still a semantic <figure>/<blockquote>/<figcaption> — a chat skin doesn't excuse div soup, and the figcaption doubles as the name+time row.

Make it yours

  • Tail position: move it by adjusting the bottom offset — high tail (near top) reads as 'speaking', low tail reads as 'caption'. Keep tails on the SPEAKER side.
  • iMessage mode: gradient bubble oklch(0.6 0.2 260)→oklch(0.55 0.22 285), white text, tails only on the last bubble of a run.
  • Comic style: 3px ink border + white fill + a border-triangle tail with a second, offset ::before triangle in ink to fake the outline continuing down the tail.
  • Thought bubble: replace the tail with two aria-hidden circles (8px, 4px) positioned toward the avatar — no new technique needed.

Gotchas — read before shipping

  • Border triangles can't inherit gradients or shadows — the tail will visibly mismatch a gradient bubble. That's the cue to switch to the clip-path tail.
  • A tail is decoration: it must be a pseudo-element or aria-hidden — a real triangle div becomes phantom content for screen readers.
  • box-shadow on the bubble doesn't wrap the tail (it's a separate box) — use filter:drop-shadow() on the figure instead, which contours BOTH shapes as one silhouette.
  • Don't round the corner the tail attaches to — a 18px radius there opens a visible gap between bubble body and tail; tighten it to ≤6px.

Browser support

ChromeSafariFirefoxEdge
114+17.5+121+114+

Floor set by oklch(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.

Border triangles work in anything ever shipped; clip-path basic shapes are Chrome 55 / Safari 13.1 / Firefox 54. Floor comes from oklch()/color-mix tokens only.

Search CodeFronts

Loading…