12 / 25
All 25 in 25 Tailwind CSS Blockquotes
- 01Tailwind Blockquote with Large Decorative Quotation Marks
- 02Tailwind CSS Blockquote Testimonial Component
- 03Tailwind Callout Box / Admonition Blockquote
- 04Responsive Tailwind Center-Aligned Quote Banner
- 05Modern Minimalist Left Border Tailwind Blockquote
- 06Animated Tailwind Gradient Border Blockquote
- 07Classic Left Border Tailwind Blockquote Snippet
- 08Brutalist Tailwind Blockquote Design
- 09Glassmorphism Tailwind Blockquote Card
- 10Glowing Neon Border Tailwind Quote Box
- 11Article Pull Quote Tailwind Design with Wrap Text
- 12Speech Bubble Tailwind Blockquote
- 13Thick Vertical Accent Rail Tailwind Blockquote
- 14Customer Testimonial Blockquote Card in Tailwind CSS
- 15Inline Highlighted Text Tailwind Blockquote
- 16Marker Highlight Underline Tailwind Blockquote
- 17Floating Tailwind Blockquote with Drop Shadow
- 18Aurora Gradient Background Tailwind Blockquote
- 19Twitter / X Post Card Style Tailwind Blockquote
- 20Multi-Column Newspaper Editorial Blockquote in Tailwind
- 21IDE Code Comment Style Blockquote in Tailwind
- 22Markdown Compatible HTML Blockquote in Tailwind
- 23JavaScript Testimonial Quote Slider with Tailwind CSS
- 24Expandable Read More Tailwind Blockquote
- 25Tailwind Blockquote with One-Click Copy Button
Speech Bubble Tailwind Blockquote
Two speech-bubble tails side by side: the classic zero-size border triangle (after:border-[11px] after:border-r-tbq12-bubble) and a clip-path polygon that can carry the bubble's gradient. The port adds what every chat UI needs next — messages that slide in with the starting: variant (@starting-style) and a typing indicator.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tbq-12 w-full min-h-svh grid place-items-center bg-linear-to-b from-tbq12-top to-tbq12-bg px-5 py-14 font-tbq12-sans text-tbq12-ink" aria-label="Speech bubble blockquote demo">
<div class="grid w-full justify-items-center gap-8">
<div class="flex w-full max-w-[560px] flex-col gap-5.5">
<figure class="flex items-end gap-3 drop-shadow-[0_6px_14px_oklch(0.3_0.03_260/0.16)] transition-[opacity,translate] duration-500 starting:translate-y-2 starting:opacity-0 motion-reduce:transition-none">
<img class="size-10 flex-none rounded-full bg-linear-135 from-tbq12-av1 to-tbq12-av2 object-cover" src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?q=80&w=120&auto=format&fit=crop" alt="" width="40" height="40" loading="lazy">
<div class="max-w-[76%]">
<blockquote class="relative rounded-[18px_18px_18px_6px] bg-tbq12-bubble px-4.5 py-3.5 text-[14.5px] leading-[1.58] after:absolute after:bottom-2.5 after:-left-2.5 after:border-[11px] after:border-l-0 after:border-transparent after:border-r-tbq12-bubble">
<p class="text-pretty">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="mt-1.5 font-tbq12-mono text-[11.5px] text-tbq12-mut">Devon · border-r-color tail</figcaption>
</div>
</figure>
<figure class="flex items-end justify-end gap-3 drop-shadow-[0_6px_14px_oklch(0.3_0.03_260/0.16)] transition-[opacity,translate] delay-200 duration-500 starting:translate-y-2 starting:opacity-0 motion-reduce:transition-none">
<div class="max-w-[76%]">
<blockquote class="relative rounded-[18px_18px_6px_18px] bg-linear-135 from-tbq12-b2a to-tbq12-b2b px-4.5 py-3.5 text-[14.5px] leading-[1.58] text-white after:absolute after:-right-[11px] after:bottom-2 after:h-4.5 after:w-3.5 after:bg-tbq12-b2b after:[clip-path:polygon(0_0,100%_78%,0_100%)]">
<p class="text-pretty">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="mt-1.5 text-right font-tbq12-mono text-[11.5px] text-tbq12-mut">Ana · clip-path polygon tail</figcaption>
</div>
<img class="size-10 flex-none rounded-full bg-linear-135 from-tbq12-av2 to-tbq12-av1 object-cover" src="https://images.unsplash.com/photo-1438761681033-6461ffad8d80?q=80&w=120&auto=format&fit=crop" alt="" width="40" height="40" loading="lazy">
</figure>
<div role="status" aria-label="Devon is typing" class="flex items-end gap-3 drop-shadow-[0_6px_14px_oklch(0.3_0.03_260/0.12)] transition-opacity delay-700 duration-500 starting:opacity-0 motion-reduce:transition-none">
<span aria-hidden="true" class="size-10 flex-none"></span>
<span aria-hidden="true" class="flex items-center gap-1 rounded-[18px_18px_18px_6px] bg-tbq12-bubble px-4 py-3.5">
<i class="size-1.5 animate-tbq12-typing rounded-full bg-tbq12-mut motion-reduce:animate-none"></i>
<i class="size-1.5 animate-tbq12-typing rounded-full bg-tbq12-mut [animation-delay:0.15s] motion-reduce:animate-none"></i>
<i class="size-1.5 animate-tbq12-typing rounded-full bg-tbq12-mut [animation-delay:0.3s] motion-reduce:animate-none"></i>
</span>
</div>
</div>
<p aria-hidden="true" class="rounded-full border border-black/10 bg-white px-3.5 py-2 font-tbq12-mono text-[11.5px] text-tbq12-mut">border-triangle vs clip-path tails · drop-shadow · starting:</p>
</div>
</section><section class="tbq-12 w-full min-h-svh grid place-items-center bg-linear-to-b from-tbq12-top to-tbq12-bg px-5 py-14 font-tbq12-sans text-tbq12-ink" aria-label="Speech bubble blockquote demo">
<div class="grid w-full justify-items-center gap-8">
<div class="flex w-full max-w-[560px] flex-col gap-5.5">
<figure class="flex items-end gap-3 drop-shadow-[0_6px_14px_oklch(0.3_0.03_260/0.16)] transition-[opacity,translate] duration-500 starting:translate-y-2 starting:opacity-0 motion-reduce:transition-none">
<img class="size-10 flex-none rounded-full bg-linear-135 from-tbq12-av1 to-tbq12-av2 object-cover" src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?q=80&w=120&auto=format&fit=crop" alt="" width="40" height="40" loading="lazy">
<div class="max-w-[76%]">
<blockquote class="relative rounded-[18px_18px_18px_6px] bg-tbq12-bubble px-4.5 py-3.5 text-[14.5px] leading-[1.58] after:absolute after:bottom-2.5 after:-left-2.5 after:border-[11px] after:border-l-0 after:border-transparent after:border-r-tbq12-bubble">
<p class="text-pretty">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="mt-1.5 font-tbq12-mono text-[11.5px] text-tbq12-mut">Devon · border-r-color tail</figcaption>
</div>
</figure>
<figure class="flex items-end justify-end gap-3 drop-shadow-[0_6px_14px_oklch(0.3_0.03_260/0.16)] transition-[opacity,translate] delay-200 duration-500 starting:translate-y-2 starting:opacity-0 motion-reduce:transition-none">
<div class="max-w-[76%]">
<blockquote class="relative rounded-[18px_18px_6px_18px] bg-linear-135 from-tbq12-b2a to-tbq12-b2b px-4.5 py-3.5 text-[14.5px] leading-[1.58] text-white after:absolute after:-right-[11px] after:bottom-2 after:h-4.5 after:w-3.5 after:bg-tbq12-b2b after:[clip-path:polygon(0_0,100%_78%,0_100%)]">
<p class="text-pretty">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="mt-1.5 text-right font-tbq12-mono text-[11.5px] text-tbq12-mut">Ana · clip-path polygon tail</figcaption>
</div>
<img class="size-10 flex-none rounded-full bg-linear-135 from-tbq12-av2 to-tbq12-av1 object-cover" src="https://images.unsplash.com/photo-1438761681033-6461ffad8d80?q=80&w=120&auto=format&fit=crop" alt="" width="40" height="40" loading="lazy">
</figure>
<div role="status" aria-label="Devon is typing" class="flex items-end gap-3 drop-shadow-[0_6px_14px_oklch(0.3_0.03_260/0.12)] transition-opacity delay-700 duration-500 starting:opacity-0 motion-reduce:transition-none">
<span aria-hidden="true" class="size-10 flex-none"></span>
<span aria-hidden="true" class="flex items-center gap-1 rounded-[18px_18px_18px_6px] bg-tbq12-bubble px-4 py-3.5">
<i class="size-1.5 animate-tbq12-typing rounded-full bg-tbq12-mut motion-reduce:animate-none"></i>
<i class="size-1.5 animate-tbq12-typing rounded-full bg-tbq12-mut [animation-delay:0.15s] motion-reduce:animate-none"></i>
<i class="size-1.5 animate-tbq12-typing rounded-full bg-tbq12-mut [animation-delay:0.3s] motion-reduce:animate-none"></i>
</span>
</div>
</div>
<p aria-hidden="true" class="rounded-full border border-black/10 bg-white px-3.5 py-2 font-tbq12-mono text-[11.5px] text-tbq12-mut">border-triangle vs clip-path tails · drop-shadow · starting:</p>
</div>
</section>@import "tailwindcss";
@theme {
--color-tbq12-bg: #eef2f7;
--color-tbq12-bg: oklch(0.96 0.008 250);
--color-tbq12-top: #f0f8fc;
--color-tbq12-top: oklch(0.975 0.01 230);
--color-tbq12-ink: #1b1f27;
--color-tbq12-ink: oklch(0.24 0.015 260);
--color-tbq12-mut: #5e646c;
--color-tbq12-mut: oklch(0.5 0.015 260);
--color-tbq12-bubble: #ffffff;
--color-tbq12-bubble: oklch(1 0 0);
--color-tbq12-b2a: #2773ee;
--color-tbq12-b2a: oklch(0.58 0.2 260);
--color-tbq12-b2b: #6d44d6;
--color-tbq12-b2b: oklch(0.52 0.21 290);
--color-tbq12-av1: #6da3da;
--color-tbq12-av1: oklch(0.7 0.1 250);
--color-tbq12-av2: #7b63a3;
--color-tbq12-av2: oklch(0.55 0.1 300);
--font-tbq12-sans: 'Geist', system-ui, sans-serif;
--font-tbq12-mono: 'Geist Mono', ui-monospace, monospace;
--animate-tbq12-typing: tbq12-typing 1.2s ease-in-out infinite;
@keyframes tbq12-typing {
0%,
60%,
100% {
opacity: 0.35;
translate: 0 0;
}
30% {
opacity: 1;
translate: 0 -3px;
}
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tbq-12 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tbq12-bg: #eef2f7;
--color-tbq12-bg: oklch(0.96 0.008 250);
--color-tbq12-top: #f0f8fc;
--color-tbq12-top: oklch(0.975 0.01 230);
--color-tbq12-ink: #1b1f27;
--color-tbq12-ink: oklch(0.24 0.015 260);
--color-tbq12-mut: #5e646c;
--color-tbq12-mut: oklch(0.5 0.015 260);
--color-tbq12-bubble: #ffffff;
--color-tbq12-bubble: oklch(1 0 0);
--color-tbq12-b2a: #2773ee;
--color-tbq12-b2a: oklch(0.58 0.2 260);
--color-tbq12-b2b: #6d44d6;
--color-tbq12-b2b: oklch(0.52 0.21 290);
--color-tbq12-av1: #6da3da;
--color-tbq12-av1: oklch(0.7 0.1 250);
--color-tbq12-av2: #7b63a3;
--color-tbq12-av2: oklch(0.55 0.1 300);
--font-tbq12-sans: 'Geist', system-ui, sans-serif;
--font-tbq12-mono: 'Geist Mono', ui-monospace, monospace;
--animate-tbq12-typing: tbq12-typing 1.2s ease-in-out infinite;
@keyframes tbq12-typing {
0%,
60%,
100% {
opacity: 0.35;
translate: 0 0;
}
30% {
opacity: 1;
translate: 0 -3px;
}
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tbq-12 {
width: 100%;
min-height: 100svh;
}Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tbq12-bg: #eef2f7;
--color-tbq12-bg: oklch(0.96 0.008 250);
--color-tbq12-top: #f0f8fc;
--color-tbq12-top: oklch(0.975 0.01 230);
--color-tbq12-ink: #1b1f27;
--color-tbq12-ink: oklch(0.24 0.015 260);
--color-tbq12-mut: #5e646c;
--color-tbq12-mut: oklch(0.5 0.015 260);
--color-tbq12-bubble: #ffffff;
--color-tbq12-bubble: oklch(1 0 0);
--color-tbq12-b2a: #2773ee;
--color-tbq12-b2a: oklch(0.58 0.2 260);
--color-tbq12-b2b: #6d44d6;
--color-tbq12-b2b: oklch(0.52 0.21 290);
--color-tbq12-av1: #6da3da;
--color-tbq12-av1: oklch(0.7 0.1 250);
--color-tbq12-av2: #7b63a3;
--color-tbq12-av2: oklch(0.55 0.1 300);
--font-tbq12-sans: 'Geist', system-ui, sans-serif;
--font-tbq12-mono: 'Geist Mono', ui-monospace, monospace;
--animate-tbq12-typing: tbq12-typing 1.2s ease-in-out infinite;
@keyframes tbq12-typing {
0%,
60%,
100% {
opacity: 0.35;
translate: 0 0;
}
30% {
opacity: 1;
translate: 0 -3px;
}
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tbq-12 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tbq12-bg: #eef2f7;
--color-tbq12-bg: oklch(0.96 0.008 250);
--color-tbq12-top: #f0f8fc;
--color-tbq12-top: oklch(0.975 0.01 230);
--color-tbq12-ink: #1b1f27;
--color-tbq12-ink: oklch(0.24 0.015 260);
--color-tbq12-mut: #5e646c;
--color-tbq12-mut: oklch(0.5 0.015 260);
--color-tbq12-bubble: #ffffff;
--color-tbq12-bubble: oklch(1 0 0);
--color-tbq12-b2a: #2773ee;
--color-tbq12-b2a: oklch(0.58 0.2 260);
--color-tbq12-b2b: #6d44d6;
--color-tbq12-b2b: oklch(0.52 0.21 290);
--color-tbq12-av1: #6da3da;
--color-tbq12-av1: oklch(0.7 0.1 250);
--color-tbq12-av2: #7b63a3;
--color-tbq12-av2: oklch(0.55 0.1 300);
--font-tbq12-sans: 'Geist', system-ui, sans-serif;
--font-tbq12-mono: 'Geist Mono', ui-monospace, monospace;
--animate-tbq12-typing: tbq12-typing 1.2s ease-in-out infinite;
@keyframes tbq12-typing {
0%,
60%,
100% {
opacity: 0.35;
translate: 0 0;
}
30% {
opacity: 1;
translate: 0 -3px;
}
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tbq-12 {
width: 100%;
min-height: 100svh;
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400..800&family=Geist+Mono:wght@400..600&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400..800&family=Geist+Mono:wght@400..600&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Both tails ported as
after:utilities; the border-triangle relies on v4 orderingborder-r-*afterborder-transparent, exactly as the cascade did in the CSS version. - The CSS avatar used
background-image:var(--img); the port uses a real<img>with a gradient background, so a failed photo still shows a coloured disc and the image can lazy-load. - UI upgrade from the CSS original: messages now enter with
starting:opacity-0 starting:translate-y-2(@starting-style), staggered withdelay-200— the thread feels live rather than printed. - UI upgrade: a typing indicator bubble in a labelled
role="status"region completes the chat pattern; its dots use a gentle 3px bob that stops under reduced motion.
Here's a working CSS demo 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 Tailwind Blockquote
Source: https://codefronts.com/snippets/tailwind-css-blockquotes/speech-bubble-tailwind-blockquote/
Two speech-bubble tails side by side: the classic zero-size border triangle (after:border-[11px] after:border-r-tbq12-bubble) and a clip-path polygon that can carry the bubble's gradient. The port adds what every chat UI needs next — messages that slide in with the starting: variant (@starting-style) and a typing indicator.
## HTML
```html
<section class="tbq-12 w-full min-h-svh grid place-items-center bg-linear-to-b from-tbq12-top to-tbq12-bg px-5 py-14 font-tbq12-sans text-tbq12-ink" aria-label="Speech bubble blockquote demo">
<div class="grid w-full justify-items-center gap-8">
<div class="flex w-full max-w-[560px] flex-col gap-5.5">
<figure class="flex items-end gap-3 drop-shadow-[0_6px_14px_oklch(0.3_0.03_260/0.16)] transition-[opacity,translate] duration-500 starting:translate-y-2 starting:opacity-0 motion-reduce:transition-none">
<img class="size-10 flex-none rounded-full bg-linear-135 from-tbq12-av1 to-tbq12-av2 object-cover" src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?q=80&w=120&auto=format&fit=crop" alt="" width="40" height="40" loading="lazy">
<div class="max-w-[76%]">
<blockquote class="relative rounded-[18px_18px_18px_6px] bg-tbq12-bubble px-4.5 py-3.5 text-[14.5px] leading-[1.58] after:absolute after:bottom-2.5 after:-left-2.5 after:border-[11px] after:border-l-0 after:border-transparent after:border-r-tbq12-bubble">
<p class="text-pretty">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="mt-1.5 font-tbq12-mono text-[11.5px] text-tbq12-mut">Devon · border-r-color tail</figcaption>
</div>
</figure>
<figure class="flex items-end justify-end gap-3 drop-shadow-[0_6px_14px_oklch(0.3_0.03_260/0.16)] transition-[opacity,translate] delay-200 duration-500 starting:translate-y-2 starting:opacity-0 motion-reduce:transition-none">
<div class="max-w-[76%]">
<blockquote class="relative rounded-[18px_18px_6px_18px] bg-linear-135 from-tbq12-b2a to-tbq12-b2b px-4.5 py-3.5 text-[14.5px] leading-[1.58] text-white after:absolute after:-right-[11px] after:bottom-2 after:h-4.5 after:w-3.5 after:bg-tbq12-b2b after:[clip-path:polygon(0_0,100%_78%,0_100%)]">
<p class="text-pretty">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="mt-1.5 text-right font-tbq12-mono text-[11.5px] text-tbq12-mut">Ana · clip-path polygon tail</figcaption>
</div>
<img class="size-10 flex-none rounded-full bg-linear-135 from-tbq12-av2 to-tbq12-av1 object-cover" src="https://images.unsplash.com/photo-1438761681033-6461ffad8d80?q=80&w=120&auto=format&fit=crop" alt="" width="40" height="40" loading="lazy">
</figure>
<div role="status" aria-label="Devon is typing" class="flex items-end gap-3 drop-shadow-[0_6px_14px_oklch(0.3_0.03_260/0.12)] transition-opacity delay-700 duration-500 starting:opacity-0 motion-reduce:transition-none">
<span aria-hidden="true" class="size-10 flex-none"></span>
<span aria-hidden="true" class="flex items-center gap-1 rounded-[18px_18px_18px_6px] bg-tbq12-bubble px-4 py-3.5">
<i class="size-1.5 animate-tbq12-typing rounded-full bg-tbq12-mut motion-reduce:animate-none"></i>
<i class="size-1.5 animate-tbq12-typing rounded-full bg-tbq12-mut [animation-delay:0.15s] motion-reduce:animate-none"></i>
<i class="size-1.5 animate-tbq12-typing rounded-full bg-tbq12-mut [animation-delay:0.3s] motion-reduce:animate-none"></i>
</span>
</div>
</div>
<p aria-hidden="true" class="rounded-full border border-black/10 bg-white px-3.5 py-2 font-tbq12-mono text-[11.5px] text-tbq12-mut">border-triangle vs clip-path tails · drop-shadow · starting:</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tbq12-bg: #eef2f7;
--color-tbq12-bg: oklch(0.96 0.008 250);
--color-tbq12-top: #f0f8fc;
--color-tbq12-top: oklch(0.975 0.01 230);
--color-tbq12-ink: #1b1f27;
--color-tbq12-ink: oklch(0.24 0.015 260);
--color-tbq12-mut: #5e646c;
--color-tbq12-mut: oklch(0.5 0.015 260);
--color-tbq12-bubble: #ffffff;
--color-tbq12-bubble: oklch(1 0 0);
--color-tbq12-b2a: #2773ee;
--color-tbq12-b2a: oklch(0.58 0.2 260);
--color-tbq12-b2b: #6d44d6;
--color-tbq12-b2b: oklch(0.52 0.21 290);
--color-tbq12-av1: #6da3da;
--color-tbq12-av1: oklch(0.7 0.1 250);
--color-tbq12-av2: #7b63a3;
--color-tbq12-av2: oklch(0.55 0.1 300);
--font-tbq12-sans: 'Geist', system-ui, sans-serif;
--font-tbq12-mono: 'Geist Mono', ui-monospace, monospace;
--animate-tbq12-typing: tbq12-typing 1.2s ease-in-out infinite;
@keyframes tbq12-typing {
0%,
60%,
100% {
opacity: 0.35;
translate: 0 0;
}
30% {
opacity: 1;
translate: 0 -3px;
}
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tbq-12 {
width: 100%;
min-height: 100svh;
}
```Here's a working CSS demo 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 Tailwind Blockquote
Source: https://codefronts.com/snippets/tailwind-css-blockquotes/speech-bubble-tailwind-blockquote/
Two speech-bubble tails side by side: the classic zero-size border triangle (after:border-[11px] after:border-r-tbq12-bubble) and a clip-path polygon that can carry the bubble's gradient. The port adds what every chat UI needs next — messages that slide in with the starting: variant (@starting-style) and a typing indicator.
## HTML
```html
<section class="tbq-12 w-full min-h-svh grid place-items-center bg-linear-to-b from-tbq12-top to-tbq12-bg px-5 py-14 font-tbq12-sans text-tbq12-ink" aria-label="Speech bubble blockquote demo">
<div class="grid w-full justify-items-center gap-8">
<div class="flex w-full max-w-[560px] flex-col gap-5.5">
<figure class="flex items-end gap-3 drop-shadow-[0_6px_14px_oklch(0.3_0.03_260/0.16)] transition-[opacity,translate] duration-500 starting:translate-y-2 starting:opacity-0 motion-reduce:transition-none">
<img class="size-10 flex-none rounded-full bg-linear-135 from-tbq12-av1 to-tbq12-av2 object-cover" src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?q=80&w=120&auto=format&fit=crop" alt="" width="40" height="40" loading="lazy">
<div class="max-w-[76%]">
<blockquote class="relative rounded-[18px_18px_18px_6px] bg-tbq12-bubble px-4.5 py-3.5 text-[14.5px] leading-[1.58] after:absolute after:bottom-2.5 after:-left-2.5 after:border-[11px] after:border-l-0 after:border-transparent after:border-r-tbq12-bubble">
<p class="text-pretty">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="mt-1.5 font-tbq12-mono text-[11.5px] text-tbq12-mut">Devon · border-r-color tail</figcaption>
</div>
</figure>
<figure class="flex items-end justify-end gap-3 drop-shadow-[0_6px_14px_oklch(0.3_0.03_260/0.16)] transition-[opacity,translate] delay-200 duration-500 starting:translate-y-2 starting:opacity-0 motion-reduce:transition-none">
<div class="max-w-[76%]">
<blockquote class="relative rounded-[18px_18px_6px_18px] bg-linear-135 from-tbq12-b2a to-tbq12-b2b px-4.5 py-3.5 text-[14.5px] leading-[1.58] text-white after:absolute after:-right-[11px] after:bottom-2 after:h-4.5 after:w-3.5 after:bg-tbq12-b2b after:[clip-path:polygon(0_0,100%_78%,0_100%)]">
<p class="text-pretty">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="mt-1.5 text-right font-tbq12-mono text-[11.5px] text-tbq12-mut">Ana · clip-path polygon tail</figcaption>
</div>
<img class="size-10 flex-none rounded-full bg-linear-135 from-tbq12-av2 to-tbq12-av1 object-cover" src="https://images.unsplash.com/photo-1438761681033-6461ffad8d80?q=80&w=120&auto=format&fit=crop" alt="" width="40" height="40" loading="lazy">
</figure>
<div role="status" aria-label="Devon is typing" class="flex items-end gap-3 drop-shadow-[0_6px_14px_oklch(0.3_0.03_260/0.12)] transition-opacity delay-700 duration-500 starting:opacity-0 motion-reduce:transition-none">
<span aria-hidden="true" class="size-10 flex-none"></span>
<span aria-hidden="true" class="flex items-center gap-1 rounded-[18px_18px_18px_6px] bg-tbq12-bubble px-4 py-3.5">
<i class="size-1.5 animate-tbq12-typing rounded-full bg-tbq12-mut motion-reduce:animate-none"></i>
<i class="size-1.5 animate-tbq12-typing rounded-full bg-tbq12-mut [animation-delay:0.15s] motion-reduce:animate-none"></i>
<i class="size-1.5 animate-tbq12-typing rounded-full bg-tbq12-mut [animation-delay:0.3s] motion-reduce:animate-none"></i>
</span>
</div>
</div>
<p aria-hidden="true" class="rounded-full border border-black/10 bg-white px-3.5 py-2 font-tbq12-mono text-[11.5px] text-tbq12-mut">border-triangle vs clip-path tails · drop-shadow · starting:</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tbq12-bg: #eef2f7;
--color-tbq12-bg: oklch(0.96 0.008 250);
--color-tbq12-top: #f0f8fc;
--color-tbq12-top: oklch(0.975 0.01 230);
--color-tbq12-ink: #1b1f27;
--color-tbq12-ink: oklch(0.24 0.015 260);
--color-tbq12-mut: #5e646c;
--color-tbq12-mut: oklch(0.5 0.015 260);
--color-tbq12-bubble: #ffffff;
--color-tbq12-bubble: oklch(1 0 0);
--color-tbq12-b2a: #2773ee;
--color-tbq12-b2a: oklch(0.58 0.2 260);
--color-tbq12-b2b: #6d44d6;
--color-tbq12-b2b: oklch(0.52 0.21 290);
--color-tbq12-av1: #6da3da;
--color-tbq12-av1: oklch(0.7 0.1 250);
--color-tbq12-av2: #7b63a3;
--color-tbq12-av2: oklch(0.55 0.1 300);
--font-tbq12-sans: 'Geist', system-ui, sans-serif;
--font-tbq12-mono: 'Geist Mono', ui-monospace, monospace;
--animate-tbq12-typing: tbq12-typing 1.2s ease-in-out infinite;
@keyframes tbq12-typing {
0%,
60%,
100% {
opacity: 0.35;
translate: 0 0;
}
30% {
opacity: 1;
translate: 0 -3px;
}
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tbq-12 {
width: 100%;
min-height: 100svh;
}
```How this works
Why drop-shadow, not shadow. box-shadow traces the rectangle and ignores the tail. The filter: drop-shadow() on the parent row traces the painted alpha, so tail and bubble share one shadow.
<figure class="flex items-end gap-3 drop-shadow-[0_6px_14px_oklch(0.3_0.03_260/0.16)]
transition-[opacity,translate] duration-500
starting:translate-y-2 starting:opacity-0">starting: is @starting-style. It defines the values an element transitions from on first render, so bubbles glide in on load, or when you append a new message to the DOM, with no animation library.
Make it yours
- Tail on top: move the classic tail to
after:top-3and remove the bottom-left corner flattening. - Solid 'me' bubble: replace the gradient with
bg-tbq12-b2a; the clip-path tail still works. - Stagger more messages with
delay-150,delay-300…
Gotchas — read before shipping
- The border-triangle tail can't take a gradient — its colour is a border colour. That's why the gradient bubble uses clip-path.
starting:needs Chrome 117 / Safari 17.5 / Firefox 129; older browsers simply show the message without animating.- Keep the typing indicator in a
role="status"region with a text label, or screen readers get nothing.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
@starting-style (the starting: variant) is progressive: Chrome 117 / Safari 17.5 / Firefox 129.