Comics — Chapter / Episode Index
A comic-styled chapter and episode index for webcomics and manga readers. Each row shows the chapter number, halftone thumbnail, title, release date, page count, a free or locked badge, and a read-state dot. The header packs a live search and filter input plus a newest or oldest sort toggle, while a tap marks a chapter read and highlights it with a pop animation, thick ink borders, and Ben-Day dot accents throughout.
MCP
Código
: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: 4px 4px 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;
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;
padding: clamp(16px, 4vw, 40px);
}
.reader {
max-width: 720px;
margin: 0 auto;
}
/* ---------- Series header ---------- */
.series {
position: relative;
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
background: var(--panel);
border: 3px solid var(--ink);
border-radius: var(--r-lg);
box-shadow: var(--shadow);
overflow: hidden;
}
.series::after {
content: "";
position: absolute;
inset: 0;
background-image: var(--halftone);
background-size: 6px 6px;
opacity: 0.35;
pointer-events: none;
-webkit-mask-image: linear-gradient(115deg, #000 0 30%, transparent 55%);
mask-image: linear-gradient(115deg, #000 0 30%, transparent 55%);
}
.series__badge {
flex: none;
width: 58px;
height: 58px;
display: grid;
place-items: center;
font-family: "Bangers", cursive;
font-size: 26px;
letter-spacing: 1px;
color: var(--paper);
background: var(--accent);
border: 3px solid var(--ink);
border-radius: var(--r-md);
box-shadow: var(--shadow-sm);
transform: rotate(-4deg);
z-index: 1;
}
.series__meta {
z-index: 1;
min-width: 0;
}
.series__kicker {
margin: 0;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--accent-blue);
}
.series__title {
margin: 2px 0 4px;
font-family: "Bangers", cursive;
font-size: clamp(34px, 8vw, 48px);
font-weight: 400;
letter-spacing: 1px;
line-height: 0.95;
}
.series__sub {
margin: 0;
font-size: 13px;
font-weight: 600;
color: var(--muted);
}
.series__sfx {
margin-left: auto;
align-self: flex-start;
font-family: "Bangers", cursive;
font-size: 24px;
color: var(--accent-2);
-webkit-text-stroke: 2px var(--ink);
paint-order: stroke fill;
transform: rotate(7deg);
z-index: 1;
white-space: nowrap;
}
/* ---------- Toolbar ---------- */
.toolbar {
display: flex;
gap: 12px;
margin: 18px 0;
}
.search {
position: relative;
flex: 1;
min-width: 0;
}
.search__icon {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
font-size: 15px;
pointer-events: none;
opacity: 0.7;
}
.search__input {
width: 100%;
padding: 12px 14px 12px 40px;
font: inherit;
font-weight: 600;
color: var(--ink);
background: var(--panel);
border: 3px solid var(--ink);
border-radius: var(--r-md);
box-shadow: var(--shadow-sm);
}
.search__input::placeholder {
color: var(--muted);
font-weight: 500;
}
.search__input:focus-visible {
outline: none;
border-color: var(--accent-blue);
box-shadow: 3px 3px 0 var(--accent-blue);
}
.sort {
flex: none;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
font: inherit;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--ink);
background: var(--accent-2);
border: 3px solid var(--ink);
border-radius: var(--r-md);
box-shadow: var(--shadow-sm);
cursor: pointer;
transition: transform 0.08s ease, box-shadow 0.08s ease;
}
.sort:hover {
transform: translate(-1px, -1px);
box-shadow: 4px 4px 0 var(--ink);
}
.sort:active {
transform: translate(3px, 3px);
box-shadow: 0 0 0 var(--ink);
}
.sort:focus-visible {
outline: 3px solid var(--accent-blue);
outline-offset: 2px;
}
.sort__arrow {
font-size: 16px;
line-height: 1;
transition: transform 0.2s ease;
}
.sort[aria-pressed="true"] .sort__arrow {
transform: rotate(180deg);
}
/* ---------- Chapter list ---------- */
.chapters {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 12px;
}
.chapter {
display: grid;
grid-template-columns: auto 64px 1fr auto;
align-items: center;
gap: 14px;
width: 100%;
padding: 12px 14px;
text-align: left;
font: inherit;
color: var(--ink);
background: var(--panel);
border: 3px solid var(--ink);
border-radius: var(--r-md);
box-shadow: var(--shadow-sm);
cursor: pointer;
transition: transform 0.08s ease, box-shadow 0.08s ease, background 0.15s ease;
}
.chapter:hover {
transform: translate(-2px, -2px);
box-shadow: 5px 5px 0 var(--ink);
}
.chapter:active {
transform: translate(2px, 2px);
box-shadow: 1px 1px 0 var(--ink);
}
.chapter:focus-visible {
outline: 3px solid var(--accent-blue);
outline-offset: 2px;
}
.chapter.is-read {
background: linear-gradient(var(--paper), var(--paper));
}
.chapter.is-locked {
cursor: not-allowed;
}
.chapter__num {
font-family: "Bangers", cursive;
font-size: 24px;
line-height: 1;
color: var(--accent);
min-width: 30px;
text-align: center;
}
.chapter__thumb {
position: relative;
width: 64px;
height: 64px;
border: 2px solid var(--ink);
border-radius: var(--r-sm);
background-size: 8px 8px;
overflow: hidden;
}
.chapter__thumb::after {
content: "";
position: absolute;
inset: 0;
background-image: var(--halftone);
background-size: 5px 5px;
opacity: 0.4;
}
.chapter__body {
min-width: 0;
}
.chapter__title {
margin: 0 0 3px;
font-size: 15px;
font-weight: 700;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chapter__info {
display: flex;
flex-wrap: wrap;
gap: 4px 10px;
font-size: 12px;
font-weight: 600;
color: var(--muted);
}
.chapter__info time {
color: var(--muted);
}
.chapter__aside {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8px;
}
/* status dot */
.dot {
width: 12px;
height: 12px;
border: 2px solid var(--ink);
border-radius: 50%;
background: transparent;
}
.chapter.is-read .dot {
background: var(--accent-blue);
}
/* badges */
.tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 8px;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
border: 2px solid var(--ink);
border-radius: 999px;
white-space: nowrap;
}
.tag--free {
color: var(--ink);
background: var(--accent-2);
}
.tag--locked {
color: var(--paper);
background: var(--ink-2);
}
.tag--read {
color: var(--paper);
background: var(--accent-blue);
}
/* highlight pulse on mark-read */
@keyframes pop {
0% { transform: scale(1); }
40% { transform: scale(1.03); }
100% { transform: scale(1); }
}
.chapter.just-read {
animation: pop 0.32s ease;
border-color: var(--accent-blue);
box-shadow: 4px 4px 0 var(--accent-blue);
}
/* ---------- Empty state ---------- */
.empty {
margin: 24px 0;
padding: 24px;
text-align: center;
font-weight: 600;
color: var(--muted);
background: var(--panel);
border: 3px dashed var(--line-2);
border-radius: var(--r-md);
}
.empty strong {
color: var(--accent);
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translate(-50%, 140%);
max-width: calc(100% - 32px);
padding: 12px 18px;
font-weight: 700;
color: var(--paper);
background: var(--ink);
border: 3px solid var(--accent-2);
border-radius: var(--r-md);
box-shadow: var(--shadow);
opacity: 0;
pointer-events: none;
transition: transform 0.28s cubic-bezier(0.2, 0.9, 0.3, 1.4), opacity 0.28s ease;
z-index: 50;
}
.toast.is-show {
transform: translate(-50%, 0);
opacity: 1;
}
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
body {
padding: 14px;
}
.series {
flex-wrap: wrap;
gap: 12px;
padding: 16px;
}
.series__sfx {
display: none;
}
.toolbar {
flex-direction: column;
}
.sort {
justify-content: center;
}
.chapter {
grid-template-columns: auto 1fr auto;
gap: 10px;
}
.chapter__thumb {
display: none;
}
.chapter__aside {
gap: 6px;
}
}
@media (prefers-reduced-motion: reduce) {
.chapter,
.sort,
.toast,
.sort__arrow {
transition: none;
}
.chapter.just-read {
animation: none;
}
}/* Neon Ronin — Chapter / Episode Index
Vanilla JS: live filter, sort toggle, click-to-mark-read. */
(function () {
"use strict";
// Fictional chapter data. `read` reflects the reader's progress.
var CHAPTERS = [
{ n: 1, title: "Rain Over Neo-Kanda", date: "2025-09-14", pages: 38, free: true, read: true, hue: 348 },
{ n: 2, title: "The Severed Oath", date: "2025-09-21", pages: 41, free: true, read: true, hue: 214 },
{ n: 3, title: "Ghost in the Vending Machine", date: "2025-09-28", pages: 36, free: true, read: true, hue: 47 },
{ n: 4, title: "Blade & Bandwidth", date: "2025-10-05", pages: 44, free: true, read: false, hue: 280 },
{ n: 5, title: "Mask of the Iron Vanguard", date: "2025-10-12", pages: 39, free: true, read: false, hue: 162 },
{ n: 6, title: "Static Bloom", date: "2025-10-19", pages: 47, free: false, read: false, hue: 16 },
{ n: 7, title: "The Vault Beneath District 9", date: "2025-10-26", pages: 52, free: false, read: false, hue: 200 },
{ n: 8, title: "Two Hundred Volts of Mercy", date: "2025-11-02", pages: 40, free: false, read: false, hue: 320 },
{ n: 9, title: "Where the Drones Sleep", date: "2025-11-09", pages: 45, free: false, read: false, hue: 95 },
{ n: 10, title: "Crimson Handshake", date: "2025-11-16", pages: 49, free: false, read: false, hue: 0 }
];
var listEl = document.getElementById("chapterList");
var inputEl = document.getElementById("filterInput");
var sortEl = document.getElementById("sortToggle");
var emptyEl = document.getElementById("empty");
var emptyTermEl = document.getElementById("emptyTerm");
var toastEl = document.getElementById("toast");
var chapterCountEl = document.getElementById("chapterCount");
var freeCountEl = document.getElementById("freeCount");
var sortNewest = true; // true => newest first (descending chapter #)
var filterTerm = "";
var toastTimer = null;
// ---- helpers ----
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 1900);
}
function fmtDate(iso) {
var d = new Date(iso + "T00:00:00");
return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
}
function thumbBg(hue) {
return (
"linear-gradient(135deg, hsl(" +
hue +
",70%,58%), hsl(" +
((hue + 40) % 360) +
",75%,46%))"
);
}
// ---- rendering ----
function visibleChapters() {
var term = filterTerm.trim().toLowerCase();
var rows = CHAPTERS.filter(function (c) {
if (!term) return true;
return (
c.title.toLowerCase().indexOf(term) !== -1 ||
String(c.n).indexOf(term) !== -1
);
});
rows.sort(function (a, b) {
return sortNewest ? b.n - a.n : a.n - b.n;
});
return rows;
}
function render() {
var rows = visibleChapters();
listEl.innerHTML = "";
rows.forEach(function (c) {
var li = document.createElement("li");
var btn = document.createElement("button");
btn.type = "button";
btn.className = "chapter";
btn.dataset.n = c.n;
if (c.read) btn.classList.add("is-read");
if (!c.free) btn.classList.add("is-locked");
var stateText = c.read ? "read" : "unread";
var lockText = c.free ? "free" : "locked";
btn.setAttribute(
"aria-label",
"Chapter " + c.n + ": " + c.title + ", " + lockText + ", " + stateText
);
btn.innerHTML =
'<span class="chapter__num">#' + c.n + "</span>" +
'<span class="chapter__thumb" style="background:' + thumbBg(c.hue) + '"></span>' +
'<span class="chapter__body">' +
'<span class="chapter__title">' + c.title + "</span>" +
'<span class="chapter__info">' +
"<time datetime=\"" + c.date + "\">" + fmtDate(c.date) + "</time>" +
"<span>· " + c.pages + " pages</span>" +
"</span>" +
"</span>" +
'<span class="chapter__aside">' +
(c.free
? (c.read
? '<span class="tag tag--read">Read</span>'
: '<span class="tag tag--free">Free</span>')
: '<span class="tag tag--locked">🔒 Locked</span>') +
'<span class="dot" aria-hidden="true"></span>' +
"</span>";
btn.addEventListener("click", function () {
onSelect(c, btn);
});
li.appendChild(btn);
listEl.appendChild(li);
});
if (rows.length === 0) {
emptyTermEl.textContent = "“" + filterTerm.trim() + "”";
emptyEl.hidden = false;
} else {
emptyEl.hidden = true;
}
}
function onSelect(c, btn) {
if (!c.free) {
toast("Chapter " + c.n + " is locked — unlock to read.");
return;
}
if (!c.read) {
c.read = true;
updateCounts();
// re-render to update sort-independent state, then re-flag the row
render();
var fresh = listEl.querySelector('.chapter[data-n="' + c.n + '"]');
if (fresh) {
fresh.classList.add("just-read");
fresh.addEventListener(
"animationend",
function () {
fresh.classList.remove("just-read");
},
{ once: true }
);
}
toast("Marked chapter " + c.n + " as read.");
} else {
toast("Opening chapter " + c.n + "…");
}
}
function updateCounts() {
chapterCountEl.textContent = CHAPTERS.length;
freeCountEl.textContent = CHAPTERS.filter(function (c) {
return c.free;
}).length;
}
// ---- events ----
inputEl.addEventListener("input", function () {
filterTerm = inputEl.value;
render();
});
sortEl.addEventListener("click", function () {
sortNewest = !sortNewest;
sortEl.setAttribute("aria-pressed", String(!sortNewest));
sortEl.querySelector(".sort__label").textContent = sortNewest ? "Newest" : "Oldest";
render();
toast(sortNewest ? "Sorted newest first." : "Sorted oldest first.");
});
// ---- init ----
updateCounts();
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Neon Ronin — Chapter Index</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>
<main class="reader" role="main">
<header class="series">
<div class="series__badge" aria-hidden="true">SS</div>
<div class="series__meta">
<p class="series__kicker">Ongoing · Weekly</p>
<h1 class="series__title">Neon Ronin</h1>
<p class="series__sub">Vol. 3 — <span id="chapterCount">0</span> chapters · <span id="freeCount">0</span> free</p>
</div>
<div class="series__sfx" aria-hidden="true">KA-POW!</div>
</header>
<section class="toolbar" aria-label="Chapter controls">
<div class="search">
<span class="search__icon" aria-hidden="true">🔍</span>
<input
id="filterInput"
class="search__input"
type="search"
placeholder="Filter by title or # (e.g. 12 or vault)"
aria-label="Filter chapters by title or number"
autocomplete="off"
/>
</div>
<button id="sortToggle" class="sort" type="button" aria-pressed="false">
<span class="sort__label">Newest</span>
<span class="sort__arrow" aria-hidden="true">↓</span>
</button>
</section>
<ol id="chapterList" class="chapters" aria-label="Chapter list"></ol>
<p id="empty" class="empty" hidden>No chapters match <strong id="emptyTerm"></strong>.</p>
</main>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Chapter / Episode Index
A self-contained chapter index for a fictional serialized comic, Neon Ronin. The scrollable list renders one ink-bordered card per chapter, each with a Bangers-lettered number, a halftone thumbnail, the title, release date and page count, a free-versus-locked badge, and a small read-state dot that fills in once you’ve finished a chapter.
The sticky header carries a live filter input — type a title fragment or a chapter number and the list narrows instantly — alongside a newest/oldest sort toggle whose arrow flips to match the active direction. Clicking a free chapter marks it as read: the dot lights up, the badge swaps to a “Read” pill, and the row plays a quick pop highlight. Locked chapters fire a toast prompting you to unlock instead, and an empty state appears when no chapter matches the current filter.
Everything is vanilla HTML, CSS, and JavaScript with no dependencies. The styling leans on
comic primitives — thick var(--ink) borders, hard drop-shadows, halftone dot textures, and
bold accent colors — and the layout collapses gracefully down to ~360px, hiding thumbnails and
stacking the toolbar on narrow screens. Buttons are keyboard-usable and every row carries an
aria-label describing its number, lock state, and read state.
Illustrative UI only — fictional series, characters, and data.