Storybook — Bookshelf Browse
A cozy children's library where colorful storybooks line wooden shelves drawn entirely in CSS. Every cover is an inline-SVG illustration — foxes, whales, comets, and cloud-ships — and hovering pulls a book forward off its plank. Category tabs sort the shelves into Animals, Bedtime, and Adventure, a live search filters by title or author, and tapping a cover opens a friendly detail card with the blurb, recommended ages, read time, star rating, a Read button, and favorites. An easy-read font toggle makes the whole page more legible.
MCP
Code
:root {
--bg: #fff8ef;
--bg-2: #fff1df;
--ink: #2c2350;
--ink-soft: #6a6190;
--primary: #ff8a3d;
--secondary: #5ec5d6;
--accent: #ffd23f;
--pink: #ff6f9c;
--green: #7bd389;
--purple: #a98bff;
--card: #ffffff;
--wood: #c98a4b;
--wood-dark: #a86a32;
--wood-edge: #8a531f;
--r: 22px;
--r-sm: 14px;
--shadow: 0 14px 30px rgba(44, 35, 80, 0.14);
--shadow-soft: 0 8px 18px rgba(44, 35, 80, 0.10);
--ring: 0 0 0 4px rgba(94, 197, 214, 0.55);
--font-display: "Baloo 2", system-ui, sans-serif;
--font-body: "Nunito", system-ui, sans-serif;
--ls: normal;
}
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: var(--font-body);
letter-spacing: var(--ls);
color: var(--ink);
line-height: 1.5;
background:
radial-gradient(1100px 600px at 80% -10%, #fff4e0 0%, transparent 60%),
radial-gradient(900px 500px at -10% 10%, #ffeaf1 0%, transparent 55%),
var(--bg);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
/* Easy-read / dyslexia-friendly mode */
body.easy-read {
--font-body: "Comic Sans MS", "Trebuchet MS", "Baloo 2", system-ui, sans-serif;
--ls: 0.03em;
line-height: 1.75;
word-spacing: 0.08em;
}
.wrap { width: min(1120px, 100% - 2rem); margin-inline: auto; }
.skip-link {
position: absolute; left: 12px; top: -60px;
background: var(--ink); color: #fff; padding: 10px 16px;
border-radius: 999px; font-weight: 700; z-index: 50;
transition: top 0.2s ease;
}
.skip-link:focus { top: 12px; }
/* ---------- Top bar ---------- */
.topbar {
position: sticky; top: 0; z-index: 30;
background: rgba(255, 248, 239, 0.86);
backdrop-filter: blur(8px);
border-bottom: 3px solid #ffe2c2;
padding: 12px 0;
}
.topbar-inner {
display: flex; align-items: center; gap: 16px;
flex-wrap: wrap; justify-content: space-between;
}
.brand { display: flex; align-items: center; gap: 12px; text-decoration: none; color: inherit; }
.brand-mark {
display: grid; place-items: center;
width: 56px; height: 56px; border-radius: 18px;
background: linear-gradient(135deg, var(--accent), var(--primary));
border: 3px solid var(--ink); box-shadow: var(--shadow-soft);
transform: rotate(-4deg);
}
.brand-text { display: flex; flex-direction: column; line-height: 1.05; }
.brand-name { font-family: var(--font-display); font-weight: 800; font-size: 1.45rem; }
.brand-sub { font-size: 0.78rem; color: var(--ink-soft); font-weight: 700; }
.topbar-tools { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.search {
display: flex; align-items: center; gap: 8px;
background: var(--card); border: 3px solid #ffd9b0;
border-radius: 999px; padding: 6px 8px 6px 16px;
min-height: 50px; box-shadow: var(--shadow-soft);
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.search:focus-within { border-color: var(--secondary); box-shadow: var(--ring); }
.search-ico { font-size: 1.05rem; }
.search input {
border: 0; outline: 0; background: transparent;
font: inherit; font-weight: 700; color: var(--ink);
width: 200px; min-width: 0;
}
.search input::placeholder { color: var(--ink-soft); font-weight: 600; }
.search-clear {
border: 0; background: #ffe2c2; color: var(--ink);
width: 34px; height: 34px; border-radius: 999px;
font-size: 1.3rem; line-height: 1; cursor: pointer;
display: grid; place-items: center;
}
.search-clear:hover { background: var(--accent); }
.dyslexia-toggle {
display: inline-flex; align-items: center; gap: 10px;
background: var(--card); border: 3px solid #d8cfff;
border-radius: 999px; padding: 8px 16px 8px 10px;
min-height: 50px; cursor: pointer; font: inherit;
font-weight: 700; color: var(--ink); box-shadow: var(--shadow-soft);
}
.dys-track {
position: relative; width: 44px; height: 26px;
background: #e7e0ff; border-radius: 999px; transition: background 0.18s ease;
}
.dys-knob {
position: absolute; top: 3px; left: 3px;
width: 20px; height: 20px; border-radius: 50%;
background: var(--purple); transition: transform 0.18s ease;
box-shadow: 0 2px 4px rgba(44, 35, 80, 0.3);
}
.dyslexia-toggle[aria-checked="true"] .dys-track { background: #c9b9ff; }
.dyslexia-toggle[aria-checked="true"] .dys-knob { transform: translateX(18px); }
.dys-label { font-size: 0.92rem; }
/* ---------- Hero ---------- */
.hero {
display: flex; align-items: center; justify-content: space-between;
gap: 18px; margin: 26px 0 12px;
background: linear-gradient(135deg, #fff, #fff3e3);
border: 3px solid #ffe2c2; border-radius: var(--r);
padding: 22px 26px; box-shadow: var(--shadow-soft);
}
.hero-text h1 {
font-family: var(--font-display); font-weight: 800;
font-size: clamp(1.6rem, 4.5vw, 2.5rem); margin: 0 0 6px;
line-height: 1.1;
}
.hero-text p { margin: 0; color: var(--ink-soft); font-weight: 600; max-width: 46ch; }
.hero-art { flex: none; animation: bob 4s ease-in-out infinite; }
@keyframes bob { 0%, 100% { transform: translateY(0) rotate(-1deg); } 50% { transform: translateY(-7px) rotate(2deg); } }
/* ---------- Tabs ---------- */
.tabs {
display: flex; gap: 10px; flex-wrap: wrap;
margin: 20px 0; padding: 6px;
}
.tab {
display: inline-flex; align-items: center; gap: 8px;
font: inherit; font-family: var(--font-display); font-weight: 700;
font-size: 1rem; color: var(--ink);
background: var(--card); border: 3px solid #ffd9b0;
border-radius: 999px; padding: 10px 20px; min-height: 48px;
cursor: pointer; box-shadow: var(--shadow-soft);
transition: transform 0.14s ease, background 0.14s ease, border-color 0.14s ease;
}
.tab .tab-emoji { font-size: 1.15rem; }
.tab:hover { transform: translateY(-2px) rotate(-1deg); }
.tab.is-active {
color: #fff; background: linear-gradient(135deg, var(--primary), var(--pink));
border-color: var(--ink);
}
/* ---------- Shelves ---------- */
.shelf { margin: 8px 0 40px; }
.shelf-head {
display: flex; align-items: baseline; gap: 10px;
margin: 0 0 14px; padding-left: 4px;
}
.shelf-head h2 {
font-family: var(--font-display); font-weight: 800;
font-size: 1.4rem; margin: 0;
}
.shelf-head .count { color: var(--ink-soft); font-weight: 700; font-size: 0.9rem; }
.shelf-board {
position: relative;
border-radius: 18px;
padding: 18px 20px 0;
background:
repeating-linear-gradient(180deg, rgba(255,255,255,0.05) 0 6px, rgba(0,0,0,0.04) 6px 12px),
linear-gradient(180deg, #ffeccb, #ffe0b3);
border: 3px solid #f0c99a;
box-shadow: var(--shadow-soft);
}
.books {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
gap: 18px;
padding-bottom: 22px;
}
/* The wooden plank under each row */
.shelf-board::after {
content: "";
display: block; height: 16px; margin: 0 -20px;
border-radius: 0 0 16px 16px;
background:
linear-gradient(180deg, var(--wood), var(--wood-dark) 70%, var(--wood-edge));
border-top: 2px solid rgba(0,0,0,0.12);
}
/* ---------- Book cover ---------- */
.book {
position: relative;
appearance: none; border: 0; padding: 0; margin: 0;
background: transparent; cursor: pointer; text-align: left;
display: block;
transition: transform 0.18s cubic-bezier(.34,1.56,.64,1);
border-radius: 12px;
}
.book:focus-visible { outline: none; }
.book:focus-visible .book-cover { box-shadow: var(--ring), var(--shadow); }
.book-cover {
position: relative;
aspect-ratio: 3 / 4;
border-radius: 6px 12px 12px 6px;
overflow: hidden;
border: 3px solid var(--ink);
box-shadow: var(--shadow-soft);
transition: box-shadow 0.18s ease;
}
.book-cover svg { display: block; width: 100%; height: 100%; }
/* book spine accent */
.book-cover::before {
content: ""; position: absolute; left: 0; top: 0; bottom: 0;
width: 9px; background: rgba(0,0,0,0.18);
border-right: 1px solid rgba(255,255,255,0.25);
z-index: 2;
}
.book-title {
margin: 9px 4px 0; font-family: var(--font-display);
font-weight: 700; font-size: 0.92rem; line-height: 1.2;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
overflow: hidden;
}
.book-author { margin: 1px 4px 0; font-size: 0.76rem; color: var(--ink-soft); font-weight: 600; }
/* Hover: pull the book forward off the shelf */
.book:hover { transform: translateY(-12px) scale(1.05) rotate(-1deg); z-index: 5; }
.book:hover .book-cover { box-shadow: var(--shadow); }
.book:active { transform: translateY(-6px) scale(1.02); }
/* favorite heart badge */
.book-fav {
position: absolute; top: 6px; right: 6px; z-index: 3;
font-size: 1.1rem; filter: drop-shadow(0 1px 1px rgba(0,0,0,0.3));
opacity: 0; transform: scale(0.4); transition: all 0.2s ease;
}
.book.is-fav .book-fav { opacity: 1; transform: scale(1); }
.empty {
text-align: center; font-family: var(--font-display);
font-weight: 700; font-size: 1.25rem; color: var(--ink-soft);
padding: 50px 20px;
}
.empty span { display: block; font-size: 2.4rem; margin-bottom: 8px; }
/* ---------- Detail dialog ---------- */
.overlay {
position: fixed; inset: 0; z-index: 60;
background: rgba(44, 35, 80, 0.5);
backdrop-filter: blur(3px);
display: grid; place-items: center; padding: 18px;
animation: fade 0.18s ease;
}
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
.detail {
position: relative;
width: min(640px, 100%);
max-height: 92vh; overflow: auto;
background: var(--card);
border: 4px solid var(--ink);
border-radius: var(--r);
box-shadow: var(--shadow);
display: grid; grid-template-columns: 200px 1fr; gap: 0;
animation: pop 0.28s cubic-bezier(.34,1.56,.64,1);
}
@keyframes pop { from { transform: scale(0.82) translateY(14px); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.detail-close {
position: absolute; top: 10px; right: 10px; z-index: 4;
width: 40px; height: 40px; border-radius: 999px;
border: 3px solid var(--ink); background: var(--accent);
font-size: 1.5rem; line-height: 1; cursor: pointer; color: var(--ink);
display: grid; place-items: center;
}
.detail-close:hover { background: var(--pink); color: #fff; }
.detail-cover {
border-right: 4px solid var(--ink);
display: grid; place-items: stretch;
}
.detail-cover svg { width: 100%; height: 100%; display: block; }
.detail-body { padding: 22px 24px 24px; }
.detail-tag {
display: inline-flex; align-items: center; gap: 6px;
font-family: var(--font-display); font-weight: 700;
font-size: 0.78rem; padding: 4px 12px; border-radius: 999px;
background: var(--bg-2); border: 2px solid #f0c99a;
}
.detail-title {
font-family: var(--font-display); font-weight: 800;
font-size: clamp(1.4rem, 4vw, 1.9rem); margin: 10px 0 2px; line-height: 1.1;
}
.detail-author { margin: 0 0 12px; color: var(--ink-soft); font-weight: 700; }
.detail-meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }
.chip {
display: inline-flex; align-items: center; gap: 6px;
font-weight: 700; font-size: 0.85rem;
padding: 5px 12px; border-radius: 999px;
background: var(--bg-2); border: 2px solid #ffd9b0;
}
.stars { font-size: 1rem; letter-spacing: 1px; color: var(--accent); }
.detail-blurb { margin: 0 0 20px; font-weight: 600; }
.detail-actions { display: flex; gap: 10px; flex-wrap: wrap; }
.btn {
font: inherit; font-family: var(--font-display); font-weight: 700;
font-size: 1rem; border-radius: 999px; min-height: 52px;
padding: 12px 22px; cursor: pointer;
border: 3px solid var(--ink);
transition: transform 0.14s ease, filter 0.14s ease;
}
.btn:hover { transform: translateY(-2px); }
.btn:active { transform: translateY(0) scale(0.98); }
.btn-read {
color: #fff;
background: linear-gradient(135deg, var(--green), var(--secondary));
}
.btn-read:hover { filter: brightness(1.05); }
.btn-ghost { background: var(--card); color: var(--ink); }
.btn-ghost[aria-pressed="true"] { background: var(--pink); color: #fff; }
/* ---------- Toast ---------- */
.toast {
position: fixed; left: 50%; bottom: 24px; z-index: 80;
transform: translateX(-50%) translateY(120%);
background: var(--ink); color: #fff;
font-weight: 700; padding: 14px 22px; border-radius: 999px;
box-shadow: var(--shadow); border: 3px solid var(--accent);
opacity: 0; transition: transform 0.3s cubic-bezier(.34,1.56,.64,1), opacity 0.3s ease;
max-width: calc(100% - 32px); text-align: center;
}
.toast.show { transform: translateX(-50%) translateY(0); opacity: 1; }
/* ---------- Focus ring (generic) ---------- */
:focus-visible { outline: 3px solid var(--secondary); outline-offset: 2px; border-radius: 8px; }
.search:focus-within :focus-visible { outline: none; }
/* ---------- Responsive ---------- */
@media (max-width: 720px) {
.topbar-tools { width: 100%; justify-content: space-between; }
.search { flex: 1; }
.search input { width: 100%; }
.hero { flex-direction: column-reverse; text-align: center; padding: 20px; }
.hero-text p { margin-inline: auto; }
.detail { grid-template-columns: 1fr; }
.detail-cover { border-right: 0; border-bottom: 4px solid var(--ink); aspect-ratio: 16 / 9; }
}
@media (max-width: 460px) {
.books { grid-template-columns: repeat(auto-fill, minmax(108px, 1fr)); gap: 14px; }
.brand-mark { width: 48px; height: 48px; }
.dys-label { display: none; }
.dyslexia-toggle { padding: 8px 10px; }
.tabs { gap: 8px; }
.tab { padding: 9px 14px; font-size: 0.92rem; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
.hero-art { animation: none; }
.book:hover { transform: translateY(-6px); }
}(function () {
"use strict";
// ---------- Data (fictional storybooks) ----------
// c1 = cover background, c2 = accent shape color, art = inline scene id
var BOOKS = [
{ id: "b1", title: "Pip the Brave Little Fox", author: "Mara Lindqvist", cat: "animals", age: "3–6", mins: 8, stars: 5, c1: "#ff8a3d", c2: "#ffd23f", art: "fox",
blurb: "When the woodland lights flicker out, a tiny fox named Pip discovers that even the smallest paws can carry a very big lantern of courage." },
{ id: "b2", title: "The Whale Who Forgot the Sea", author: "Tomás Rivera", cat: "animals", age: "4–7", mins: 11, stars: 4, c1: "#5ec5d6", c2: "#a98bff", art: "whale",
blurb: "A gentle blue whale wakes up in a tide pool with no memory of the ocean — and a hermit crab who promises to help her find her song again." },
{ id: "b3", title: "Buttons & the Tea-Party Bears", author: "Priya Anand", cat: "animals", age: "2–5", mins: 6, stars: 5, c1: "#ff6f9c", c2: "#ffd23f", art: "bear",
blurb: "Three sleepy bears, one stack of honey cakes, and a teapot that whistles tunes. Will Buttons get a slice before nap time?" },
{ id: "b4", title: "Goodnight, Little Comet", author: "Sofia Marlowe", cat: "bedtime", age: "2–5", mins: 5, stars: 5, c1: "#3b3a7a", c2: "#ffd23f", art: "comet",
blurb: "A drowsy comet says goodnight to every planet it passes, dimming its tail one sparkle at a time until the whole sky is hushed and warm." },
{ id: "b5", title: "The Moon Has a Pillow Fort", author: "Kenji Watanabe", cat: "bedtime", age: "3–6", mins: 7, stars: 4, c1: "#a98bff", c2: "#fff1df", art: "moon",
blurb: "Up past the rooftops, the moon builds a fort of clouds and invites the stars in for whispered stories until everyone drifts off." },
{ id: "b6", title: "Counting Sheep on Cloud Nine", author: "Hana Brooks", cat: "bedtime", age: "2–4", mins: 4, stars: 5, c1: "#7bd389", c2: "#fff8ef", art: "sheep",
blurb: "One fluffy sheep, two fluffy sheep… each hops onto a softer cloud, and by ten the whole flock is snoring sweet bedtime snores." },
{ id: "b7", title: "Captain Mango & the Sky Pirates", author: "Diego Salt", cat: "adventure", age: "5–8", mins: 13, stars: 5, c1: "#ff8a3d", c2: "#5ec5d6", art: "ship",
blurb: "Hoist the banana flag! Captain Mango sails a cloud-ship over rainbow seas to rescue a kite stolen by the giggling Sky Pirates." },
{ id: "b8", title: "The Treehouse at the Edge of Maps", author: "Lena Okoye", cat: "adventure", age: "6–9", mins: 15, stars: 4, c1: "#7bd389", c2: "#ffd23f", art: "tree",
blurb: "Two friends find a treehouse that grows a new room every night, each door opening onto a country that isn't on any map." },
{ id: "b9", title: "Rocket Boots & the Bouncing Planet", author: "Ravi Kapoor", cat: "adventure", age: "4–7", mins: 9, stars: 5, c1: "#a98bff", c2: "#ff6f9c", art: "rocket",
blurb: "Strap on your rocket boots! On Planet Springy, the ground is a trampoline and the only way down is the bounciest bounce of all." },
{ id: "b10", title: "Hedgehog's Umbrella Garden", author: "Mei-Ling Chen", cat: "animals", age: "3–6", mins: 7, stars: 5, c1: "#7bd389", c2: "#ff6f9c", art: "fox",
blurb: "Quill the hedgehog plants umbrellas instead of flowers, and when the spring rain comes, the whole meadow blooms into a parade of color." },
{ id: "b11", title: "Snore the Dragon's Quiet Cave", author: "Otis Greenfield", cat: "bedtime", age: "3–6", mins: 6, stars: 4, c1: "#3b3a7a", c2: "#ff8a3d", art: "moon",
blurb: "Snore is the sleepiest dragon in the valley, and tonight he tucks the village in with one warm, glowing, gentle breath of firelight." },
{ id: "b12", title: "The Lighthouse That Sailed Away", author: "Cora Westbrook", cat: "adventure", age: "5–8", mins: 12, stars: 5, c1: "#5ec5d6", c2: "#ffd23f", art: "ship",
blurb: "When the tide rose too high, a lonely lighthouse pulled up its anchor and set off to find the ships it had been waving to for years." }
];
var CATS = {
all: { label: "All Stories", emoji: "📚" },
animals: { label: "Animal Friends", emoji: "🦊" },
bedtime: { label: "Bedtime & Dreams", emoji: "🌙" },
adventure: { label: "Big Adventures", emoji: "🚀" }
};
var favs = {}; // id -> true
// ---------- Inline SVG cover artwork ----------
function scene(id, c2) {
switch (id) {
case "fox":
return '<path d="M30 64c0-18 14-30 30-30s30 12 30 30v34H30Z" fill="' + c2 + '" opacity="0.9"/>' +
'<circle cx="60" cy="70" r="22" fill="#fff"/><path d="M40 52l8 14M80 52l-8 14" stroke="#2c2350" stroke-width="3" stroke-linecap="round"/>' +
'<circle cx="52" cy="70" r="3.4" fill="#2c2350"/><circle cx="68" cy="70" r="3.4" fill="#2c2350"/><path d="M55 80h10l-5 5Z" fill="#2c2350"/>';
case "whale":
return '<path d="M22 78c10-18 66-18 76 0 6 10-6 18-18 14-10 12-30 12-40 0-12 4-24-4-18-14Z" fill="' + c2 + '"/>' +
'<circle cx="46" cy="74" r="3.2" fill="#fff"/><path d="M78 44c4-10 14-8 14 0" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round"/>';
case "bear":
return '<circle cx="60" cy="72" r="26" fill="' + c2 + '"/><circle cx="42" cy="50" r="9" fill="' + c2 + '"/><circle cx="78" cy="50" r="9" fill="' + c2 + '"/>' +
'<circle cx="52" cy="70" r="3.2" fill="#2c2350"/><circle cx="68" cy="70" r="3.2" fill="#2c2350"/><circle cx="60" cy="78" r="4" fill="#2c2350"/>';
case "comet":
return '<path d="M28 96L84 40" stroke="' + c2 + '" stroke-width="10" stroke-linecap="round" opacity="0.7"/>' +
'<circle cx="84" cy="40" r="13" fill="' + c2 + '"/><path d="M84 40l3 7 7 3-7 3-3 7-3-7-7-3 7-3 3-7Z" fill="#fff"/>';
case "moon":
return '<path d="M82 38a30 30 0 1 0 0 56 24 24 0 0 1 0-56Z" fill="' + c2 + '"/>' +
'<circle cx="40" cy="44" r="2.6" fill="#fff"/><circle cx="92" cy="84" r="2.6" fill="#fff"/><circle cx="34" cy="80" r="2" fill="#fff"/>';
case "sheep":
return '<circle cx="60" cy="66" r="24" fill="' + c2 + '"/><circle cx="40" cy="58" r="11" fill="' + c2 + '"/><circle cx="80" cy="58" r="11" fill="' + c2 + '"/>' +
'<ellipse cx="60" cy="92" rx="20" ry="6" fill="#2c2350" opacity="0.25"/><circle cx="54" cy="66" r="3" fill="#2c2350"/><circle cx="66" cy="66" r="3" fill="#2c2350"/>';
case "ship":
return '<path d="M30 78h60l-8 16H38Z" fill="' + c2 + '"/><path d="M60 26v52" stroke="#fff" stroke-width="4"/><path d="M60 30l24 8-24 8Z" fill="#fff"/>' +
'<path d="M24 86c8 6 16 6 24 0 8 6 16 6 24 0 8 6 16 6 24 0" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" opacity="0.7"/>';
case "tree":
return '<rect x="54" y="70" width="12" height="28" rx="3" fill="#8a531f"/><circle cx="60" cy="56" r="26" fill="' + c2 + '"/>' +
'<circle cx="44" cy="62" r="13" fill="' + c2 + '"/><circle cx="76" cy="62" r="13" fill="' + c2 + '"/><circle cx="52" cy="48" r="3" fill="#fff"/><circle cx="70" cy="52" r="3" fill="#fff"/>';
case "rocket":
return '<path d="M60 28c14 8 18 26 12 44l-24 0c-6-18-2-36 12-44Z" fill="' + c2 + '"/><circle cx="60" cy="52" r="7" fill="#fff"/>' +
'<path d="M48 72l-10 14h14ZM72 72l10 14H68Z" fill="' + c2 + '"/><path d="M54 88c2 6 10 6 12 0" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round"/>';
default:
return '<circle cx="60" cy="64" r="24" fill="' + c2 + '"/>';
}
}
function coverSVG(book) {
return '<svg viewBox="0 0 120 160" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Cover of ' +
esc(book.title) + '">' +
'<rect width="120" height="160" fill="' + book.c1 + '"/>' +
'<rect x="8" y="8" width="104" height="144" rx="6" fill="none" stroke="rgba(255,255,255,0.5)" stroke-width="2" stroke-dasharray="4 4"/>' +
'<circle cx="22" cy="22" r="3" fill="rgba(255,255,255,0.6)"/><circle cx="98" cy="22" r="3" fill="rgba(255,255,255,0.6)"/>' +
scene(book.art, book.c2) +
'<rect x="14" y="110" width="92" height="36" rx="6" fill="rgba(0,0,0,0.18)"/>' +
'</svg>';
}
// ---------- DOM helpers ----------
function esc(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
});
}
function el(id) { return document.getElementById(id); }
var library = el("library");
var emptyEl = el("empty");
var searchInput = el("search");
var searchClear = el("searchClear");
var tabsNav = el("tabs");
var currentCat = "all";
var currentQuery = "";
// ---------- Render ----------
function matches(book) {
var catOk = currentCat === "all" || book.cat === currentCat;
if (!catOk) return false;
if (!currentQuery) return true;
var q = currentQuery.toLowerCase();
return book.title.toLowerCase().indexOf(q) > -1 ||
book.author.toLowerCase().indexOf(q) > -1;
}
function bookButton(book) {
var btn = document.createElement("button");
btn.className = "book" + (favs[book.id] ? " is-fav" : "");
btn.type = "button";
btn.setAttribute("data-id", book.id);
btn.setAttribute("aria-label", "Open " + book.title + " by " + book.author);
btn.innerHTML =
'<span class="book-fav" aria-hidden="true">❤️</span>' +
'<span class="book-cover">' + coverSVG(book) + '</span>' +
'<span class="book-title">' + esc(book.title) + '</span>' +
'<span class="book-author">' + esc(book.author) + '</span>';
btn.addEventListener("click", function () { openDetail(book); });
return btn;
}
function render() {
var list = BOOKS.filter(matches);
library.innerHTML = "";
if (!list.length) {
emptyEl.hidden = false;
return;
}
emptyEl.hidden = true;
// Group into shelves: by category when "all", else one shelf
var groups;
if (currentCat === "all") {
groups = ["animals", "bedtime", "adventure"].map(function (cat) {
return { cat: cat, items: list.filter(function (b) { return b.cat === cat; }) };
}).filter(function (g) { return g.items.length; });
} else {
groups = [{ cat: currentCat, items: list }];
}
groups.forEach(function (g) {
var meta = CATS[g.cat];
var shelf = document.createElement("section");
shelf.className = "shelf";
var head = document.createElement("div");
head.className = "shelf-head";
head.innerHTML = '<h2><span aria-hidden="true">' + meta.emoji + '</span> ' + esc(meta.label) + '</h2>' +
'<span class="count">' + g.items.length + (g.items.length === 1 ? " story" : " stories") + '</span>';
shelf.appendChild(head);
var board = document.createElement("div");
board.className = "shelf-board";
var grid = document.createElement("div");
grid.className = "books";
g.items.forEach(function (b) { grid.appendChild(bookButton(b)); });
board.appendChild(grid);
shelf.appendChild(board);
library.appendChild(shelf);
});
}
// ---------- Tabs ----------
var tabBtns = Array.prototype.slice.call(tabsNav.querySelectorAll(".tab"));
function selectTab(btn) {
tabBtns.forEach(function (b) {
var on = b === btn;
b.classList.toggle("is-active", on);
b.setAttribute("aria-selected", on ? "true" : "false");
b.tabIndex = on ? 0 : -1;
});
currentCat = btn.getAttribute("data-cat");
library.setAttribute("aria-labelledby", btn.id);
render();
}
tabBtns.forEach(function (btn, i) {
btn.addEventListener("click", function () { selectTab(btn); });
btn.addEventListener("keydown", function (e) {
var dir = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
if (!dir) return;
e.preventDefault();
var next = tabBtns[(i + dir + tabBtns.length) % tabBtns.length];
next.focus();
selectTab(next);
});
});
// ---------- Search ----------
searchInput.addEventListener("input", function () {
currentQuery = searchInput.value.trim();
searchClear.hidden = !currentQuery;
render();
});
searchClear.addEventListener("click", function () {
searchInput.value = "";
currentQuery = "";
searchClear.hidden = true;
searchInput.focus();
render();
});
// ---------- Detail dialog ----------
var overlay = el("overlay");
var detail = el("detail");
var lastFocused = null;
var activeBook = null;
function openDetail(book) {
activeBook = book;
lastFocused = document.activeElement;
var meta = CATS[book.cat];
el("detailCover").innerHTML = coverSVG(book);
el("detailCat").innerHTML = '<span aria-hidden="true">' + meta.emoji + '</span> ' + esc(meta.label);
el("detail-title").textContent = book.title;
el("detailAuthor").textContent = "by " + book.author;
el("detailAge").innerHTML = '<span aria-hidden="true">👶</span> Ages ' + esc(book.age);
el("detailMins").innerHTML = '<span aria-hidden="true">⏱️</span> ' + book.mins + " min read";
el("detailStars").textContent = "★★★★★☆☆☆☆☆".slice(5 - book.stars, 10 - book.stars);
el("detailBlurb").textContent = book.blurb;
var favBtn = el("favBtn");
var isFav = !!favs[book.id];
favBtn.setAttribute("aria-pressed", isFav ? "true" : "false");
favBtn.innerHTML = isFav ? "♥ In favorites" : "♡ Add to favorites";
overlay.hidden = false;
document.body.style.overflow = "hidden";
detail.focus();
}
function closeDetail() {
overlay.hidden = true;
document.body.style.overflow = "";
activeBook = null;
if (lastFocused && lastFocused.focus) lastFocused.focus();
}
el("detailClose").addEventListener("click", closeDetail);
overlay.addEventListener("mousedown", function (e) {
if (e.target === overlay) closeDetail();
});
document.addEventListener("keydown", function (e) {
if (overlay.hidden) return;
if (e.key === "Escape") { closeDetail(); return; }
if (e.key === "Tab") {
// simple focus trap
var focusables = detail.querySelectorAll("button");
var first = focusables[0];
var last = focusables[focusables.length - 1];
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
}
});
el("readBtn").addEventListener("click", function () {
if (activeBook) toast('Opening "' + activeBook.title + '" — happy reading! 📖');
});
el("favBtn").addEventListener("click", function () {
if (!activeBook) return;
var id = activeBook.id;
var btn = el("favBtn");
if (favs[id]) {
delete favs[id];
btn.setAttribute("aria-pressed", "false");
btn.innerHTML = "♡ Add to favorites";
toast("Removed from favorites");
} else {
favs[id] = true;
btn.setAttribute("aria-pressed", "true");
btn.innerHTML = "♥ In favorites";
toast("Added to favorites! ⭐");
}
// update the matching card on the shelf
var card = library.querySelector('.book[data-id="' + id + '"]');
if (card) card.classList.toggle("is-fav", !!favs[id]);
});
// ---------- Dyslexia-friendly toggle ----------
var dysToggle = el("dysToggle");
dysToggle.addEventListener("click", function () {
var on = dysToggle.getAttribute("aria-checked") === "true";
dysToggle.setAttribute("aria-checked", on ? "false" : "true");
document.body.classList.toggle("easy-read", !on);
toast(on ? "Easy-read font off" : "Easy-read font on — bigger spacing!");
});
// ---------- Toast ----------
var toastEl = el("toast");
var toastTimer = null;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2600);
}
// ---------- Init ----------
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Storybook — Bookshelf Browse</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=Baloo+2:wght@500;600;700;800&family=Nunito:ital,wght@0,400;0,600;0,700;0,800;1,600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#shelves">Skip to the bookshelf</a>
<header class="topbar" role="banner">
<div class="wrap topbar-inner">
<a class="brand" href="#" aria-label="Twinkle Tales library home">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 48 48" width="40" height="40" role="img" aria-label="Open book">
<path d="M24 12C18 7 9 7 5 9v30c4-2 13-2 19 3 6-5 15-5 19-3V9c-4-2-13-2-19 3Z" fill="#fff" stroke="#2c2350" stroke-width="2.4" stroke-linejoin="round"/>
<path d="M24 12v30" stroke="#2c2350" stroke-width="2.4"/>
<path d="M10 16c3-1 7-1 10 0M10 22c3-1 7-1 10 0M28 16c3-1 7-1 10 0M28 22c3-1 7-1 10 0" stroke="#5ec5d6" stroke-width="2" stroke-linecap="round"/>
<path d="M38 5l1.4 3 3 1.4-3 1.4L38 14l-1.4-3.2L33.6 9.4l3-1.4L38 5Z" fill="#ffd23f" stroke="#2c2350" stroke-width="1.4" stroke-linejoin="round"/>
</svg>
</span>
<span class="brand-text">
<span class="brand-name">Twinkle Tales</span>
<span class="brand-sub">Storybook Library</span>
</span>
</a>
<div class="topbar-tools">
<label class="search" for="search">
<span class="search-ico" aria-hidden="true">🔍</span>
<input id="search" type="search" placeholder="Search stories…" autocomplete="off" aria-label="Search stories by title or author" />
<button type="button" class="search-clear" id="searchClear" aria-label="Clear search" hidden>×</button>
</label>
<button type="button" class="dyslexia-toggle" id="dysToggle" role="switch" aria-checked="false">
<span class="dys-track" aria-hidden="true"><span class="dys-knob"></span></span>
<span class="dys-label">Easy-read font</span>
</button>
</div>
</div>
</header>
<main class="wrap" id="shelves">
<section class="hero" aria-labelledby="hero-title">
<div class="hero-text">
<h1 id="hero-title">Pick a story off the shelf!</h1>
<p>Tap any book to peek inside. Hop between shelves to find animals, bedtime cuddles, and big adventures.</p>
</div>
<div class="hero-art" aria-hidden="true">
<svg viewBox="0 0 160 120" width="160" height="120">
<ellipse cx="80" cy="108" rx="60" ry="9" fill="#2c2350" opacity="0.08"/>
<circle cx="80" cy="60" r="34" fill="#ffd23f" stroke="#2c2350" stroke-width="2.4"/>
<circle cx="68" cy="55" r="4" fill="#2c2350"/>
<circle cx="92" cy="55" r="4" fill="#2c2350"/>
<path d="M66 72c5 6 23 6 28 0" fill="none" stroke="#2c2350" stroke-width="3" stroke-linecap="round"/>
<circle cx="58" cy="66" r="5" fill="#ff6f9c" opacity="0.6"/>
<circle cx="102" cy="66" r="5" fill="#ff6f9c" opacity="0.6"/>
<path d="M30 30l2 5 5 2-5 2-2 5-2-5-5-2 5-2 2-5Z" fill="#5ec5d6" stroke="#2c2350" stroke-width="1.4" stroke-linejoin="round"/>
<path d="M130 28l1.6 4 4 1.6-4 1.6-1.6 4-1.6-4-4-1.6 4-1.6 1.6-4Z" fill="#ff8a3d" stroke="#2c2350" stroke-width="1.4" stroke-linejoin="round"/>
</svg>
</div>
</section>
<nav class="tabs" id="tabs" role="tablist" aria-label="Story categories">
<button class="tab is-active" role="tab" id="tab-all" aria-selected="true" aria-controls="library" data-cat="all"><span class="tab-emoji" aria-hidden="true">📚</span>All Stories</button>
<button class="tab" role="tab" id="tab-animals" aria-selected="false" aria-controls="library" data-cat="animals" tabindex="-1"><span class="tab-emoji" aria-hidden="true">🦊</span>Animals</button>
<button class="tab" role="tab" id="tab-bedtime" aria-selected="false" aria-controls="library" data-cat="bedtime" tabindex="-1"><span class="tab-emoji" aria-hidden="true">🌙</span>Bedtime</button>
<button class="tab" role="tab" id="tab-adventure" aria-selected="false" aria-controls="library" data-cat="adventure" tabindex="-1"><span class="tab-emoji" aria-hidden="true">🚀</span>Adventure</button>
</nav>
<div class="library" id="library" role="tabpanel" aria-labelledby="tab-all" aria-live="polite">
<!-- Shelves injected by script.js -->
</div>
<p class="empty" id="empty" hidden>
<span aria-hidden="true">🔎🐛</span>
No stories found. Try another word or shelf!
</p>
</main>
<!-- Book detail dialog -->
<div class="overlay" id="overlay" hidden>
<div class="detail" id="detail" role="dialog" aria-modal="true" aria-labelledby="detail-title" tabindex="-1">
<button type="button" class="detail-close" id="detailClose" aria-label="Close story details">×</button>
<div class="detail-cover" id="detailCover" aria-hidden="true"></div>
<div class="detail-body">
<span class="detail-tag" id="detailCat"></span>
<h2 class="detail-title" id="detail-title"></h2>
<p class="detail-author" id="detailAuthor"></p>
<div class="detail-meta">
<span class="chip" id="detailAge"></span>
<span class="chip" id="detailMins"></span>
<span class="stars" id="detailStars" aria-hidden="true"></span>
</div>
<p class="detail-blurb" id="detailBlurb"></p>
<div class="detail-actions">
<button type="button" class="btn btn-read" id="readBtn">▶ Read this story</button>
<button type="button" class="btn btn-ghost" id="favBtn" aria-pressed="false">♡ Add to favorites</button>
</div>
</div>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Bookshelf Browse
A warm, round-cornered storybook library built for little hands. Wooden shelves are drawn purely in CSS — planks, grain, and soft shadows — and each one holds a responsive grid of book covers. Every cover is its own inline SVG: a brave fox, a forgetful whale, a sleepy comet, a banana-flag sky-ship. Hovering or focusing a book gently lifts it forward off the shelf with a springy bounce, and a heart badge pops onto any title you have favorited.
The header carries a friendly search box that filters every shelf in real time by story title or author, plus four big pill tabs — All Stories, Animals, Bedtime, and Adventure — that are fully keyboard-navigable with the arrow keys. Picking a category regroups the books into the right wooden shelves, and an empty state appears with an encouraging message when nothing matches. Tapping any cover opens a modal detail card showing the cover art, a short blurb, recommended ages, an estimated read time, a star rating, a Read this story button, and an Add to favorites toggle. The dialog traps focus, closes on Escape or backdrop click, and returns focus to the book you came from.
An easy-read font toggle in the header switches the body to a more legible typeface with extra letter and word spacing, helping early and dyslexic readers. The layout collapses gracefully to a single-column shelf and a stacked detail card down to 360px, large touch targets stay at least 48px, motion respects prefers-reduced-motion, and a small toast confirms each action.
Illustrative kids’ UI only — fictional stories, characters, and audio.