Comics — Speech / Thought / Shout Balloon Set
A comic-book lettering kit of five CSS-drawn balloons set on inked, halftone panels for the fictional Neon Ronin series. A pointed speech balloon, a scalloped thought bubble with a trailing dot chain, a jagged Bangers-lettered shout burst, a dashed whisper, and a boxed narration caption each sit over gradient panel art. Segmented controls swap the active style while a live lettering field retypes the words, and clicking any balloon cycles its tail between left, right, and down with toast feedback and full keyboard support.
MCP
コード
:root {
--ink: #0e0e12;
--ink-2: #23232b;
--paper: #fdfcf7;
--panel: #ffffff;
--accent: #ff2e4d;
--accent-2: #ffd23f;
--accent-blue: #2e6bff;
--muted: #6b6b78;
--line: rgba(14, 14, 18, 0.14);
--line-2: rgba(14, 14, 18, 0.28);
--halftone: radial-gradient(circle, rgba(14, 14, 18, 0.18) 1px, transparent 1.6px);
--r-sm: 6px;
--r-md: 12px;
--r-lg: 18px;
--shadow-ink: 4px 4px 0 var(--ink);
--shadow-soft: 6px 6px 0 rgba(14, 14, 18, 0.85);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
font-family: "Inter", system-ui, sans-serif;
line-height: 1.5;
color: var(--ink);
background-color: var(--paper);
background-image:
var(--halftone),
linear-gradient(180deg, rgba(255, 210, 63, 0.08), transparent 280px);
background-size: 7px 7px, 100% 100%;
}
.skip-link {
position: absolute;
left: -9999px;
top: 0;
background: var(--ink);
color: var(--paper);
padding: 10px 16px;
z-index: 50;
font-weight: 700;
}
.skip-link:focus {
left: 12px;
top: 12px;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
/* ---------- Top bar ---------- */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
padding: 18px 24px;
background: var(--ink);
color: var(--paper);
border-bottom: 4px solid var(--accent);
}
.brand {
display: flex;
align-items: center;
gap: 14px;
}
.brand-mark {
font-family: "Bangers", system-ui, sans-serif;
font-size: 2rem;
line-height: 1;
color: var(--ink);
background: var(--accent-2);
border: 3px solid var(--paper);
border-radius: var(--r-sm);
padding: 4px 12px;
transform: rotate(-4deg);
box-shadow: 3px 3px 0 var(--accent);
}
.brand-title {
font-family: "Bangers", system-ui, sans-serif;
font-size: 1.8rem;
letter-spacing: 0.04em;
margin: 0;
}
.brand-sub {
margin: 2px 0 0;
font-size: 0.8rem;
color: rgba(253, 252, 247, 0.72);
letter-spacing: 0.02em;
}
.brand-tag {
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.16em;
color: var(--accent-2);
border: 2px solid var(--accent-2);
border-radius: var(--r-sm);
padding: 6px 10px;
}
/* ---------- Layout ---------- */
.wrap {
max-width: 1080px;
margin: 0 auto;
padding: 32px 24px 56px;
}
.intro {
margin-bottom: 28px;
}
.kicker {
display: inline-block;
margin: 0 0 8px;
font-size: 0.72rem;
font-weight: 800;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--paper);
background: var(--accent);
padding: 5px 10px;
border: 2px solid var(--ink);
transform: rotate(-1.5deg);
}
.intro-title {
font-family: "Bangers", system-ui, sans-serif;
font-size: clamp(2.4rem, 7vw, 3.8rem);
letter-spacing: 0.03em;
margin: 4px 0 12px;
text-shadow: 3px 3px 0 var(--accent-2);
}
.intro-lede {
max-width: 60ch;
margin: 0;
font-size: 1.02rem;
color: var(--ink-2);
}
/* ---------- Controls ---------- */
.controls {
display: flex;
flex-wrap: wrap;
gap: 18px;
align-items: flex-end;
margin-bottom: 32px;
padding: 18px;
background: var(--panel);
border: 3px solid var(--ink);
border-radius: var(--r-md);
box-shadow: var(--shadow-ink);
}
.control-group {
border: 0;
margin: 0;
padding: 0;
min-width: 0;
}
.control-group legend {
font-size: 0.72rem;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 8px;
padding: 0;
}
.seg {
display: inline-flex;
flex-wrap: wrap;
gap: 0;
border: 3px solid var(--ink);
border-radius: var(--r-sm);
overflow: hidden;
}
.seg-btn {
font: inherit;
font-weight: 700;
cursor: pointer;
border: 0;
border-right: 2px solid var(--ink);
background: var(--paper);
color: var(--ink);
padding: 9px 14px;
transition: background 0.12s ease, color 0.12s ease, transform 0.05s ease;
}
.seg-btn:last-child {
border-right: 0;
}
.seg-btn:hover {
background: var(--accent-2);
}
.seg-btn:active {
transform: translateY(1px);
}
.seg-btn.is-active {
background: var(--accent);
color: var(--paper);
}
.seg-btn:focus-visible {
outline: 3px solid var(--accent-blue);
outline-offset: -3px;
}
.control-text {
flex: 1 1 320px;
display: flex;
gap: 10px;
align-items: flex-end;
}
.control-text legend {
flex-basis: 100%;
}
.text-input {
flex: 1;
font: inherit;
padding: 9px 12px;
border: 3px solid var(--ink);
border-radius: var(--r-sm);
background: var(--paper);
min-width: 0;
}
.text-input:focus-visible {
outline: 3px solid var(--accent-blue);
outline-offset: 1px;
}
.ink-btn {
font: inherit;
font-weight: 700;
cursor: pointer;
white-space: nowrap;
background: var(--ink);
color: var(--paper);
border: 3px solid var(--ink);
border-radius: var(--r-sm);
padding: 9px 16px;
box-shadow: 3px 3px 0 var(--accent);
transition: transform 0.06s ease, box-shadow 0.06s ease;
}
.ink-btn:hover {
transform: translate(-1px, -1px);
box-shadow: 4px 4px 0 var(--accent);
}
.ink-btn:active {
transform: translate(2px, 2px);
box-shadow: 1px 1px 0 var(--accent);
}
.ink-btn:focus-visible {
outline: 3px solid var(--accent-blue);
outline-offset: 2px;
}
/* ---------- Stage / panels ---------- */
.stage {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 22px;
}
.panel--street {
grid-column: 1 / -1;
}
.panel {
position: relative;
min-height: 220px;
padding: 44px 26px 30px;
display: flex;
align-items: center;
justify-content: center;
border: 3px solid var(--ink);
border-radius: var(--r-sm);
box-shadow: var(--shadow-soft);
overflow: hidden;
isolation: isolate;
}
.panel::after {
content: "";
position: absolute;
inset: 0;
background-image: var(--halftone);
background-size: 6px 6px;
opacity: 0.5;
z-index: -1;
pointer-events: none;
}
.panel--alley {
background: linear-gradient(135deg, #2e6bff 0%, #142b6b 100%);
}
.panel--sky {
background: linear-gradient(160deg, #8aa6ff 0%, #4b5bb5 60%, #23232b 100%);
}
.panel--dojo {
background: linear-gradient(135deg, #ff2e4d 0%, #7a1024 100%);
}
.panel--lab {
background: linear-gradient(150deg, #23232b 0%, #0e0e12 100%);
}
.panel--street {
background: linear-gradient(120deg, #ffd23f 0%, #ff7a3f 55%, #ff2e4d 100%);
min-height: 180px;
}
.panel-label {
position: absolute;
top: 10px;
left: 10px;
z-index: 2;
font-size: 0.66rem;
font-weight: 800;
letter-spacing: 0.12em;
color: var(--ink);
background: var(--paper);
border: 2px solid var(--ink);
padding: 3px 8px;
}
/* ---------- Balloons (shared) ---------- */
.balloon {
position: relative;
max-width: 78%;
padding: 16px 20px;
background: var(--panel);
color: var(--ink);
border: 3px solid var(--ink);
cursor: pointer;
z-index: 3;
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.balloon:hover {
transform: translateY(-2px) scale(1.015);
}
.balloon:active {
transform: translateY(0) scale(0.99);
}
.balloon:focus-visible {
outline: 3px solid var(--accent-blue);
outline-offset: 4px;
}
.balloon-text {
margin: 0;
font-weight: 600;
font-size: 1rem;
text-align: center;
}
/* Speech */
.balloon--speech {
border-radius: 22px;
box-shadow: 4px 4px 0 rgba(14, 14, 18, 0.35);
}
.balloon--speech::after {
content: "";
position: absolute;
bottom: -16px;
width: 26px;
height: 22px;
background: var(--panel);
border: 3px solid var(--ink);
border-top: 0;
z-index: 2;
}
.balloon--speech.tail-left::after {
left: 26px;
clip-path: polygon(0 0, 100% 0, 12% 100%);
transform: skewX(-4deg);
}
.balloon--speech.tail-right::after {
right: 26px;
clip-path: polygon(0 0, 100% 0, 88% 100%);
transform: skewX(4deg);
}
.balloon--speech.tail-down::after {
left: 50%;
transform: translateX(-50%);
clip-path: polygon(0 0, 100% 0, 50% 100%);
}
/* mask the seam between balloon body and tail */
.balloon--speech::before {
content: "";
position: absolute;
bottom: -3px;
height: 6px;
width: 30px;
background: var(--panel);
z-index: 3;
}
.balloon--speech.tail-left::before {
left: 24px;
}
.balloon--speech.tail-right::before {
right: 24px;
}
.balloon--speech.tail-down::before {
left: 50%;
transform: translateX(-50%);
}
/* Thought */
.balloon--thought {
border-radius: 50% / 42%;
padding: 22px 26px;
}
.balloon--thought .balloon-text {
font-style: italic;
}
.thought-trail {
position: absolute;
width: 0;
height: 0;
}
.thought-trail::before,
.thought-trail::after {
content: "";
position: absolute;
background: var(--panel);
border: 3px solid var(--ink);
border-radius: 50%;
}
.thought-trail::before {
width: 18px;
height: 18px;
}
.thought-trail::after {
width: 10px;
height: 10px;
}
.balloon--thought.tail-down .thought-trail::before {
left: 50%;
bottom: -28px;
}
.balloon--thought.tail-down .thought-trail::after {
left: 56%;
bottom: -46px;
}
.balloon--thought.tail-left .thought-trail::before {
left: 6px;
bottom: -26px;
}
.balloon--thought.tail-left .thought-trail::after {
left: -8px;
bottom: -44px;
}
.balloon--thought.tail-right .thought-trail::before {
right: 6px;
bottom: -26px;
}
.balloon--thought.tail-right .thought-trail::after {
right: -8px;
bottom: -44px;
}
/* Shout / burst */
.balloon--shout {
background: var(--accent-2);
border-width: 4px;
border-radius: var(--r-sm);
padding: 22px 28px;
clip-path: polygon(
0% 18%, 9% 8%, 20% 16%, 33% 4%, 46% 14%, 58% 2%, 70% 13%,
83% 5%, 94% 15%, 100% 22%, 92% 36%, 100% 50%, 92% 64%, 100% 78%,
90% 90%, 80% 82%, 68% 96%, 55% 86%, 44% 98%, 32% 86%, 20% 96%,
10% 84%, 0% 78%, 7% 62%, 0% 50%, 7% 36%
);
transform: rotate(-2deg);
}
.balloon--shout:hover {
transform: rotate(-2deg) translateY(-2px) scale(1.02);
}
.balloon--shout .balloon-text {
font-family: "Bangers", system-ui, sans-serif;
font-weight: 400;
font-size: 1.7rem;
letter-spacing: 0.04em;
color: var(--ink);
text-transform: uppercase;
}
/* shout tail spike */
.balloon--shout::after {
content: "";
position: absolute;
bottom: -14px;
width: 28px;
height: 26px;
background: var(--accent-2);
z-index: 1;
}
.balloon--shout.tail-left::after {
left: 18%;
clip-path: polygon(0 0, 100% 0, 20% 100%);
}
.balloon--shout.tail-right::after {
right: 18%;
clip-path: polygon(0 0, 100% 0, 80% 100%);
}
.balloon--shout.tail-down::after {
left: 50%;
transform: translateX(-50%);
clip-path: polygon(0 0, 100% 0, 50% 100%);
}
/* Whisper */
.balloon--whisper {
border-style: dashed;
border-radius: 20px;
background: rgba(255, 255, 255, 0.92);
}
.balloon--whisper .balloon-text {
font-style: italic;
font-weight: 500;
color: var(--ink-2);
}
.balloon--whisper::after {
content: "";
position: absolute;
bottom: -16px;
width: 24px;
height: 20px;
background: rgba(255, 255, 255, 0.92);
border: 3px dashed var(--ink);
border-top: 0;
}
.balloon--whisper.tail-left::after {
left: 28px;
clip-path: polygon(0 0, 100% 0, 12% 100%);
}
.balloon--whisper.tail-right::after {
right: 28px;
clip-path: polygon(0 0, 100% 0, 88% 100%);
}
.balloon--whisper.tail-down::after {
left: 50%;
transform: translateX(-50%);
clip-path: polygon(0 0, 100% 0, 50% 100%);
}
/* Caption box */
.balloon--caption {
border-radius: 0;
background: var(--paper);
border-width: 3px;
box-shadow: var(--shadow-ink);
max-width: 88%;
}
.balloon--caption .balloon-text {
text-align: left;
font-weight: 600;
}
.balloon--caption .balloon-text::first-letter {
/* keeps narration tidy */
font-weight: 700;
}
/* ---------- Legend ---------- */
.legend {
margin-top: 34px;
padding: 20px 22px;
background: var(--panel);
border: 3px solid var(--ink);
border-radius: var(--r-md);
box-shadow: var(--shadow-ink);
}
.legend-title {
font-family: "Bangers", system-ui, sans-serif;
font-size: 1.6rem;
letter-spacing: 0.04em;
margin: 0 0 14px;
}
.legend-grid {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
}
.legend-grid li {
display: flex;
align-items: center;
gap: 10px;
font-size: 0.92rem;
color: var(--ink-2);
}
.dot {
flex: none;
width: 16px;
height: 16px;
border: 2px solid var(--ink);
border-radius: 50%;
}
.dot--speech {
background: var(--panel);
}
.dot--thought {
background: var(--accent-blue);
}
.dot--shout {
background: var(--accent-2);
}
.dot--whisper {
background: rgba(255, 255, 255, 0.5);
border-style: dashed;
}
.dot--caption {
background: var(--accent);
border-radius: 0;
}
/* ---------- SFX ---------- */
.sfx {
position: absolute;
z-index: 4;
font-family: "Bangers", system-ui, sans-serif;
text-transform: uppercase;
pointer-events: none;
line-height: 0.9;
}
.sfx--big {
bottom: 14px;
right: 14px;
font-size: clamp(1.8rem, 6vw, 2.8rem);
color: var(--accent-2);
-webkit-text-stroke: 2px var(--ink);
text-stroke: 2px var(--ink);
transform: rotate(6deg);
text-shadow: 3px 3px 0 var(--ink);
}
.sfx--small {
bottom: 16px;
right: 16px;
font-size: 1.4rem;
color: var(--paper);
transform: rotate(-8deg);
opacity: 0.85;
}
/* ---------- Footer ---------- */
.foot {
border-top: 4px solid var(--ink);
background: var(--ink);
color: rgba(253, 252, 247, 0.82);
text-align: center;
padding: 18px 24px;
font-size: 0.85rem;
}
.foot strong {
color: var(--accent-2);
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 24px);
background: var(--ink);
color: var(--paper);
font-weight: 700;
padding: 12px 20px;
border: 3px solid var(--accent-2);
border-radius: var(--r-sm);
box-shadow: var(--shadow-ink);
opacity: 0;
pointer-events: none;
transition: opacity 0.18s ease, transform 0.18s ease;
z-index: 60;
max-width: 90vw;
}
.toast.is-on {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
.topbar {
padding: 14px 16px;
}
.brand-title {
font-size: 1.5rem;
}
.wrap {
padding: 24px 16px 44px;
}
.controls {
padding: 14px;
}
.seg {
width: 100%;
}
.seg-btn {
flex: 1 1 auto;
padding: 9px 10px;
}
.control-text {
flex-direction: column;
align-items: stretch;
}
.ink-btn {
width: 100%;
}
.stage {
grid-template-columns: 1fr;
}
.balloon {
max-width: 90%;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
transition: none !important;
}
}(function () {
"use strict";
var TAIL_ORDER = ["tail-left", "tail-right", "tail-down"];
var TAIL_LABEL = {
"tail-left": "left",
"tail-right": "right",
"tail-down": "down",
};
var balloons = Array.prototype.slice.call(
document.querySelectorAll(".balloon")
);
var segBtns = Array.prototype.slice.call(
document.querySelectorAll(".seg-btn")
);
var textInput = document.getElementById("balloon-text");
var applyBtn = document.getElementById("apply-text");
var activeStyle = "speech";
var toastTimer = null;
/* ---- toast helper ---- */
function toast(msg) {
var el = document.getElementById("toast");
if (!el) return;
el.textContent = msg;
el.classList.add("is-on");
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
el.classList.remove("is-on");
}, 1900);
}
/* ---- find which balloon is the active style ---- */
function activeBalloon() {
for (var i = 0; i < balloons.length; i++) {
if (balloons[i].getAttribute("data-type") === activeStyle) {
return balloons[i];
}
}
return null;
}
function currentTail(balloon) {
for (var i = 0; i < TAIL_ORDER.length; i++) {
if (balloon.classList.contains(TAIL_ORDER[i])) return TAIL_ORDER[i];
}
return null;
}
/* ---- cycle tail direction on click ---- */
function cycleTail(balloon) {
var type = balloon.getAttribute("data-type");
if (type === "caption") {
toast("Caption boxes have no tail.");
return;
}
var current = currentTail(balloon);
var idx = TAIL_ORDER.indexOf(current);
var next = TAIL_ORDER[(idx + 1) % TAIL_ORDER.length];
if (current) balloon.classList.remove(current);
balloon.classList.add(next);
toast(cap(type) + " tail → " + TAIL_LABEL[next]);
}
function cap(s) {
return s.charAt(0).toUpperCase() + s.slice(1);
}
balloons.forEach(function (balloon) {
balloon.addEventListener("click", function () {
cycleTail(balloon);
});
balloon.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " " || e.key === "Spacebar") {
e.preventDefault();
cycleTail(balloon);
}
});
});
/* ---- style selector (focus + highlight the matching balloon) ---- */
function setActiveStyle(style, btn) {
activeStyle = style;
segBtns.forEach(function (b) {
var on = b === btn;
b.classList.toggle("is-active", on);
b.setAttribute("aria-checked", on ? "true" : "false");
});
var target = activeBalloon();
if (target) {
// brief pulse so the user can spot the active style
target.animate(
[
{ transform: "scale(1)" },
{ transform: "scale(1.06)" },
{ transform: "scale(1)" },
],
{ duration: 320, easing: "ease-out" }
);
target.focus({ preventScroll: false });
target.scrollIntoView({ behavior: "smooth", block: "center" });
}
toast("Editing: " + cap(style));
}
segBtns.forEach(function (btn) {
btn.addEventListener("click", function () {
setActiveStyle(btn.getAttribute("data-style"), btn);
});
// arrow-key navigation across the radiogroup
btn.addEventListener("keydown", function (e) {
var idx = segBtns.indexOf(btn);
var nextIdx = null;
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
nextIdx = (idx + 1) % segBtns.length;
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
nextIdx = (idx - 1 + segBtns.length) % segBtns.length;
}
if (nextIdx !== null) {
e.preventDefault();
var nb = segBtns[nextIdx];
nb.focus();
setActiveStyle(nb.getAttribute("data-style"), nb);
}
});
});
/* ---- apply lettering to the active balloon ---- */
function applyText() {
var value = (textInput.value || "").trim();
if (!value) {
toast("Type some lettering first!");
textInput.focus();
return;
}
var target = activeBalloon();
if (!target) return;
var p = target.querySelector(".balloon-text");
if (!p) return;
// shout balloons read as upper-case lettering
p.textContent = activeStyle === "shout" ? value.toUpperCase() : value;
target.animate(
[{ opacity: 0.35 }, { opacity: 1 }],
{ duration: 260, easing: "ease-out" }
);
toast(cap(activeStyle) + " lettering set.");
}
applyBtn.addEventListener("click", applyText);
textInput.addEventListener("keydown", function (e) {
if (e.key === "Enter") {
e.preventDefault();
applyText();
}
});
// greet once mounted
toast("Click any balloon to cycle its tail.");
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Comics — Speech / Thought / Shout Balloon Set</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Bangers&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#main">Skip to balloons</a>
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">!?</span>
<div class="brand-text">
<h1 class="brand-title">NEON RONIN</h1>
<p class="brand-sub">Balloon & Lettering Kit — Issue #07</p>
</div>
</div>
<div class="brand-tag">VANILLA JS • CSS TAILS</div>
</header>
<main id="main" class="wrap">
<section class="intro">
<p class="kicker">Letterer's Toolkit</p>
<h2 class="intro-title">Speech / Thought / Shout</h2>
<p class="intro-lede">
Five hand-inked balloon styles drawn entirely in CSS — pointed speech tails,
a trailing thought bubble, a jagged burst shout, a dashed whisper, and a
boxed caption. Click any balloon to cycle its tail; use the controls to
retype the lettering or swap balloon styles live.
</p>
</section>
<section class="controls" aria-label="Balloon controls">
<fieldset class="control-group">
<legend>Active style</legend>
<div class="seg" role="radiogroup" aria-label="Balloon style">
<button class="seg-btn is-active" role="radio" aria-checked="true" data-style="speech" type="button">Speech</button>
<button class="seg-btn" role="radio" aria-checked="false" data-style="thought" type="button">Thought</button>
<button class="seg-btn" role="radio" aria-checked="false" data-style="shout" type="button">Shout</button>
<button class="seg-btn" role="radio" aria-checked="false" data-style="whisper" type="button">Whisper</button>
<button class="seg-btn" role="radio" aria-checked="false" data-style="caption" type="button">Caption</button>
</div>
</fieldset>
<fieldset class="control-group control-text">
<legend>Lettering</legend>
<label class="visually-hidden" for="balloon-text">Balloon text</label>
<input id="balloon-text" class="text-input" type="text" maxlength="80"
value="I'll cut the neon — meet me on the rooftop!" />
<button id="apply-text" class="ink-btn" type="button">Set Lettering</button>
</fieldset>
</section>
<section class="stage" aria-label="Balloon panels">
<article class="panel panel--alley">
<span class="panel-label">PANEL 01 — BACK ALLEY</span>
<div class="balloon balloon--speech tail-left" data-type="speech" tabindex="0" role="button"
aria-label="Speech balloon. Press Enter to cycle tail direction.">
<p class="balloon-text">I’ll cut the neon — meet me on the rooftop!</p>
</div>
<span class="sfx sfx--small" aria-hidden="true">tsss</span>
</article>
<article class="panel panel--sky">
<span class="panel-label">PANEL 02 — RAIN SKY</span>
<div class="balloon balloon--thought tail-down" data-type="thought" tabindex="0" role="button"
aria-label="Thought bubble. Press Enter to cycle tail direction.">
<p class="balloon-text">Was the Iron Vanguard ever really on our side…?</p>
<span class="thought-trail" aria-hidden="true"></span>
</div>
</article>
<article class="panel panel--dojo">
<span class="panel-label">PANEL 03 — DOJO RUINS</span>
<div class="balloon balloon--shout tail-right" data-type="shout" tabindex="0" role="button"
aria-label="Shout balloon. Press Enter to cycle tail direction.">
<p class="balloon-text">YOU CALL THAT A BLADE?!</p>
</div>
<span class="sfx sfx--big" aria-hidden="true">KRAKOOM</span>
</article>
<article class="panel panel--lab">
<span class="panel-label">PANEL 04 — HIDDEN LAB</span>
<div class="balloon balloon--whisper tail-left" data-type="whisper" tabindex="0" role="button"
aria-label="Whisper balloon. Press Enter to cycle tail direction.">
<p class="balloon-text">…keep your voice down, the drones can hear us.</p>
</div>
</article>
<article class="panel panel--street">
<span class="panel-label">PANEL 05 — NIGHT MARKET</span>
<div class="balloon balloon--caption" data-type="caption" tabindex="0" role="button"
aria-label="Caption box. Tails are disabled for captions.">
<p class="balloon-text">MEANWHILE, three districts away, the rain had only just begun.</p>
</div>
</article>
</section>
<section class="legend" aria-label="Balloon style guide">
<h3 class="legend-title">Style Guide</h3>
<ul class="legend-grid">
<li><span class="dot dot--speech"></span> <strong>Speech</strong> — ink outline, pointed tail</li>
<li><span class="dot dot--thought"></span> <strong>Thought</strong> — scalloped, bubble trail</li>
<li><span class="dot dot--shout"></span> <strong>Shout</strong> — jagged burst, Bangers</li>
<li><span class="dot dot--whisper"></span> <strong>Whisper</strong> — dashed outline</li>
<li><span class="dot dot--caption"></span> <strong>Caption</strong> — boxed narration</li>
</ul>
</section>
</main>
<footer class="foot">
<p>Click a balloon to cycle its tail • Fictional series — <strong>Neon Ronin</strong></p>
</footer>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Speech / Thought / Shout Balloon Set
A letterer’s toolkit for the fictional Neon Ronin comic, rendering five distinct balloon styles entirely in CSS — no images. A rounded speech balloon carries a pointed pseudo-element tail, a scalloped thought bubble trails two shrinking dots, a jagged clip-path burst shouts in the Bangers display face, a dashed-outline whisper hangs over a hidden lab, and a boxed caption narrates between scenes. Each balloon floats on its own gradient panel dusted with the shared Ben-Day halftone texture, ink-thick borders, and offset drop shadows, with bold SFX lettering anchoring the action.
The control bar drives everything from one place. A segmented radiogroup picks the active style — focusing and pulsing the matching balloon so it is easy to spot — while the lettering field retypes that balloon’s words on demand, automatically upper-casing the shout. Clicking or pressing Enter on any balloon cycles its tail through left, right, and down, re-drawing the tail with clip-path so it always points from a believable direction.
Everything is keyboard-usable: balloons are focusable buttons, the style segments support arrow-key navigation, and a small toast() helper announces every change through an aria-live region. The layout holds its hierarchy down to roughly 360px, collapsing the panel grid to a single column and stacking the controls, and it honours prefers-reduced-motion.
Illustrative UI only — fictional series, characters, and data.