15 CSS Chat Bubbles12 / 15
CSS Comic Book Speech Bubble
A four-panel comic strip with the whole balloon vocabulary drawn in CSS: a rounded speech balloon, a starburst shout cut with a 24-point clip-path, a dashed whisper, and a narration caption. Halftone dots come from a two-layer radial-gradient, panel art is duotoned with filter + mix-blend-mode, and three ink palettes swap with a pure-CSS control.
Published
The code
<div class="cb-12">
<input class="cb-12__sr" type="radio" name="cb-12-ink" id="cb-12-ink" checked>
<input class="cb-12__sr" type="radio" name="cb-12-ink" id="cb-12-pulp">
<input class="cb-12__sr" type="radio" name="cb-12-ink" id="cb-12-riso">
<div class="cb-12__stage">
<div class="cb-12__strip">
<figure class="cb-12__panel">
<img src="https://images.unsplash.com/photo-1449824913935-59a10b8d2000?auto=format&fit=crop&w=680&q=70" width="680" height="520" alt="An empty city street at dusk" loading="lazy" decoding="async">
<p class="cb-12__balloon">The whole city went quiet at once.</p>
</figure>
<figure class="cb-12__panel">
<img src="https://images.unsplash.com/photo-1516450360452-9312f5e86fc7?auto=format&fit=crop&w=680&q=70" width="680" height="520" alt="Silhouetted figures under hard shafts of light" loading="lazy" decoding="async">
<p class="cb-12__shout"><span>Not<br>again!</span></p>
</figure>
<figure class="cb-12__panel">
<img src="https://images.unsplash.com/photo-1509023464722-18d996393ca8?auto=format&fit=crop&w=680&q=70" width="680" height="520" alt="A lone peak across still water at first light" loading="lazy" decoding="async">
<p class="cb-12__whisper">don't look up yet…</p>
</figure>
<figure class="cb-12__panel">
<img src="https://images.unsplash.com/photo-1419242902214-272b3f66ee7a?auto=format&fit=crop&w=680&q=70" width="680" height="520" alt="A wide field of stars above a dark treeline" loading="lazy" decoding="async">
<p class="cb-12__caption">Meanwhile, four hundred miles above the argument…</p>
</figure>
</div>
<div class="cb-12__switch" role="radiogroup" aria-label="Ink palette">
<label class="cb-12__chip" for="cb-12-ink">Ink</label>
<label class="cb-12__chip" for="cb-12-pulp">Pulp</label>
<label class="cb-12__chip" for="cb-12-riso">Riso</label>
</div>
</div>
</div><div class="cb-12">
<input class="cb-12__sr" type="radio" name="cb-12-ink" id="cb-12-ink" checked>
<input class="cb-12__sr" type="radio" name="cb-12-ink" id="cb-12-pulp">
<input class="cb-12__sr" type="radio" name="cb-12-ink" id="cb-12-riso">
<div class="cb-12__stage">
<div class="cb-12__strip">
<figure class="cb-12__panel">
<img src="https://images.unsplash.com/photo-1449824913935-59a10b8d2000?auto=format&fit=crop&w=680&q=70" width="680" height="520" alt="An empty city street at dusk" loading="lazy" decoding="async">
<p class="cb-12__balloon">The whole city went quiet at once.</p>
</figure>
<figure class="cb-12__panel">
<img src="https://images.unsplash.com/photo-1516450360452-9312f5e86fc7?auto=format&fit=crop&w=680&q=70" width="680" height="520" alt="Silhouetted figures under hard shafts of light" loading="lazy" decoding="async">
<p class="cb-12__shout"><span>Not<br>again!</span></p>
</figure>
<figure class="cb-12__panel">
<img src="https://images.unsplash.com/photo-1509023464722-18d996393ca8?auto=format&fit=crop&w=680&q=70" width="680" height="520" alt="A lone peak across still water at first light" loading="lazy" decoding="async">
<p class="cb-12__whisper">don't look up yet…</p>
</figure>
<figure class="cb-12__panel">
<img src="https://images.unsplash.com/photo-1419242902214-272b3f66ee7a?auto=format&fit=crop&w=680&q=70" width="680" height="520" alt="A wide field of stars above a dark treeline" loading="lazy" decoding="async">
<p class="cb-12__caption">Meanwhile, four hundred miles above the argument…</p>
</figure>
</div>
<div class="cb-12__switch" role="radiogroup" aria-label="Ink palette">
<label class="cb-12__chip" for="cb-12-ink">Ink</label>
<label class="cb-12__chip" for="cb-12-pulp">Pulp</label>
<label class="cb-12__chip" for="cb-12-riso">Riso</label>
</div>
</div>
</div>@import url('https://fonts.googleapis.com/css2?family=Bangers&family=Archivo:wght@500;600;700&display=swap');
.cb-12 {
--ink: #12100e;
--paper: #f4ead6;
--pop: #ffd23f;
--pop-2: #ff4b3e;
--ink-soft: rgba(18,16,14,.14);
--display: "Bangers",Impact,"Haettenschweiler","Arial Narrow Bold",sans-serif;
--sans: "Archivo",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
position: relative;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(1rem,4vw,3rem);
background-color: var(--paper);
background-image: radial-gradient(circle, var(--ink-soft) 1.1px, transparent 1.2px), radial-gradient(circle, var(--ink-soft) .8px, transparent .9px);
background-size: 9px 9px, 9px 9px;
background-position: 0 0, 4.5px 4.5px;
color: var(--ink);
font-family: var(--sans);
-webkit-font-smoothing: antialiased;
}
.cb-12,
.cb-12 *,
.cb-12 *::before,
.cb-12 *::after {
box-sizing: border-box;
}
.cb-12__sr {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
opacity: 0;
}
.cb-12__stage {
inline-size: min(100%,58rem);
display: grid;
gap: clamp(1.25rem,3vw,2rem);
justify-items: center;
}
.cb-12__strip {
inline-size: 100%;
display: grid;
gap: clamp(.75rem,2vw,1.15rem);
grid-template-columns: repeat(2,minmax(0,1fr));
}
@media (min-width: 64rem) {
.cb-12__strip {
grid-template-columns: repeat(4,minmax(0,1fr));
}
}
@media (max-width: 26rem) {
.cb-12__strip {
grid-template-columns: minmax(0,1fr);
}
}
/* ── panel ── */
.cb-12__panel {
position: relative;
margin: 0;
overflow: hidden;
isolation: isolate;
aspect-ratio: 5/4;
border: 3px solid var(--ink);
border-radius: .35rem;
background: var(--pop);
box-shadow: 6px 6px 0 var(--ink);
transition: transform .32s cubic-bezier(.16,1,.3,1), box-shadow .32s;
}
.cb-12__panel:nth-child(2) {
rotate: -.8deg;
}
.cb-12__panel:nth-child(3) {
rotate: .7deg;
}
.cb-12__panel:hover {
transform: translate(-3px,-4px);
box-shadow: 10px 11px 0 var(--ink);
}
.cb-12__panel img {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
object-fit: cover;
filter: grayscale(1) contrast(1.45) brightness(1.05);
mix-blend-mode: luminosity;
}
/* ── 1 · speech balloon ── */
.cb-12__balloon {
position: absolute;
inset: auto 1rem 1.5rem 1rem;
z-index: 2;
margin: 0;
padding: .7rem .95rem;
border: 3px solid var(--ink);
border-radius: 1.1rem;
background: #fff;
color: var(--ink);
font-size: clamp(.9rem,2.2vw,1rem);
font-weight: 600;
line-height: 1.35;
text-wrap: pretty;
box-shadow: 4px 4px 0 var(--ink);
}
.cb-12__balloon::before,
.cb-12__balloon::after {
content: '';
position: absolute;
inline-size: 0;
block-size: 0;
inset-block-start: 100%;
inset-inline-start: 1.9rem;
}
.cb-12__balloon::before {
border-inline: 12px solid transparent;
border-block-start: 20px solid var(--ink);
}
.cb-12__balloon::after {
inset-inline-start: 2.1rem;
border-inline: 8px solid transparent;
border-block-start: 14px solid #fff;
}
/* ── 2 · starburst shout ── */
.cb-12__shout {
position: absolute;
inset: 50% auto auto 50%;
translate: -50% -50%;
z-index: 2;
margin: 0;
inline-size: min(84%,15rem);
aspect-ratio: 1;
display: grid;
place-items: center;
background: var(--pop-2);
color: #fff;
clip-path: polygon(50% 0%,58% 14%,68% 4%,71% 19%,84% 12%,82% 27%,97% 26%,89% 38%,100% 47%,88% 55%,97% 66%,83% 68%,87% 82%,73% 79%,71% 94%,60% 85%,50% 100%,40% 85%,29% 94%,27% 79%,13% 82%,17% 68%,3% 66%,12% 55%,0% 47%,11% 38%,3% 26%,18% 27%,16% 12%,29% 19%,32% 4%,42% 14%);
animation: cb-12-throb 3.4s cubic-bezier(.16,1,.3,1) infinite;
}
.cb-12__shout::before {
content: '';
position: absolute;
inset: -5px;
z-index: -1;
background: var(--ink);
clip-path: inherit;
}
.cb-12__shout span {
font-family: var(--display);
font-size: clamp(1.5rem,6vw,2.35rem);
line-height: .95;
letter-spacing: .03em;
text-transform: uppercase;
text-align: center;
text-wrap: balance;
paint-order: stroke fill;
}
@keyframes cb-12-throb {
0%,
88%,
100% {
scale: 1;
rotate: -2deg;
}
92% {
scale: 1.05;
rotate: 1deg;
}
}
/* ── 3 · dashed whisper ── */
.cb-12__whisper {
position: absolute;
inset: auto 1.15rem 1.6rem auto;
z-index: 2;
margin: 0;
max-inline-size: 70%;
padding: .6rem .9rem;
border: 2.5px dashed var(--ink);
border-radius: 1.4rem;
background: color-mix(in oklab,#fff 88%,var(--pop));
font-size: clamp(.85rem,2vw,.95rem);
font-weight: 500;
font-style: italic;
line-height: 1.3;
}
.cb-12__whisper::after {
content: '';
position: absolute;
inset-block-start: 100%;
inset-inline-end: 1.6rem;
inline-size: .55rem;
block-size: .55rem;
border-radius: 50%;
border: 2.5px dashed var(--ink);
background: color-mix(in oklab,#fff 88%,var(--pop));
translate: 0 .35rem;
}
/* ── 4 · narration caption ── */
.cb-12__caption {
position: absolute;
inset: .9rem .9rem auto .9rem;
z-index: 2;
margin: 0;
padding: .6rem .85rem;
border: 3px solid var(--ink);
background: var(--pop);
color: var(--ink);
font-size: clamp(.8rem,1.9vw,.9rem);
font-weight: 700;
line-height: 1.3;
letter-spacing: .01em;
text-transform: uppercase;
text-wrap: balance;
box-shadow: 4px 4px 0 var(--ink);
}
/* ── palettes ── */
.cb-12:has(#cb-12-pulp:checked) {
--ink: #1a1210;
--paper: #e8d9b5;
--pop: #f2884b;
--pop-2: #c62828;
--ink-soft: rgba(26,18,16,.18);
}
.cb-12:has(#cb-12-riso:checked) {
--ink: #141a2e;
--paper: #eef1f7;
--pop: #7cc6ff;
--pop-2: #ff3d8b;
--ink-soft: rgba(20,26,46,.13);
}
/* ── switch ── */
.cb-12__switch {
display: flex;
flex-wrap: wrap;
gap: .4rem;
padding: .35rem;
justify-content: center;
border: 3px solid var(--ink);
border-radius: .8rem;
background: #fff;
box-shadow: 5px 5px 0 var(--ink);
}
.cb-12__chip {
min-block-size: 44px;
display: grid;
place-items: center;
padding: .45rem 1.15rem;
cursor: pointer;
border-radius: .5rem;
color: var(--ink);
font-size: .85rem;
font-weight: 700;
letter-spacing: .05em;
text-transform: uppercase;
transition: background .18s, color .18s;
}
.cb-12__chip:hover {
background: var(--pop);
}
#cb-12-ink:checked ~ .cb-12__stage .cb-12__chip[for="cb-12-ink"],
#cb-12-pulp:checked ~ .cb-12__stage .cb-12__chip[for="cb-12-pulp"],
#cb-12-riso:checked ~ .cb-12__stage .cb-12__chip[for="cb-12-riso"] {
background: var(--ink);
color: var(--paper);
}
#cb-12-ink:focus-visible ~ .cb-12__stage .cb-12__chip[for="cb-12-ink"],
#cb-12-pulp:focus-visible ~ .cb-12__stage .cb-12__chip[for="cb-12-pulp"],
#cb-12-riso:focus-visible ~ .cb-12__stage .cb-12__chip[for="cb-12-riso"] {
outline: 3px solid var(--pop-2);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.cb-12 *,
.cb-12 *::before,
.cb-12 *::after {
animation: none !important;
transition: none !important;
}
}
@media (forced-colors: active) {
.cb-12__panel,
.cb-12__balloon,
.cb-12__caption,
.cb-12__whisper {
border: 2px solid CanvasText;
box-shadow: none;
}
.cb-12__panel img {
mix-blend-mode: normal;
filter: none;
}
}@import url('https://fonts.googleapis.com/css2?family=Bangers&family=Archivo:wght@500;600;700&display=swap');
.cb-12 {
--ink: #12100e;
--paper: #f4ead6;
--pop: #ffd23f;
--pop-2: #ff4b3e;
--ink-soft: rgba(18,16,14,.14);
--display: "Bangers",Impact,"Haettenschweiler","Arial Narrow Bold",sans-serif;
--sans: "Archivo",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
position: relative;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(1rem,4vw,3rem);
background-color: var(--paper);
background-image: radial-gradient(circle, var(--ink-soft) 1.1px, transparent 1.2px), radial-gradient(circle, var(--ink-soft) .8px, transparent .9px);
background-size: 9px 9px, 9px 9px;
background-position: 0 0, 4.5px 4.5px;
color: var(--ink);
font-family: var(--sans);
-webkit-font-smoothing: antialiased;
}
.cb-12,
.cb-12 *,
.cb-12 *::before,
.cb-12 *::after {
box-sizing: border-box;
}
.cb-12__sr {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
opacity: 0;
}
.cb-12__stage {
inline-size: min(100%,58rem);
display: grid;
gap: clamp(1.25rem,3vw,2rem);
justify-items: center;
}
.cb-12__strip {
inline-size: 100%;
display: grid;
gap: clamp(.75rem,2vw,1.15rem);
grid-template-columns: repeat(2,minmax(0,1fr));
}
@media (min-width: 64rem) {
.cb-12__strip {
grid-template-columns: repeat(4,minmax(0,1fr));
}
}
@media (max-width: 26rem) {
.cb-12__strip {
grid-template-columns: minmax(0,1fr);
}
}
/* ── panel ── */
.cb-12__panel {
position: relative;
margin: 0;
overflow: hidden;
isolation: isolate;
aspect-ratio: 5/4;
border: 3px solid var(--ink);
border-radius: .35rem;
background: var(--pop);
box-shadow: 6px 6px 0 var(--ink);
transition: transform .32s cubic-bezier(.16,1,.3,1), box-shadow .32s;
}
.cb-12__panel:nth-child(2) {
rotate: -.8deg;
}
.cb-12__panel:nth-child(3) {
rotate: .7deg;
}
.cb-12__panel:hover {
transform: translate(-3px,-4px);
box-shadow: 10px 11px 0 var(--ink);
}
.cb-12__panel img {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
object-fit: cover;
filter: grayscale(1) contrast(1.45) brightness(1.05);
mix-blend-mode: luminosity;
}
/* ── 1 · speech balloon ── */
.cb-12__balloon {
position: absolute;
inset: auto 1rem 1.5rem 1rem;
z-index: 2;
margin: 0;
padding: .7rem .95rem;
border: 3px solid var(--ink);
border-radius: 1.1rem;
background: #fff;
color: var(--ink);
font-size: clamp(.9rem,2.2vw,1rem);
font-weight: 600;
line-height: 1.35;
text-wrap: pretty;
box-shadow: 4px 4px 0 var(--ink);
}
.cb-12__balloon::before,
.cb-12__balloon::after {
content: '';
position: absolute;
inline-size: 0;
block-size: 0;
inset-block-start: 100%;
inset-inline-start: 1.9rem;
}
.cb-12__balloon::before {
border-inline: 12px solid transparent;
border-block-start: 20px solid var(--ink);
}
.cb-12__balloon::after {
inset-inline-start: 2.1rem;
border-inline: 8px solid transparent;
border-block-start: 14px solid #fff;
}
/* ── 2 · starburst shout ── */
.cb-12__shout {
position: absolute;
inset: 50% auto auto 50%;
translate: -50% -50%;
z-index: 2;
margin: 0;
inline-size: min(84%,15rem);
aspect-ratio: 1;
display: grid;
place-items: center;
background: var(--pop-2);
color: #fff;
clip-path: polygon(50% 0%,58% 14%,68% 4%,71% 19%,84% 12%,82% 27%,97% 26%,89% 38%,100% 47%,88% 55%,97% 66%,83% 68%,87% 82%,73% 79%,71% 94%,60% 85%,50% 100%,40% 85%,29% 94%,27% 79%,13% 82%,17% 68%,3% 66%,12% 55%,0% 47%,11% 38%,3% 26%,18% 27%,16% 12%,29% 19%,32% 4%,42% 14%);
animation: cb-12-throb 3.4s cubic-bezier(.16,1,.3,1) infinite;
}
.cb-12__shout::before {
content: '';
position: absolute;
inset: -5px;
z-index: -1;
background: var(--ink);
clip-path: inherit;
}
.cb-12__shout span {
font-family: var(--display);
font-size: clamp(1.5rem,6vw,2.35rem);
line-height: .95;
letter-spacing: .03em;
text-transform: uppercase;
text-align: center;
text-wrap: balance;
paint-order: stroke fill;
}
@keyframes cb-12-throb {
0%,
88%,
100% {
scale: 1;
rotate: -2deg;
}
92% {
scale: 1.05;
rotate: 1deg;
}
}
/* ── 3 · dashed whisper ── */
.cb-12__whisper {
position: absolute;
inset: auto 1.15rem 1.6rem auto;
z-index: 2;
margin: 0;
max-inline-size: 70%;
padding: .6rem .9rem;
border: 2.5px dashed var(--ink);
border-radius: 1.4rem;
background: color-mix(in oklab,#fff 88%,var(--pop));
font-size: clamp(.85rem,2vw,.95rem);
font-weight: 500;
font-style: italic;
line-height: 1.3;
}
.cb-12__whisper::after {
content: '';
position: absolute;
inset-block-start: 100%;
inset-inline-end: 1.6rem;
inline-size: .55rem;
block-size: .55rem;
border-radius: 50%;
border: 2.5px dashed var(--ink);
background: color-mix(in oklab,#fff 88%,var(--pop));
translate: 0 .35rem;
}
/* ── 4 · narration caption ── */
.cb-12__caption {
position: absolute;
inset: .9rem .9rem auto .9rem;
z-index: 2;
margin: 0;
padding: .6rem .85rem;
border: 3px solid var(--ink);
background: var(--pop);
color: var(--ink);
font-size: clamp(.8rem,1.9vw,.9rem);
font-weight: 700;
line-height: 1.3;
letter-spacing: .01em;
text-transform: uppercase;
text-wrap: balance;
box-shadow: 4px 4px 0 var(--ink);
}
/* ── palettes ── */
.cb-12:has(#cb-12-pulp:checked) {
--ink: #1a1210;
--paper: #e8d9b5;
--pop: #f2884b;
--pop-2: #c62828;
--ink-soft: rgba(26,18,16,.18);
}
.cb-12:has(#cb-12-riso:checked) {
--ink: #141a2e;
--paper: #eef1f7;
--pop: #7cc6ff;
--pop-2: #ff3d8b;
--ink-soft: rgba(20,26,46,.13);
}
/* ── switch ── */
.cb-12__switch {
display: flex;
flex-wrap: wrap;
gap: .4rem;
padding: .35rem;
justify-content: center;
border: 3px solid var(--ink);
border-radius: .8rem;
background: #fff;
box-shadow: 5px 5px 0 var(--ink);
}
.cb-12__chip {
min-block-size: 44px;
display: grid;
place-items: center;
padding: .45rem 1.15rem;
cursor: pointer;
border-radius: .5rem;
color: var(--ink);
font-size: .85rem;
font-weight: 700;
letter-spacing: .05em;
text-transform: uppercase;
transition: background .18s, color .18s;
}
.cb-12__chip:hover {
background: var(--pop);
}
#cb-12-ink:checked ~ .cb-12__stage .cb-12__chip[for="cb-12-ink"],
#cb-12-pulp:checked ~ .cb-12__stage .cb-12__chip[for="cb-12-pulp"],
#cb-12-riso:checked ~ .cb-12__stage .cb-12__chip[for="cb-12-riso"] {
background: var(--ink);
color: var(--paper);
}
#cb-12-ink:focus-visible ~ .cb-12__stage .cb-12__chip[for="cb-12-ink"],
#cb-12-pulp:focus-visible ~ .cb-12__stage .cb-12__chip[for="cb-12-pulp"],
#cb-12-riso:focus-visible ~ .cb-12__stage .cb-12__chip[for="cb-12-riso"] {
outline: 3px solid var(--pop-2);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.cb-12 *,
.cb-12 *::before,
.cb-12 *::after {
animation: none !important;
transition: none !important;
}
}
@media (forced-colors: active) {
.cb-12__panel,
.cb-12__balloon,
.cb-12__caption,
.cb-12__whisper {
border: 2px solid CanvasText;
box-shadow: none;
}
.cb-12__panel img {
mix-blend-mode: normal;
filter: none;
}
}Here's a working CSS Chat Bubble 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: CSS Comic Book Speech Bubble
Source: https://codefronts.com/snippets/css-chat-bubbles/css-comic-book-speech-bubble/
A four-panel comic strip with the whole balloon vocabulary drawn in CSS: a rounded speech balloon, a starburst shout cut with a 24-point clip-path, a dashed whisper, and a narration caption. Halftone dots come from a two-layer radial-gradient, panel art is duotoned with filter + mix-blend-mode, and three ink palettes swap with a pure-CSS control.
## HTML
```html
<div class="cb-12">
<input class="cb-12__sr" type="radio" name="cb-12-ink" id="cb-12-ink" checked>
<input class="cb-12__sr" type="radio" name="cb-12-ink" id="cb-12-pulp">
<input class="cb-12__sr" type="radio" name="cb-12-ink" id="cb-12-riso">
<div class="cb-12__stage">
<div class="cb-12__strip">
<figure class="cb-12__panel">
<img src="https://images.unsplash.com/photo-1449824913935-59a10b8d2000?auto=format&fit=crop&w=680&q=70" width="680" height="520" alt="An empty city street at dusk" loading="lazy" decoding="async">
<p class="cb-12__balloon">The whole city went quiet at once.</p>
</figure>
<figure class="cb-12__panel">
<img src="https://images.unsplash.com/photo-1516450360452-9312f5e86fc7?auto=format&fit=crop&w=680&q=70" width="680" height="520" alt="Silhouetted figures under hard shafts of light" loading="lazy" decoding="async">
<p class="cb-12__shout"><span>Not<br>again!</span></p>
</figure>
<figure class="cb-12__panel">
<img src="https://images.unsplash.com/photo-1509023464722-18d996393ca8?auto=format&fit=crop&w=680&q=70" width="680" height="520" alt="A lone peak across still water at first light" loading="lazy" decoding="async">
<p class="cb-12__whisper">don't look up yet…</p>
</figure>
<figure class="cb-12__panel">
<img src="https://images.unsplash.com/photo-1419242902214-272b3f66ee7a?auto=format&fit=crop&w=680&q=70" width="680" height="520" alt="A wide field of stars above a dark treeline" loading="lazy" decoding="async">
<p class="cb-12__caption">Meanwhile, four hundred miles above the argument…</p>
</figure>
</div>
<div class="cb-12__switch" role="radiogroup" aria-label="Ink palette">
<label class="cb-12__chip" for="cb-12-ink">Ink</label>
<label class="cb-12__chip" for="cb-12-pulp">Pulp</label>
<label class="cb-12__chip" for="cb-12-riso">Riso</label>
</div>
</div>
</div>
```
## CSS
```css
@import url('https://fonts.googleapis.com/css2?family=Bangers&family=Archivo:wght@500;600;700&display=swap');
.cb-12 {
--ink: #12100e;
--paper: #f4ead6;
--pop: #ffd23f;
--pop-2: #ff4b3e;
--ink-soft: rgba(18,16,14,.14);
--display: "Bangers",Impact,"Haettenschweiler","Arial Narrow Bold",sans-serif;
--sans: "Archivo",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
position: relative;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(1rem,4vw,3rem);
background-color: var(--paper);
background-image: radial-gradient(circle, var(--ink-soft) 1.1px, transparent 1.2px), radial-gradient(circle, var(--ink-soft) .8px, transparent .9px);
background-size: 9px 9px, 9px 9px;
background-position: 0 0, 4.5px 4.5px;
color: var(--ink);
font-family: var(--sans);
-webkit-font-smoothing: antialiased;
}
.cb-12,
.cb-12 *,
.cb-12 *::before,
.cb-12 *::after {
box-sizing: border-box;
}
.cb-12__sr {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
opacity: 0;
}
.cb-12__stage {
inline-size: min(100%,58rem);
display: grid;
gap: clamp(1.25rem,3vw,2rem);
justify-items: center;
}
.cb-12__strip {
inline-size: 100%;
display: grid;
gap: clamp(.75rem,2vw,1.15rem);
grid-template-columns: repeat(2,minmax(0,1fr));
}
@media (min-width: 64rem) {
.cb-12__strip {
grid-template-columns: repeat(4,minmax(0,1fr));
}
}
@media (max-width: 26rem) {
.cb-12__strip {
grid-template-columns: minmax(0,1fr);
}
}
/* ── panel ── */
.cb-12__panel {
position: relative;
margin: 0;
overflow: hidden;
isolation: isolate;
aspect-ratio: 5/4;
border: 3px solid var(--ink);
border-radius: .35rem;
background: var(--pop);
box-shadow: 6px 6px 0 var(--ink);
transition: transform .32s cubic-bezier(.16,1,.3,1), box-shadow .32s;
}
.cb-12__panel:nth-child(2) {
rotate: -.8deg;
}
.cb-12__panel:nth-child(3) {
rotate: .7deg;
}
.cb-12__panel:hover {
transform: translate(-3px,-4px);
box-shadow: 10px 11px 0 var(--ink);
}
.cb-12__panel img {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
object-fit: cover;
filter: grayscale(1) contrast(1.45) brightness(1.05);
mix-blend-mode: luminosity;
}
/* ── 1 · speech balloon ── */
.cb-12__balloon {
position: absolute;
inset: auto 1rem 1.5rem 1rem;
z-index: 2;
margin: 0;
padding: .7rem .95rem;
border: 3px solid var(--ink);
border-radius: 1.1rem;
background: #fff;
color: var(--ink);
font-size: clamp(.9rem,2.2vw,1rem);
font-weight: 600;
line-height: 1.35;
text-wrap: pretty;
box-shadow: 4px 4px 0 var(--ink);
}
.cb-12__balloon::before,
.cb-12__balloon::after {
content: '';
position: absolute;
inline-size: 0;
block-size: 0;
inset-block-start: 100%;
inset-inline-start: 1.9rem;
}
.cb-12__balloon::before {
border-inline: 12px solid transparent;
border-block-start: 20px solid var(--ink);
}
.cb-12__balloon::after {
inset-inline-start: 2.1rem;
border-inline: 8px solid transparent;
border-block-start: 14px solid #fff;
}
/* ── 2 · starburst shout ── */
.cb-12__shout {
position: absolute;
inset: 50% auto auto 50%;
translate: -50% -50%;
z-index: 2;
margin: 0;
inline-size: min(84%,15rem);
aspect-ratio: 1;
display: grid;
place-items: center;
background: var(--pop-2);
color: #fff;
clip-path: polygon(50% 0%,58% 14%,68% 4%,71% 19%,84% 12%,82% 27%,97% 26%,89% 38%,100% 47%,88% 55%,97% 66%,83% 68%,87% 82%,73% 79%,71% 94%,60% 85%,50% 100%,40% 85%,29% 94%,27% 79%,13% 82%,17% 68%,3% 66%,12% 55%,0% 47%,11% 38%,3% 26%,18% 27%,16% 12%,29% 19%,32% 4%,42% 14%);
animation: cb-12-throb 3.4s cubic-bezier(.16,1,.3,1) infinite;
}
.cb-12__shout::before {
content: '';
position: absolute;
inset: -5px;
z-index: -1;
background: var(--ink);
clip-path: inherit;
}
.cb-12__shout span {
font-family: var(--display);
font-size: clamp(1.5rem,6vw,2.35rem);
line-height: .95;
letter-spacing: .03em;
text-transform: uppercase;
text-align: center;
text-wrap: balance;
paint-order: stroke fill;
}
@keyframes cb-12-throb {
0%,
88%,
100% {
scale: 1;
rotate: -2deg;
}
92% {
scale: 1.05;
rotate: 1deg;
}
}
/* ── 3 · dashed whisper ── */
.cb-12__whisper {
position: absolute;
inset: auto 1.15rem 1.6rem auto;
z-index: 2;
margin: 0;
max-inline-size: 70%;
padding: .6rem .9rem;
border: 2.5px dashed var(--ink);
border-radius: 1.4rem;
background: color-mix(in oklab,#fff 88%,var(--pop));
font-size: clamp(.85rem,2vw,.95rem);
font-weight: 500;
font-style: italic;
line-height: 1.3;
}
.cb-12__whisper::after {
content: '';
position: absolute;
inset-block-start: 100%;
inset-inline-end: 1.6rem;
inline-size: .55rem;
block-size: .55rem;
border-radius: 50%;
border: 2.5px dashed var(--ink);
background: color-mix(in oklab,#fff 88%,var(--pop));
translate: 0 .35rem;
}
/* ── 4 · narration caption ── */
.cb-12__caption {
position: absolute;
inset: .9rem .9rem auto .9rem;
z-index: 2;
margin: 0;
padding: .6rem .85rem;
border: 3px solid var(--ink);
background: var(--pop);
color: var(--ink);
font-size: clamp(.8rem,1.9vw,.9rem);
font-weight: 700;
line-height: 1.3;
letter-spacing: .01em;
text-transform: uppercase;
text-wrap: balance;
box-shadow: 4px 4px 0 var(--ink);
}
/* ── palettes ── */
.cb-12:has(#cb-12-pulp:checked) {
--ink: #1a1210;
--paper: #e8d9b5;
--pop: #f2884b;
--pop-2: #c62828;
--ink-soft: rgba(26,18,16,.18);
}
.cb-12:has(#cb-12-riso:checked) {
--ink: #141a2e;
--paper: #eef1f7;
--pop: #7cc6ff;
--pop-2: #ff3d8b;
--ink-soft: rgba(20,26,46,.13);
}
/* ── switch ── */
.cb-12__switch {
display: flex;
flex-wrap: wrap;
gap: .4rem;
padding: .35rem;
justify-content: center;
border: 3px solid var(--ink);
border-radius: .8rem;
background: #fff;
box-shadow: 5px 5px 0 var(--ink);
}
.cb-12__chip {
min-block-size: 44px;
display: grid;
place-items: center;
padding: .45rem 1.15rem;
cursor: pointer;
border-radius: .5rem;
color: var(--ink);
font-size: .85rem;
font-weight: 700;
letter-spacing: .05em;
text-transform: uppercase;
transition: background .18s, color .18s;
}
.cb-12__chip:hover {
background: var(--pop);
}
#cb-12-ink:checked ~ .cb-12__stage .cb-12__chip[for="cb-12-ink"],
#cb-12-pulp:checked ~ .cb-12__stage .cb-12__chip[for="cb-12-pulp"],
#cb-12-riso:checked ~ .cb-12__stage .cb-12__chip[for="cb-12-riso"] {
background: var(--ink);
color: var(--paper);
}
#cb-12-ink:focus-visible ~ .cb-12__stage .cb-12__chip[for="cb-12-ink"],
#cb-12-pulp:focus-visible ~ .cb-12__stage .cb-12__chip[for="cb-12-pulp"],
#cb-12-riso:focus-visible ~ .cb-12__stage .cb-12__chip[for="cb-12-riso"] {
outline: 3px solid var(--pop-2);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.cb-12 *,
.cb-12 *::before,
.cb-12 *::after {
animation: none !important;
transition: none !important;
}
}
@media (forced-colors: active) {
.cb-12__panel,
.cb-12__balloon,
.cb-12__caption,
.cb-12__whisper {
border: 2px solid CanvasText;
box-shadow: none;
}
.cb-12__panel img {
mix-blend-mode: normal;
filter: none;
}
}
```Here's a working CSS Chat Bubble 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: CSS Comic Book Speech Bubble
Source: https://codefronts.com/snippets/css-chat-bubbles/css-comic-book-speech-bubble/
A four-panel comic strip with the whole balloon vocabulary drawn in CSS: a rounded speech balloon, a starburst shout cut with a 24-point clip-path, a dashed whisper, and a narration caption. Halftone dots come from a two-layer radial-gradient, panel art is duotoned with filter + mix-blend-mode, and three ink palettes swap with a pure-CSS control.
## HTML
```html
<div class="cb-12">
<input class="cb-12__sr" type="radio" name="cb-12-ink" id="cb-12-ink" checked>
<input class="cb-12__sr" type="radio" name="cb-12-ink" id="cb-12-pulp">
<input class="cb-12__sr" type="radio" name="cb-12-ink" id="cb-12-riso">
<div class="cb-12__stage">
<div class="cb-12__strip">
<figure class="cb-12__panel">
<img src="https://images.unsplash.com/photo-1449824913935-59a10b8d2000?auto=format&fit=crop&w=680&q=70" width="680" height="520" alt="An empty city street at dusk" loading="lazy" decoding="async">
<p class="cb-12__balloon">The whole city went quiet at once.</p>
</figure>
<figure class="cb-12__panel">
<img src="https://images.unsplash.com/photo-1516450360452-9312f5e86fc7?auto=format&fit=crop&w=680&q=70" width="680" height="520" alt="Silhouetted figures under hard shafts of light" loading="lazy" decoding="async">
<p class="cb-12__shout"><span>Not<br>again!</span></p>
</figure>
<figure class="cb-12__panel">
<img src="https://images.unsplash.com/photo-1509023464722-18d996393ca8?auto=format&fit=crop&w=680&q=70" width="680" height="520" alt="A lone peak across still water at first light" loading="lazy" decoding="async">
<p class="cb-12__whisper">don't look up yet…</p>
</figure>
<figure class="cb-12__panel">
<img src="https://images.unsplash.com/photo-1419242902214-272b3f66ee7a?auto=format&fit=crop&w=680&q=70" width="680" height="520" alt="A wide field of stars above a dark treeline" loading="lazy" decoding="async">
<p class="cb-12__caption">Meanwhile, four hundred miles above the argument…</p>
</figure>
</div>
<div class="cb-12__switch" role="radiogroup" aria-label="Ink palette">
<label class="cb-12__chip" for="cb-12-ink">Ink</label>
<label class="cb-12__chip" for="cb-12-pulp">Pulp</label>
<label class="cb-12__chip" for="cb-12-riso">Riso</label>
</div>
</div>
</div>
```
## CSS
```css
@import url('https://fonts.googleapis.com/css2?family=Bangers&family=Archivo:wght@500;600;700&display=swap');
.cb-12 {
--ink: #12100e;
--paper: #f4ead6;
--pop: #ffd23f;
--pop-2: #ff4b3e;
--ink-soft: rgba(18,16,14,.14);
--display: "Bangers",Impact,"Haettenschweiler","Arial Narrow Bold",sans-serif;
--sans: "Archivo",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
position: relative;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(1rem,4vw,3rem);
background-color: var(--paper);
background-image: radial-gradient(circle, var(--ink-soft) 1.1px, transparent 1.2px), radial-gradient(circle, var(--ink-soft) .8px, transparent .9px);
background-size: 9px 9px, 9px 9px;
background-position: 0 0, 4.5px 4.5px;
color: var(--ink);
font-family: var(--sans);
-webkit-font-smoothing: antialiased;
}
.cb-12,
.cb-12 *,
.cb-12 *::before,
.cb-12 *::after {
box-sizing: border-box;
}
.cb-12__sr {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
opacity: 0;
}
.cb-12__stage {
inline-size: min(100%,58rem);
display: grid;
gap: clamp(1.25rem,3vw,2rem);
justify-items: center;
}
.cb-12__strip {
inline-size: 100%;
display: grid;
gap: clamp(.75rem,2vw,1.15rem);
grid-template-columns: repeat(2,minmax(0,1fr));
}
@media (min-width: 64rem) {
.cb-12__strip {
grid-template-columns: repeat(4,minmax(0,1fr));
}
}
@media (max-width: 26rem) {
.cb-12__strip {
grid-template-columns: minmax(0,1fr);
}
}
/* ── panel ── */
.cb-12__panel {
position: relative;
margin: 0;
overflow: hidden;
isolation: isolate;
aspect-ratio: 5/4;
border: 3px solid var(--ink);
border-radius: .35rem;
background: var(--pop);
box-shadow: 6px 6px 0 var(--ink);
transition: transform .32s cubic-bezier(.16,1,.3,1), box-shadow .32s;
}
.cb-12__panel:nth-child(2) {
rotate: -.8deg;
}
.cb-12__panel:nth-child(3) {
rotate: .7deg;
}
.cb-12__panel:hover {
transform: translate(-3px,-4px);
box-shadow: 10px 11px 0 var(--ink);
}
.cb-12__panel img {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
object-fit: cover;
filter: grayscale(1) contrast(1.45) brightness(1.05);
mix-blend-mode: luminosity;
}
/* ── 1 · speech balloon ── */
.cb-12__balloon {
position: absolute;
inset: auto 1rem 1.5rem 1rem;
z-index: 2;
margin: 0;
padding: .7rem .95rem;
border: 3px solid var(--ink);
border-radius: 1.1rem;
background: #fff;
color: var(--ink);
font-size: clamp(.9rem,2.2vw,1rem);
font-weight: 600;
line-height: 1.35;
text-wrap: pretty;
box-shadow: 4px 4px 0 var(--ink);
}
.cb-12__balloon::before,
.cb-12__balloon::after {
content: '';
position: absolute;
inline-size: 0;
block-size: 0;
inset-block-start: 100%;
inset-inline-start: 1.9rem;
}
.cb-12__balloon::before {
border-inline: 12px solid transparent;
border-block-start: 20px solid var(--ink);
}
.cb-12__balloon::after {
inset-inline-start: 2.1rem;
border-inline: 8px solid transparent;
border-block-start: 14px solid #fff;
}
/* ── 2 · starburst shout ── */
.cb-12__shout {
position: absolute;
inset: 50% auto auto 50%;
translate: -50% -50%;
z-index: 2;
margin: 0;
inline-size: min(84%,15rem);
aspect-ratio: 1;
display: grid;
place-items: center;
background: var(--pop-2);
color: #fff;
clip-path: polygon(50% 0%,58% 14%,68% 4%,71% 19%,84% 12%,82% 27%,97% 26%,89% 38%,100% 47%,88% 55%,97% 66%,83% 68%,87% 82%,73% 79%,71% 94%,60% 85%,50% 100%,40% 85%,29% 94%,27% 79%,13% 82%,17% 68%,3% 66%,12% 55%,0% 47%,11% 38%,3% 26%,18% 27%,16% 12%,29% 19%,32% 4%,42% 14%);
animation: cb-12-throb 3.4s cubic-bezier(.16,1,.3,1) infinite;
}
.cb-12__shout::before {
content: '';
position: absolute;
inset: -5px;
z-index: -1;
background: var(--ink);
clip-path: inherit;
}
.cb-12__shout span {
font-family: var(--display);
font-size: clamp(1.5rem,6vw,2.35rem);
line-height: .95;
letter-spacing: .03em;
text-transform: uppercase;
text-align: center;
text-wrap: balance;
paint-order: stroke fill;
}
@keyframes cb-12-throb {
0%,
88%,
100% {
scale: 1;
rotate: -2deg;
}
92% {
scale: 1.05;
rotate: 1deg;
}
}
/* ── 3 · dashed whisper ── */
.cb-12__whisper {
position: absolute;
inset: auto 1.15rem 1.6rem auto;
z-index: 2;
margin: 0;
max-inline-size: 70%;
padding: .6rem .9rem;
border: 2.5px dashed var(--ink);
border-radius: 1.4rem;
background: color-mix(in oklab,#fff 88%,var(--pop));
font-size: clamp(.85rem,2vw,.95rem);
font-weight: 500;
font-style: italic;
line-height: 1.3;
}
.cb-12__whisper::after {
content: '';
position: absolute;
inset-block-start: 100%;
inset-inline-end: 1.6rem;
inline-size: .55rem;
block-size: .55rem;
border-radius: 50%;
border: 2.5px dashed var(--ink);
background: color-mix(in oklab,#fff 88%,var(--pop));
translate: 0 .35rem;
}
/* ── 4 · narration caption ── */
.cb-12__caption {
position: absolute;
inset: .9rem .9rem auto .9rem;
z-index: 2;
margin: 0;
padding: .6rem .85rem;
border: 3px solid var(--ink);
background: var(--pop);
color: var(--ink);
font-size: clamp(.8rem,1.9vw,.9rem);
font-weight: 700;
line-height: 1.3;
letter-spacing: .01em;
text-transform: uppercase;
text-wrap: balance;
box-shadow: 4px 4px 0 var(--ink);
}
/* ── palettes ── */
.cb-12:has(#cb-12-pulp:checked) {
--ink: #1a1210;
--paper: #e8d9b5;
--pop: #f2884b;
--pop-2: #c62828;
--ink-soft: rgba(26,18,16,.18);
}
.cb-12:has(#cb-12-riso:checked) {
--ink: #141a2e;
--paper: #eef1f7;
--pop: #7cc6ff;
--pop-2: #ff3d8b;
--ink-soft: rgba(20,26,46,.13);
}
/* ── switch ── */
.cb-12__switch {
display: flex;
flex-wrap: wrap;
gap: .4rem;
padding: .35rem;
justify-content: center;
border: 3px solid var(--ink);
border-radius: .8rem;
background: #fff;
box-shadow: 5px 5px 0 var(--ink);
}
.cb-12__chip {
min-block-size: 44px;
display: grid;
place-items: center;
padding: .45rem 1.15rem;
cursor: pointer;
border-radius: .5rem;
color: var(--ink);
font-size: .85rem;
font-weight: 700;
letter-spacing: .05em;
text-transform: uppercase;
transition: background .18s, color .18s;
}
.cb-12__chip:hover {
background: var(--pop);
}
#cb-12-ink:checked ~ .cb-12__stage .cb-12__chip[for="cb-12-ink"],
#cb-12-pulp:checked ~ .cb-12__stage .cb-12__chip[for="cb-12-pulp"],
#cb-12-riso:checked ~ .cb-12__stage .cb-12__chip[for="cb-12-riso"] {
background: var(--ink);
color: var(--paper);
}
#cb-12-ink:focus-visible ~ .cb-12__stage .cb-12__chip[for="cb-12-ink"],
#cb-12-pulp:focus-visible ~ .cb-12__stage .cb-12__chip[for="cb-12-pulp"],
#cb-12-riso:focus-visible ~ .cb-12__stage .cb-12__chip[for="cb-12-riso"] {
outline: 3px solid var(--pop-2);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.cb-12 *,
.cb-12 *::before,
.cb-12 *::after {
animation: none !important;
transition: none !important;
}
}
@media (forced-colors: active) {
.cb-12__panel,
.cb-12__balloon,
.cb-12__caption,
.cb-12__whisper {
border: 2px solid CanvasText;
box-shadow: none;
}
.cb-12__panel img {
mix-blend-mode: normal;
filter: none;
}
}
```How this works
The starburst. A star is just a polygon whose radius alternates between two values. Twelve spikes means 24 points — and because the coordinates are percentages, the shape scales with the text inside it:
.cb-12__shout{
clip-path:polygon(
50% 0%, 58% 14%, 68% 4%, 71% 19%, 84% 12%, 82% 27%, 97% 26%, 89% 38%,
100% 47%, 88% 55%, 97% 66%, 83% 68%, 87% 82%, 73% 79%, 71% 94%, 60% 85%,
50% 100%, 40% 85%, 29% 94%, 27% 79%, 13% 82%, 17% 68%, 3% 66%, 12% 55%,
0% 47%, 11% 38%, 3% 26%, 18% 27%, 16% 12%, 29% 19%, 32% 4%, 42% 14%);
}A clipped element cannot also have a border — so the outline is a second, slightly larger clipped element behind it, which is exactly how a screen-printed comic builds a keyline.
Halftone. Two repeating radial gradients at different sizes and offsets read as a printed dot screen, and they cost nothing to render because they never animate:
background-image:
radial-gradient(circle, var(--ink) 1.1px, transparent 1.2px),
radial-gradient(circle, var(--ink) .8px, transparent .9px);
background-size:8px 8px, 8px 8px;
background-position:0 0, 4px 4px;Ink outlines have no blur. The whole look depends on it — a hard offset shadow reads as printed ink, a soft one reads as a web card:
box-shadow:5px 5px 0 var(--ink); /* not 5px 5px 12px */Duotone art. Desaturate the photograph, crush the contrast, then multiply a flat colour over it. Two properties turn any stock photo into panel art.
Make it yours
- Change the spike count by editing the polygon — swap 12 spikes for 8 by halving the point list; keep the radii at roughly 100% and 58%.
- Tune the halftone density with
background-size: 6px for a fine screen, 12px for pulp newsprint. - Add a fourth palette by copying one
:has()token block; each palette is four values (--ink,--paper,--pop,--pop-2). - For real lettering, load a comic display face and set
letter-spacing: .02emplustext-transform: uppercaseon the shout only — body balloons stay mixed case for readability. - Rotate a panel with
rotate: -1.2degfor a hand-pasted feel; keep it under 2° or the text becomes tiring to read. - Swap the photos for your own art — the duotone filter chain works on anything with a mid-grey range.
Gotchas — read before shipping
clip-pathandborderare mutually exclusive: the clip cuts through the border. Layer two shapes instead.- Comic display faces are usually all-caps with tight spacing — never set body copy in them. Keep the balloon text at 1rem or larger in a readable face.
- Halftone dots at low opacity can create moiré against a photo. Put the screen on the panel background, not over the image.
mix-blend-modecreates a new stacking context, which can trap your balloons behind the art — give balloons az-indexand keep the blend on the image only.- Uppercase text needs a real
text-transform, not typed capitals: typed caps are read letter-by-letter by some screen readers. - Hard-shadow outlines disappear under
forced-colors; add a real border inside a@media (forced-colors: active)block, as here.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 114+ | 17.5+ | 121+ | 114+ |
Floor set by :has(), color-mix(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 105+.
clip-path polygons, mix-blend-mode, filter and gradient halftones are supported in every evergreen browser and back to about 2018. :has() (Chrome 105 / Safari 15.4 / Firefox 121) drives the palette switch — replace it with a class on the root for older support. text-wrap: balance is Chrome 114 / Safari 17.5 / Firefox 121 and is cosmetic only.