Comics — Sound-Effect (SFX) Lettering
Bold comic-book sound-effect lettering set in Bangers with hard ink strokes, layered 3D drop-shadows, skew, and accent fills, each word sitting over a spinning halftone burst. A grid of preset blasts — BOOM, POW, ZAP, CRASH, WHOOSH, KA-POW — replays its scale-rotate-settle pop-in on click, while a maker stage renders any typed word in the same SFX style with a randomly picked burst and fill color. Vanilla JS only, with a shuffle control, toast feedback, keyboard support, and a reduced-motion fallback.
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: 6px 6px 0 var(--ink);
--shadow-sm: 3px 3px 0 var(--ink);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: "Inter", system-ui, sans-serif;
font-weight: 400;
line-height: 1.5;
color: var(--ink);
background-color: var(--paper);
background-image: var(--halftone);
background-size: 6px 6px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.skip-link {
position: absolute;
left: -999px;
top: 0;
z-index: 50;
background: var(--ink);
color: var(--paper);
padding: 10px 16px;
font-weight: 700;
border-radius: 0 0 var(--r-sm) 0;
}
.skip-link:focus {
left: 0;
}
/* ---------- Masthead ---------- */
.masthead {
border-bottom: 3px solid var(--ink);
background: var(--accent-2);
background-image: var(--halftone);
background-size: 6px 6px;
}
.masthead__inner {
max-width: 1040px;
margin: 0 auto;
padding: 22px 20px 26px;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 14px 24px;
}
.brand {
display: flex;
align-items: center;
gap: 14px;
}
.brand__badge {
display: grid;
place-items: center;
width: 52px;
height: 52px;
font-size: 26px;
color: var(--accent-2);
background: var(--ink);
border: 3px solid var(--ink);
border-radius: 50%;
box-shadow: var(--shadow-sm);
transform: rotate(-8deg);
}
.brand__kicker {
margin: 0;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--ink-2);
}
.brand__title {
margin: 0;
font-family: "Bangers", cursive;
font-weight: 400;
font-size: clamp(2rem, 6vw, 3.2rem);
line-height: 0.95;
letter-spacing: 0.02em;
-webkit-text-stroke: 1px var(--ink);
color: var(--panel);
text-shadow: 3px 3px 0 var(--ink);
}
.masthead__tag {
margin: 0;
max-width: 320px;
font-weight: 500;
font-size: 0.95rem;
color: var(--ink-2);
padding: 8px 14px;
background: var(--panel);
border: 2px solid var(--ink);
border-radius: var(--r-md);
box-shadow: var(--shadow-sm);
}
.masthead__tag b {
color: var(--accent);
}
/* ---------- Sheet / sections ---------- */
.sheet {
max-width: 1040px;
margin: 0 auto;
padding: 32px 20px 8px;
display: grid;
gap: 40px;
}
.section-head {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: space-between;
gap: 6px 16px;
margin-bottom: 18px;
padding-bottom: 8px;
border-bottom: 3px solid var(--ink);
}
.section-head__title {
margin: 0;
font-family: "Bangers", cursive;
font-weight: 400;
font-size: clamp(1.6rem, 4vw, 2.4rem);
letter-spacing: 0.03em;
color: var(--ink);
}
.section-head__hint {
margin: 0;
font-size: 0.85rem;
font-weight: 600;
color: var(--muted);
}
/* ---------- Gallery grid ---------- */
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 18px;
}
.panel {
position: relative;
display: grid;
place-items: center;
min-height: 168px;
padding: 18px;
font: inherit;
text-align: center;
cursor: pointer;
background: var(--panel);
background-image: var(--halftone);
background-size: 6px 6px;
border: 3px solid var(--ink);
border-radius: var(--r-md);
box-shadow: var(--shadow);
overflow: hidden;
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.panel:hover {
transform: translate(-2px, -2px);
box-shadow: 9px 9px 0 var(--ink);
}
.panel:active {
transform: translate(2px, 2px);
box-shadow: 2px 2px 0 var(--ink);
}
.panel:focus-visible {
outline: 4px solid var(--accent-blue);
outline-offset: 3px;
}
.panel__caption {
position: absolute;
left: 10px;
bottom: 10px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--ink);
background: var(--paper);
border: 2px solid var(--ink);
border-radius: var(--r-sm);
padding: 2px 7px;
}
/* radial burst behind the lettering */
.burst {
position: absolute;
inset: 0;
margin: auto;
width: 150%;
aspect-ratio: 1;
background:
repeating-conic-gradient(
var(--burst, var(--accent-2)) 0deg 11deg,
transparent 11deg 22deg
);
-webkit-mask: radial-gradient(circle, #000 38%, transparent 70%);
mask: radial-gradient(circle, #000 38%, transparent 70%);
opacity: 0.85;
z-index: 0;
pointer-events: none;
animation: spin 28s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* ---------- The SFX lettering itself ---------- */
.sfx {
position: relative;
z-index: 1;
display: inline-block;
font-family: "Bangers", cursive;
font-weight: 400;
font-size: clamp(2.4rem, 7vw, 3.6rem);
line-height: 0.9;
letter-spacing: 0.02em;
color: var(--fill, var(--accent));
-webkit-text-stroke: 2px var(--ink);
transform: rotate(-6deg) skewX(-6deg);
text-shadow:
2px 2px 0 var(--ink),
4px 4px 0 var(--ink),
6px 6px 0 rgba(14, 14, 18, 0.55);
will-change: transform;
}
/* per-preset fills + burst colors */
.panel--boom {
--fill: var(--accent);
--burst: var(--accent-2);
}
.panel--pow {
--fill: var(--accent-2);
--burst: var(--accent);
}
.panel--zap {
--fill: var(--accent-blue);
--burst: var(--accent-2);
}
.panel--crash {
--fill: var(--accent-2);
--burst: var(--ink-2);
}
.panel--whoosh {
--fill: var(--paper);
--burst: var(--accent-blue);
}
.panel--kapow {
--fill: var(--accent);
--burst: var(--accent-blue);
}
/* pop-in keyframes — scale + rotate + settle */
@keyframes sfx-pop {
0% {
transform: scale(0.2) rotate(-26deg) skewX(-6deg);
opacity: 0;
}
55% {
transform: scale(1.22) rotate(5deg) skewX(-6deg);
opacity: 1;
}
75% {
transform: scale(0.94) rotate(-9deg) skewX(-6deg);
}
100% {
transform: scale(1) rotate(-6deg) skewX(-6deg);
opacity: 1;
}
}
.is-popping {
animation: sfx-pop 0.55s cubic-bezier(0.22, 1.2, 0.36, 1) both;
}
/* ---------- Workshop / maker ---------- */
.workshop__stage {
position: relative;
display: grid;
place-items: center;
min-height: 220px;
margin-bottom: 22px;
padding: 24px;
background: var(--ink);
background-image: radial-gradient(
circle,
rgba(255, 255, 255, 0.12) 1px,
transparent 1.6px
);
background-size: 7px 7px;
border: 3px solid var(--ink);
border-radius: var(--r-lg);
box-shadow: var(--shadow);
overflow: hidden;
}
.burst--stage {
width: 120%;
opacity: 0.5;
}
.sfx--stage {
font-size: clamp(2.6rem, 11vw, 5.2rem);
--fill: var(--accent-2);
-webkit-text-stroke: 3px var(--ink);
text-shadow:
3px 3px 0 var(--ink),
6px 6px 0 var(--ink),
9px 9px 0 rgba(0, 0, 0, 0.6);
word-break: break-word;
}
.maker {
display: grid;
gap: 8px;
padding: 18px;
background: var(--panel);
border: 3px solid var(--ink);
border-radius: var(--r-md);
box-shadow: var(--shadow-sm);
}
.maker__label {
font-size: 12px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--ink-2);
}
.maker__row {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.maker__input {
flex: 1 1 200px;
min-width: 0;
font: inherit;
font-weight: 600;
font-size: 1.05rem;
padding: 11px 14px;
color: var(--ink);
background: var(--paper);
border: 2px solid var(--ink);
border-radius: var(--r-sm);
}
.maker__input::placeholder {
color: var(--muted);
font-weight: 500;
}
.maker__input:focus-visible {
outline: 3px solid var(--accent-blue);
outline-offset: 2px;
}
.maker__help {
margin: 0;
font-size: 0.82rem;
color: var(--muted);
}
.btn {
font: inherit;
font-weight: 700;
font-size: 0.95rem;
letter-spacing: 0.02em;
padding: 11px 18px;
cursor: pointer;
color: var(--ink);
border: 2px solid var(--ink);
border-radius: var(--r-sm);
box-shadow: var(--shadow-sm);
transition: transform 0.1s ease, box-shadow 0.1s ease, background 0.1s ease;
}
.btn:hover {
transform: translate(-1px, -1px);
box-shadow: 4px 4px 0 var(--ink);
}
.btn:active {
transform: translate(2px, 2px);
box-shadow: 1px 1px 0 var(--ink);
}
.btn:focus-visible {
outline: 3px solid var(--accent-blue);
outline-offset: 2px;
}
.btn--make {
background: var(--accent);
color: var(--paper);
}
.btn--ghost {
background: var(--panel);
}
/* ---------- Footer / disclaimer ---------- */
.footsig {
max-width: 1040px;
margin: 0 auto;
padding: 28px 20px 40px;
display: flex;
align-items: center;
gap: 10px;
font-size: 0.82rem;
font-weight: 600;
color: var(--muted);
}
.footsig__dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--accent);
border: 2px solid var(--ink);
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translate(-50%, 24px);
max-width: calc(100% - 32px);
padding: 12px 18px;
font-weight: 700;
font-size: 0.92rem;
color: var(--paper);
background: var(--ink);
border: 2px solid var(--ink);
border-radius: var(--r-md);
box-shadow: var(--shadow-sm);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
z-index: 60;
}
.toast::before {
content: "✦ ";
color: var(--accent-2);
}
.toast.is-visible {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------- Responsive ---------- */
@media (max-width: 760px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 520px) {
.masthead__inner {
padding: 18px 16px 22px;
}
.masthead__tag {
max-width: none;
}
.sheet {
padding: 24px 16px 4px;
gap: 32px;
}
.grid {
grid-template-columns: 1fr;
gap: 14px;
}
.panel {
min-height: 140px;
}
.maker__row {
flex-direction: column;
align-items: stretch;
}
.btn {
width: 100%;
}
.workshop__stage {
min-height: 180px;
}
}
@media (prefers-reduced-motion: reduce) {
.burst {
animation: none;
}
.is-popping {
animation-duration: 0.01ms;
}
.panel,
.btn,
.toast {
transition: none;
}
}(function () {
"use strict";
/* ---------- Toast helper ---------- */
var toastEl = document.getElementById("toast");
var toastTimer = null;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("is-visible");
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-visible");
}, 1900);
}
/* ---------- Replay the pop-in animation on an element ---------- */
function pop(el) {
if (!el) return;
el.classList.remove("is-popping");
// force reflow so the animation restarts even on rapid clicks
void el.offsetWidth;
el.classList.add("is-popping");
}
// Clean up the class once the animation ends (lets :hover transforms work again)
document.addEventListener(
"animationend",
function (e) {
if (e.animationName === "sfx-pop") {
e.target.classList.remove("is-popping");
}
},
true
);
/* ---------- Preset panels: click to re-pop ---------- */
var panels = document.querySelectorAll("[data-sfx]");
panels.forEach(function (panel) {
panel.addEventListener("click", function () {
var word = panel.querySelector("[data-word]");
pop(word);
var label = word ? word.textContent.trim() : "SFX";
toast(label + " — re-lettered!");
});
});
/* ---------- SFX maker ---------- */
var burstColors = [
"#ff2e4d", // accent
"#ffd23f", // accent-2
"#2e6bff", // accent-blue
"#23232b", // ink-2
"#11b886", // emerald
"#ff7a18", // orange
"#8e44ff", // violet
];
var fillColors = ["#ff2e4d", "#ffd23f", "#2e6bff", "#fdfcf7", "#ff7a18"];
var stage = document.getElementById("stage");
var stageWord = document.getElementById("stage-word");
var stageBurst = stage ? stage.querySelector(".burst") : null;
var form = document.getElementById("maker");
var input = document.getElementById("sfx-input");
var shuffleBtn = document.getElementById("shuffle");
function pick(arr, avoid) {
var c;
do {
c = arr[Math.floor(Math.random() * arr.length)];
} while (avoid && c === avoid && arr.length > 1);
return c;
}
var lastFill = null;
function randomizeColors() {
var fill = pick(fillColors, lastFill);
lastFill = fill;
var burst = pick(burstColors);
if (stageWord) stageWord.style.setProperty("--fill", fill);
if (stageBurst) stageBurst.style.setProperty("--burst", burst);
}
function renderSfx(raw) {
var word = (raw || "").trim();
if (!word) {
toast("Type a word first!");
if (input) input.focus();
return false;
}
word = word.slice(0, 14).toUpperCase();
if (stageWord) {
stageWord.textContent = word;
randomizeColors();
pop(stageWord);
}
return true;
}
if (form) {
form.addEventListener("submit", function (e) {
e.preventDefault();
if (renderSfx(input ? input.value : "")) {
toast("Lettered “" + stageWord.textContent + "”");
}
});
}
if (shuffleBtn) {
shuffleBtn.addEventListener("click", function () {
randomizeColors();
pop(stageWord);
toast("New burst color");
});
}
// Click the stage word itself to replay
if (stageWord) {
stageWord.style.cursor = "pointer";
stageWord.setAttribute("role", "button");
stageWord.setAttribute("tabindex", "0");
stageWord.setAttribute("aria-label", "Replay sound-effect animation");
stageWord.addEventListener("click", function () {
pop(stageWord);
});
stageWord.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
pop(stageWord);
}
});
}
// Initial flourish
randomizeColors();
pop(stageWord);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Comics — Sound-Effect (SFX) Lettering</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="#workshop">Skip to SFX maker</a>
<header class="masthead">
<div class="masthead__inner">
<div class="brand">
<span class="brand__badge" aria-hidden="true">★</span>
<div class="brand__text">
<p class="brand__kicker">Neon Ronin · Iron Vanguard</p>
<h1 class="brand__title">SFX Lettering Lab</h1>
</div>
</div>
<p class="masthead__tag">
Click any word to <b>re-pop</b> it — or letter your own SFX below.
</p>
</div>
</header>
<main class="sheet">
<!-- Gallery of preset SFX -->
<section class="gallery" aria-labelledby="gallery-title">
<div class="section-head">
<h2 id="gallery-title" class="section-head__title">Preset Blasts</h2>
<p class="section-head__hint">Tap a panel to replay the pop-in</p>
</div>
<div class="grid" id="sfx-grid">
<button class="panel panel--boom" type="button" data-sfx>
<span class="burst" aria-hidden="true"></span>
<span class="sfx" data-word>BOOM</span>
<span class="panel__caption">Iron Vanguard #12</span>
</button>
<button class="panel panel--pow" type="button" data-sfx>
<span class="burst" aria-hidden="true"></span>
<span class="sfx" data-word>POW!</span>
<span class="panel__caption">Neon Ronin #04</span>
</button>
<button class="panel panel--zap" type="button" data-sfx>
<span class="burst" aria-hidden="true"></span>
<span class="sfx" data-word>ZAP</span>
<span class="panel__caption">Voltcat: Origins</span>
</button>
<button class="panel panel--crash" type="button" data-sfx>
<span class="burst" aria-hidden="true"></span>
<span class="sfx" data-word>CRASH</span>
<span class="panel__caption">Iron Vanguard #07</span>
</button>
<button class="panel panel--whoosh" type="button" data-sfx>
<span class="burst" aria-hidden="true"></span>
<span class="sfx" data-word>WHOOSH</span>
<span class="panel__caption">Skyline Drift</span>
</button>
<button class="panel panel--kapow" type="button" data-sfx>
<span class="burst" aria-hidden="true"></span>
<span class="sfx" data-word>KA-POW</span>
<span class="panel__caption">Neon Ronin #11</span>
</button>
</div>
</section>
<!-- SFX maker -->
<section class="workshop" id="workshop" aria-labelledby="workshop-title">
<div class="section-head">
<h2 id="workshop-title" class="section-head__title">Make Your Own</h2>
<p class="section-head__hint">Type a word, get a random burst</p>
</div>
<div class="workshop__stage" id="stage">
<span class="burst burst--stage" aria-hidden="true"></span>
<span class="sfx sfx--stage" id="stage-word" data-word>BANG</span>
</div>
<form class="maker" id="maker" novalidate>
<label class="maker__label" for="sfx-input">Your sound effect</label>
<div class="maker__row">
<input
class="maker__input"
id="sfx-input"
name="sfx"
type="text"
maxlength="14"
autocomplete="off"
placeholder="e.g. THWACK"
aria-describedby="maker-help"
/>
<button class="btn btn--make" type="submit">Make SFX</button>
<button class="btn btn--ghost" type="button" id="shuffle">Shuffle color</button>
</div>
<p class="maker__help" id="maker-help">
Up to 14 characters. Letters get the full Bangers treatment.
</p>
</form>
</section>
</main>
<footer class="footsig">
<span class="footsig__dot" aria-hidden="true"></span>
<p>Illustrative UI only — fictional series, characters, and data.</p>
</footer>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Sound-Effect (SFX) Lettering
A comic-panel showcase of animated sound-effect lettering, art-directed in the classic ink-and-halftone tradition. Each word — BOOM, POW, ZAP, CRASH, WHOOSH, KA-POW — is set in Bangers with a thick ink stroke, a stepped 3D drop-shadow, and a skewed rotation, then floated over a spinning Ben-Day burst. The panels carry hard 3px ink borders, halftone fills, and chunky offset shadows, captioned with fictional issue lines from Neon Ronin and Iron Vanguard.
Click any panel to replay its pop-in: the lettering scales up from nothing, overshoots with a rotation, then settles into place on a snappy spring curve. A toast confirms each replay. Below the gallery, a maker stage lets you letter your own word — type up to fourteen characters, hit Make SFX, and it renders in the same style with a freshly randomized burst and fill color. A Shuffle color button re-rolls the palette, and the giant stage word is itself a keyboard-operable button that replays on Enter or Space.
Everything is vanilla HTML, CSS, and JavaScript — no frameworks or build step. Interactions are accessible with visible focus rings, ARIA labels, a skip link, and live-region toasts, and the spinning bursts plus pop-in animations are disabled under prefers-reduced-motion. The layout reflows from a three-column grid down to a single column and stacked controls at narrow widths.
Illustrative UI only — fictional series, characters, and data.