Comics — Halftone / Ben-Day Dot Backgrounds
A comic-book halftone toolkit built entirely from CSS radial-gradients. Six live swatches show flat Ben-Day dots, a density gradient ramp for shading, a radial impact burst, diagonal speed-lines, and a two-color duotone screen, all framed in thick ink borders on halftone paper. A large preview panel carries a speech balloon and bold SFX while range sliders for dot size, spacing, and angle plus dot and paper color pickers update CSS variables in real time, and a copy CSS button drops the generated background-image rule onto the clipboard with a toast.
MCP
Code
: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: 5px 5px 0 var(--ink);
/* live workbench vars (driven by JS) */
--dot: #0e0e12;
--bg: #ffd23f;
--dot-size: 5px;
--gap: 14px;
--angle: 0deg;
}
*,
*::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);
background-size: 22px 22px;
}
.skip-link {
position: absolute;
left: -999px;
top: 8px;
background: var(--ink);
color: var(--paper);
padding: 8px 14px;
border-radius: var(--r-sm);
z-index: 50;
font-weight: 700;
}
.skip-link:focus {
left: 8px;
}
/* ============ MASTHEAD ============ */
.masthead {
max-width: 1080px;
margin: 28px auto 0;
padding: 22px 26px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
background: var(--accent-2);
border: 3px solid var(--ink);
border-radius: var(--r-lg);
box-shadow: var(--shadow);
}
.masthead__kicker {
margin: 0 0 2px;
font-weight: 800;
font-size: 0.72rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ink-2);
}
.masthead__title {
margin: 0;
font-family: "Bangers", system-ui, cursive;
font-weight: 400;
font-size: clamp(2.4rem, 7vw, 4.2rem);
line-height: 0.92;
letter-spacing: 0.02em;
color: var(--ink);
text-shadow: 3px 3px 0 var(--accent);
}
.masthead__sub {
margin: 6px 0 0;
max-width: 40ch;
font-weight: 600;
color: var(--ink-2);
}
.masthead__sfx {
flex: none;
font-family: "Bangers", system-ui, cursive;
font-size: clamp(2rem, 6vw, 3.6rem);
color: var(--paper);
background: var(--accent);
border: 3px solid var(--ink);
border-radius: var(--r-md);
padding: 6px 18px;
transform: rotate(-6deg);
box-shadow: var(--shadow);
}
/* ============ LAYOUT ============ */
main {
max-width: 1080px;
margin: 0 auto;
padding: 26px 18px 48px;
display: grid;
gap: 30px;
}
/* ============ WORKBENCH ============ */
.workbench {
display: grid;
grid-template-columns: 1.15fr 1fr;
gap: 22px;
}
.preview {
position: relative;
min-height: 340px;
border: 3px solid var(--ink);
border-radius: var(--r-lg);
box-shadow: var(--shadow);
overflow: hidden;
background-color: var(--bg);
background-image: radial-gradient(
circle at center,
var(--dot) calc(var(--dot-size) / 2),
transparent calc(var(--dot-size) / 2 + 0.5px)
);
background-size: var(--gap) var(--gap);
background-position: 0 0;
transform: rotate(0deg);
}
/* angle handled via JS gradient string; this is a fallback overlay */
.preview::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(transparent, rgba(255, 255, 255, 0.06));
pointer-events: none;
}
.preview__balloon {
position: absolute;
left: 26px;
top: 26px;
max-width: 62%;
background: var(--panel);
border: 3px solid var(--ink);
border-radius: var(--r-lg);
padding: 14px 18px;
box-shadow: 4px 4px 0 rgba(14, 14, 18, 0.5);
}
.preview__balloon::after {
content: "";
position: absolute;
left: 34px;
bottom: -18px;
width: 26px;
height: 22px;
background: var(--panel);
border-right: 3px solid var(--ink);
border-bottom: 3px solid var(--ink);
transform: skewX(-18deg);
}
.preview__line1 {
margin: 0;
font-family: "Bangers", system-ui, cursive;
font-size: 1.7rem;
letter-spacing: 0.03em;
color: var(--ink);
}
.preview__line2 {
margin: 2px 0 0;
font-weight: 600;
font-size: 0.85rem;
color: var(--muted);
}
.preview__sfx {
position: absolute;
right: 18px;
bottom: 16px;
font-family: "Bangers", system-ui, cursive;
font-size: clamp(2.4rem, 7vw, 4rem);
color: var(--accent-2);
-webkit-text-stroke: 2.5px var(--ink);
paint-order: stroke fill;
transform: rotate(7deg);
filter: drop-shadow(3px 3px 0 var(--ink));
}
/* ----- controls ----- */
.controls {
display: grid;
gap: 16px;
align-content: start;
background: var(--panel);
border: 3px solid var(--ink);
border-radius: var(--r-lg);
box-shadow: var(--shadow);
padding: 18px;
}
.controls__row {
display: grid;
gap: 14px;
}
.controls__row--colors {
grid-template-columns: auto auto 1fr;
align-items: center;
gap: 14px;
}
.control {
display: grid;
gap: 6px;
}
.control__label {
display: flex;
justify-content: space-between;
font-weight: 700;
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.control__label b {
font-family: "Bangers", system-ui, cursive;
font-weight: 400;
font-size: 1.05rem;
letter-spacing: 0.02em;
color: var(--accent);
}
input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 12px;
background: var(--accent-2);
border: 2.5px solid var(--ink);
border-radius: 999px;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--accent);
border: 2.5px solid var(--ink);
box-shadow: 2px 2px 0 var(--ink);
}
input[type="range"]::-moz-range-thumb {
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--accent);
border: 2.5px solid var(--ink);
box-shadow: 2px 2px 0 var(--ink);
}
input[type="range"]:focus-visible {
outline: 3px solid var(--accent-blue);
outline-offset: 3px;
}
.swatch-pick {
display: grid;
gap: 4px;
font-weight: 700;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.swatch-pick input[type="color"] {
width: 56px;
height: 38px;
padding: 0;
border: 2.5px solid var(--ink);
border-radius: var(--r-sm);
background: none;
cursor: pointer;
}
.presets {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-self: end;
}
.chip {
font: inherit;
font-weight: 700;
font-size: 0.78rem;
padding: 7px 12px;
background: var(--paper);
border: 2.5px solid var(--ink);
border-radius: 999px;
cursor: pointer;
transition: transform 0.08s ease, background 0.12s ease, box-shadow 0.08s ease;
}
.chip:hover {
background: var(--accent-2);
transform: translate(-1px, -1px);
box-shadow: 2px 2px 0 var(--ink);
}
.chip:active {
transform: translate(1px, 1px);
box-shadow: none;
}
.controls__actions {
display: flex;
align-items: stretch;
gap: 12px;
flex-wrap: wrap;
}
.css-out {
flex: 1 1 220px;
min-width: 0;
font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
font-size: 0.74rem;
line-height: 1.4;
color: var(--ink-2);
background: var(--paper);
border: 2.5px solid var(--line-2);
border-radius: var(--r-sm);
padding: 10px 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.btn {
font: inherit;
font-weight: 800;
letter-spacing: 0.04em;
text-transform: uppercase;
padding: 10px 20px;
border: 3px solid var(--ink);
border-radius: var(--r-md);
cursor: pointer;
transition: transform 0.08s ease, box-shadow 0.08s ease;
}
.btn--primary {
background: var(--accent);
color: var(--paper);
box-shadow: var(--shadow);
}
.btn--primary:hover {
transform: translate(-2px, -2px);
box-shadow: 7px 7px 0 var(--ink);
}
.btn--primary:active {
transform: translate(2px, 2px);
box-shadow: 2px 2px 0 var(--ink);
}
.btn:focus-visible {
outline: 3px solid var(--accent-blue);
outline-offset: 3px;
}
/* ============ GALLERY ============ */
.gallery__title,
.footer p {
margin: 0;
}
.gallery__title {
font-family: "Bangers", system-ui, cursive;
font-weight: 400;
font-size: clamp(1.8rem, 4vw, 2.6rem);
letter-spacing: 0.03em;
margin-bottom: 16px;
text-shadow: 2px 2px 0 var(--accent-2);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 18px;
}
.card {
background: var(--panel);
border: 3px solid var(--ink);
border-radius: var(--r-lg);
box-shadow: var(--shadow);
overflow: hidden;
transition: transform 0.1s ease, box-shadow 0.1s ease;
}
.card:hover,
.card:focus-visible {
transform: translate(-3px, -3px);
box-shadow: 8px 8px 0 var(--ink);
outline: none;
}
.card__art {
height: 140px;
border-bottom: 3px solid var(--ink);
background-color: var(--paper);
}
.card__meta {
padding: 12px 14px 16px;
}
.card__meta h3 {
margin: 0 0 4px;
font-family: "Bangers", system-ui, cursive;
font-weight: 400;
font-size: 1.25rem;
letter-spacing: 0.02em;
}
.card__meta p {
margin: 0;
font-size: 0.84rem;
color: var(--muted);
}
/* ----- swatch fills ----- */
.tone-flat {
background-color: var(--accent-2);
background-image: radial-gradient(circle, var(--ink) 3px, transparent 3.6px);
background-size: 14px 14px;
}
.tone-gradient {
background-color: var(--paper);
background-image: radial-gradient(circle, var(--ink) 3.5px, transparent 4px),
linear-gradient(to bottom, rgba(253, 252, 247, 0) 30%, var(--paper) 100%);
background-size: 14px 14px, 100% 100%;
}
.tone-burst {
background-color: var(--accent);
background-image: radial-gradient(circle at center, var(--paper) 30%, transparent 30%),
repeating-radial-gradient(
circle at center,
var(--ink) 0 2px,
transparent 2px 11px
);
}
.tone-speed {
background-color: var(--accent-blue);
background-image: repeating-linear-gradient(
66deg,
var(--paper) 0 3px,
transparent 3px 13px
);
}
.tone-duo {
background-color: var(--accent-blue);
background-image: radial-gradient(circle, var(--accent) 4px, transparent 4.6px);
background-size: 13px 13px;
}
.tone-live {
background-color: var(--bg);
background-image: radial-gradient(
circle at center,
var(--dot) calc(var(--dot-size) / 2),
transparent calc(var(--dot-size) / 2 + 0.5px)
);
background-size: var(--gap) var(--gap);
}
/* ============ FOOTER ============ */
.footer {
max-width: 1080px;
margin: 0 auto;
padding: 0 18px 40px;
text-align: center;
font-size: 0.8rem;
color: var(--muted);
font-weight: 600;
}
/* ============ TOAST ============ */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 140%);
background: var(--ink);
color: var(--paper);
font-weight: 700;
padding: 12px 22px;
border-radius: 999px;
border: 2.5px solid var(--accent-2);
box-shadow: var(--shadow);
opacity: 0;
transition: transform 0.32s cubic-bezier(0.2, 0.9, 0.3, 1.3), opacity 0.2s ease;
z-index: 40;
pointer-events: none;
}
.toast.is-on {
transform: translate(-50%, 0);
opacity: 1;
}
/* ============ RESPONSIVE ============ */
@media (max-width: 880px) {
.workbench {
grid-template-columns: 1fr;
}
}
@media (max-width: 520px) {
.masthead {
flex-direction: column;
align-items: flex-start;
margin-top: 16px;
}
.masthead__sfx {
align-self: flex-end;
}
.controls__row--colors {
grid-template-columns: 1fr 1fr;
}
.presets {
grid-column: 1 / -1;
justify-self: start;
}
.controls__actions {
flex-direction: column;
}
.btn--primary {
width: 100%;
}
main {
padding: 18px 14px 40px;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
transition: none !important;
}
}(function () {
"use strict";
var root = document.documentElement;
var els = {
dotSize: document.getElementById("dotSize"),
spacing: document.getElementById("spacing"),
angle: document.getElementById("angle"),
dotColor: document.getElementById("dotColor"),
paperColor: document.getElementById("paperColor"),
dotSizeOut: document.getElementById("dotSizeOut"),
spacingOut: document.getElementById("spacingOut"),
angleOut: document.getElementById("angleOut"),
cssOut: document.getElementById("cssOut"),
copyBtn: document.getElementById("copyBtn"),
preview: document.getElementById("preview"),
};
/* ----- toast helper ----- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("is-on");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-on");
}, 2000);
}
/* ----- build the CSS the dots are made of ----- */
function buildCss() {
var dot = els.dotColor.value;
var paper = els.paperColor.value;
var size = parseFloat(els.dotSize.value);
var gap = parseFloat(els.spacing.value);
var angle = parseInt(els.angle.value, 10);
// Angle is expressed by rotating the gradient's positional grid.
// For a true axis-aligned dot grid we keep gradient radial and rotate the
// whole layer via background-position trickery is limited, so we expose the
// angle as a CSS custom prop used by a transform on the preview layer.
var radius = (size / 2).toFixed(2);
var image =
"radial-gradient(circle at center, " +
dot +
" " +
radius +
"px, transparent " +
(parseFloat(radius) + 0.5).toFixed(2) +
"px)";
var css =
"background-color: " +
paper +
";\n" +
"background-image: " +
image +
";\n" +
"background-size: " +
gap +
"px " +
gap +
"px;\n" +
"background-position: 0 0;\n" +
"transform: rotate(" +
angle +
"deg);";
return css;
}
/* ----- push current state into CSS vars + readouts ----- */
function apply() {
var size = parseFloat(els.dotSize.value);
var gap = parseFloat(els.spacing.value);
var angle = parseInt(els.angle.value, 10);
root.style.setProperty("--dot", els.dotColor.value);
root.style.setProperty("--bg", els.paperColor.value);
root.style.setProperty("--dot-size", size + "px");
root.style.setProperty("--gap", gap + "px");
root.style.setProperty("--angle", angle + "deg");
// rotate the live preview's dot layer
if (els.preview) {
els.preview.style.transform = "rotate(" + angle + "deg)";
}
els.dotSizeOut.textContent = size + "px";
els.spacingOut.textContent = gap + "px";
els.angleOut.textContent = angle + "°";
els.cssOut.textContent = "background-image: " + buildCss().split("\n")[1].replace("background-image: ", "");
}
/* ----- presets ----- */
var presets = {
flat: { dotSize: 5, spacing: 14, angle: 0, dot: "#0e0e12", paper: "#ffd23f" },
fine: { dotSize: 2.5, spacing: 8, angle: 0, dot: "#0e0e12", paper: "#fdfcf7" },
bold: { dotSize: 9, spacing: 22, angle: 45, dot: "#ff2e4d", paper: "#2e6bff" },
};
function applyPreset(name) {
var p = presets[name];
if (!p) return;
els.dotSize.value = p.dotSize;
els.spacing.value = p.spacing;
els.angle.value = p.angle;
els.dotColor.value = p.dot;
els.paperColor.value = p.paper;
apply();
toast("Preset: " + name + " applied");
}
/* ----- copy CSS ----- */
function copyCss() {
var css = buildCss();
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(css).then(
function () {
toast("CSS copied to clipboard");
},
function () {
fallbackCopy(css);
}
);
} else {
fallbackCopy(css);
}
}
function fallbackCopy(text) {
var ta = document.createElement("textarea");
ta.value = text;
ta.setAttribute("readonly", "");
ta.style.position = "absolute";
ta.style.left = "-9999px";
document.body.appendChild(ta);
ta.select();
try {
document.execCommand("copy");
toast("CSS copied to clipboard");
} catch (e) {
toast("Copy failed — select manually");
}
document.body.removeChild(ta);
}
/* ----- wire up ----- */
["dotSize", "spacing", "angle", "dotColor", "paperColor"].forEach(function (id) {
els[id].addEventListener("input", apply);
});
document.querySelectorAll(".chip[data-preset]").forEach(function (chip) {
chip.addEventListener("click", function () {
applyPreset(chip.getAttribute("data-preset"));
});
});
els.copyBtn.addEventListener("click", copyCss);
// keyboard activation for gallery cards
document.querySelectorAll(".card").forEach(function (card) {
card.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
var name = card.querySelector("h3");
toast((name ? name.textContent : "Swatch") + " — copy its idea from the docs");
}
});
});
apply();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Halftone / Ben-Day Dot Backgrounds — Stealthis Comics Kit</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="#workbench">Skip to dot workbench</a>
<header class="masthead" role="banner">
<div class="masthead__plate">
<p class="masthead__kicker">Stealthis Comics Kit · Issue #07</p>
<h1 class="masthead__title">Halftone Foundry</h1>
<p class="masthead__sub">
Ben-Day dots, screentone bursts & speed-lines — tuned live, copied as CSS.
</p>
</div>
<div class="masthead__sfx" aria-hidden="true">DOT!</div>
</header>
<main>
<!-- ============ WORKBENCH ============ -->
<section id="workbench" class="workbench" aria-label="Live halftone workbench">
<div class="preview" id="preview">
<div class="preview__balloon">
<p class="preview__line1">NEON RONIN</p>
<p class="preview__line2">issue #07 — “Static City”</p>
</div>
<span class="preview__sfx" aria-hidden="true">POW</span>
</div>
<form class="controls" aria-label="Halftone controls">
<div class="controls__row">
<label class="control" for="dotSize">
<span class="control__label">Dot size <b id="dotSizeOut">5px</b></span>
<input id="dotSize" type="range" min="1" max="14" step="0.5" value="5" />
</label>
<label class="control" for="spacing">
<span class="control__label">Spacing <b id="spacingOut">14px</b></span>
<input id="spacing" type="range" min="6" max="40" step="1" value="14" />
</label>
<label class="control" for="angle">
<span class="control__label">Angle <b id="angleOut">0°</b></span>
<input id="angle" type="range" min="0" max="90" step="1" value="0" />
</label>
</div>
<div class="controls__row controls__row--colors">
<label class="swatch-pick" for="dotColor">
<span>Dot ink</span>
<input id="dotColor" type="color" value="#0e0e12" />
</label>
<label class="swatch-pick" for="paperColor">
<span>Paper</span>
<input id="paperColor" type="color" value="#ffd23f" />
</label>
<div class="presets" role="group" aria-label="Quick presets">
<button type="button" class="chip" data-preset="flat">Flat dots</button>
<button type="button" class="chip" data-preset="fine">Fine screen</button>
<button type="button" class="chip" data-preset="bold">Bold pulp</button>
</div>
</div>
<div class="controls__actions">
<code class="css-out" id="cssOut" aria-live="polite">background-image: …</code>
<button type="button" class="btn btn--primary" id="copyBtn">Copy CSS</button>
</div>
</form>
</section>
<!-- ============ SWATCH GALLERY ============ -->
<section class="gallery" aria-label="Halftone pattern swatches">
<h2 class="gallery__title">Pattern Swatches</h2>
<div class="grid">
<article class="card" tabindex="0">
<div class="card__art tone-flat" aria-hidden="true"></div>
<div class="card__meta">
<h3>Flat Ben-Day</h3>
<p>Even dot grid — the classic pulp fill.</p>
</div>
</article>
<article class="card" tabindex="0">
<div class="card__art tone-gradient" aria-hidden="true"></div>
<div class="card__meta">
<h3>Gradient Ramp</h3>
<p>Dot density fades top-to-bottom for shading.</p>
</div>
</article>
<article class="card" tabindex="0">
<div class="card__art tone-burst" aria-hidden="true"></div>
<div class="card__meta">
<h3>Radial Burst</h3>
<p>Dots explode from center — impact panels.</p>
</div>
</article>
<article class="card" tabindex="0">
<div class="card__art tone-speed" aria-hidden="true"></div>
<div class="card__meta">
<h3>Speed Lines</h3>
<p>Diagonal motion streaks behind the action.</p>
</div>
</article>
<article class="card" tabindex="0">
<div class="card__art tone-duo" aria-hidden="true"></div>
<div class="card__meta">
<h3>Duotone</h3>
<p>Two-color screen — accent over accent-blue.</p>
</div>
</article>
<article class="card" tabindex="0">
<div class="card__art tone-live" id="liveSwatch" aria-hidden="true"></div>
<div class="card__meta">
<h3>Your Mix</h3>
<p>Mirrors the workbench controls above.</p>
</div>
</article>
</div>
</section>
</main>
<footer class="footer">
<p>Halftone Foundry — vanilla CSS radial-gradients. Fictional series for demo only.</p>
</footer>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Halftone / Ben-Day Dot Backgrounds
A self-contained halftone foundry for comic-style interfaces. The workbench pairs a large preview panel — filled with a live Ben-Day dot field, a tailed speech balloon reading Neon Ronin #07 and an oversized POW SFX — with a control deck. Range sliders for dot size, spacing, and angle write straight to CSS custom properties, so the dot grid reflows the instant you drag, and color pickers swap the dot ink and paper hues independently.
Below the bench, a swatch gallery showcases six reusable patterns assembled purely from CSS radial-gradient and repeating-linear-gradient: a flat Ben-Day grid, a density gradient ramp for shading, a radial impact burst, diagonal speed-lines, a two-color duotone screen, and a “Your Mix” tile that mirrors the workbench live. Each card sits in a thick ink border with a hard drop-shadow and lifts on hover and keyboard focus.
The Copy CSS button serializes the current dot field into a ready-to-paste background-image rule and copies it to the clipboard (with a navigator.clipboard path and a document.execCommand fallback), confirming with a small toast(). Quick presets — flat dots, a fine screen, and a bold pulp mix — reset every control at once. No frameworks, no build step: just Bangers and Inter over vanilla CSS and JS.
Illustrative UI only — fictional series, characters, and data.