Storybook — Sticker Reward Board
A playful storybook sticker reward board where a grid of dashed empty slots fills with collectible inline-SVG stickers as a child checks off tasks from a Tasks Today list. Each completed chore flies an emoji into the next slot and pops a hand-drawn sticker on with a bouncy spring. A progress bar tracks the way to the next reward chest, and filling it triggers a confetti celebration that awards a special golden trophy sticker. Earned stickers, checked tasks, and unlocked chests persist to localStorage.
MCP
Code
:root {
--bg: #fff8ef;
--bg-2: #ffeede;
--card: #ffffff;
--ink: #2c2350;
--ink-soft: #6a6090;
--primary: #ff8a3d;
--secondary: #5ec5d6;
--accent: #ffd23f;
--pink: #ff6f9c;
--green: #7bd389;
--purple: #a98cff;
--r: 22px;
--r-sm: 14px;
--r-pill: 999px;
--border: 3px solid #2c2350;
--shadow: 0 10px 0 rgba(44, 35, 80, 0.12);
--shadow-soft: 0 14px 30px rgba(44, 35, 80, 0.14);
--ring: 0 0 0 4px #fff, 0 0 0 8px var(--secondary);
--font-head: "Baloo 2", system-ui, sans-serif;
--font-body: "Nunito", system-ui, sans-serif;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
min-height: 100vh;
font-family: var(--font-body);
font-size: 17px;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(1200px 520px at 12% -10%, #fff6e3 0%, transparent 60%),
radial-gradient(900px 480px at 110% 0%, #e9f7fb 0%, transparent 55%),
linear-gradient(180deg, var(--bg) 0%, var(--bg-2) 100%);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
/* dyslexia-friendly easy-read mode */
body.easy-read {
--font-body: "Comic Sans MS", "Baloo 2", system-ui, sans-serif;
letter-spacing: 0.4px;
word-spacing: 2px;
line-height: 1.7;
}
img,
svg {
display: block;
max-width: 100%;
}
button {
font: inherit;
cursor: pointer;
}
.skip-link {
position: absolute;
left: 50%;
top: -64px;
transform: translateX(-50%);
background: var(--ink);
color: #fff;
padding: 10px 18px;
border-radius: var(--r-pill);
font-family: var(--font-head);
font-weight: 700;
z-index: 60;
transition: top 0.18s ease;
}
.skip-link:focus {
top: 12px;
outline: none;
}
/* ---------- decorative sky ---------- */
.sky {
position: fixed;
inset: 0;
overflow: hidden;
pointer-events: none;
z-index: 0;
}
.cloud {
position: absolute;
width: 130px;
height: 46px;
background: #ffffff;
border-radius: 999px;
opacity: 0.7;
box-shadow:
34px 8px 0 -6px #fff,
-32px 10px 0 -8px #fff;
animation: drift 26s linear infinite;
}
.cloud--1 { top: 9%; left: -160px; animation-duration: 32s; }
.cloud--2 { top: 26%; left: -200px; transform: scale(0.8); animation-duration: 40s; animation-delay: -8s; }
.cloud--3 { top: 14%; left: -180px; transform: scale(1.15); animation-duration: 48s; animation-delay: -20s; }
@keyframes drift {
to { transform: translateX(118vw); }
}
.sparkle {
position: absolute;
color: var(--accent);
font-size: 26px;
filter: drop-shadow(0 2px 2px rgba(44, 35, 80, 0.18));
animation: twinkle 3.4s ease-in-out infinite;
}
.sparkle--1 { top: 18%; right: 12%; }
.sparkle--2 { top: 62%; left: 8%; animation-delay: 0.8s; color: var(--pink); }
.sparkle--3 { top: 78%; right: 16%; animation-delay: 1.6s; color: var(--secondary); }
@keyframes twinkle {
0%, 100% { transform: scale(0.7) rotate(-8deg); opacity: 0.45; }
50% { transform: scale(1.2) rotate(8deg); opacity: 1; }
}
/* ---------- shell ---------- */
.app {
position: relative;
z-index: 1;
width: min(1040px, 94vw);
margin: clamp(18px, 4vw, 42px) auto;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
flex-wrap: wrap;
margin-bottom: clamp(16px, 3vw, 26px);
}
.brand {
display: flex;
align-items: center;
gap: 14px;
}
.brand__mark svg {
filter: drop-shadow(0 5px 0 rgba(44, 35, 80, 0.15));
animation: bob 4s ease-in-out infinite;
}
@keyframes bob {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
.brand__kicker {
margin: 0;
font-family: var(--font-head);
font-weight: 600;
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 1.4px;
color: var(--primary);
}
.brand__title {
margin: 0;
font-family: var(--font-head);
font-weight: 800;
font-size: clamp(1.5rem, 4.4vw, 2.1rem);
line-height: 1.05;
color: var(--ink);
}
.topbar__tools {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.chip-btn {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 48px;
padding: 0 18px;
border: var(--border);
border-radius: var(--r-pill);
background: var(--card);
color: var(--ink);
font-family: var(--font-head);
font-weight: 700;
font-size: 0.95rem;
box-shadow: 0 4px 0 rgba(44, 35, 80, 0.18);
transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.12s ease;
}
.chip-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 0 rgba(44, 35, 80, 0.2);
}
.chip-btn:active {
transform: translateY(2px);
box-shadow: 0 2px 0 rgba(44, 35, 80, 0.2);
}
.chip-btn[aria-pressed="true"] {
background: var(--accent);
}
/* ---------- layout ---------- */
.layout {
display: grid;
grid-template-columns: 1.35fr 1fr;
gap: clamp(16px, 2.4vw, 24px);
align-items: start;
}
.board-card,
.tasks-card {
background: var(--card);
border: var(--border);
border-radius: var(--r);
box-shadow: var(--shadow), var(--shadow-soft);
padding: clamp(18px, 2.6vw, 26px);
}
.board-card__head,
.tasks-card__head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.board-card__title,
.tasks-card__title {
margin: 0;
font-family: var(--font-head);
font-weight: 800;
font-size: clamp(1.2rem, 3vw, 1.5rem);
color: var(--ink);
}
.board-card__count {
margin: 0;
font-weight: 700;
color: var(--ink-soft);
}
.board-card__count strong {
color: var(--primary);
font-size: 1.15rem;
}
.tasks-card__sub {
margin: 2px 0 0;
font-size: 0.92rem;
color: var(--ink-soft);
flex-basis: 100%;
}
/* ---------- sticker board grid ---------- */
.board {
list-style: none;
margin: 0 0 20px;
padding: 0;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: clamp(8px, 1.6vw, 14px);
}
.slot {
position: relative;
aspect-ratio: 1 / 1;
border-radius: var(--r-sm);
border: 3px dashed #cdc4e6;
background:
repeating-linear-gradient(45deg, #fbf7ff 0 8px, #f3eeff 8px 16px);
display: grid;
place-items: center;
overflow: hidden;
transition: border-color 0.2s ease, background 0.2s ease;
}
.slot__ghost {
font-size: clamp(1.1rem, 4vw, 1.6rem);
opacity: 0.32;
filter: grayscale(1);
user-select: none;
}
.slot.is-filled {
border-style: solid;
border-color: var(--accent);
background:
radial-gradient(circle at 50% 38%, #fff 0%, #fff5d8 70%, #ffe9a8 100%);
box-shadow: inset 0 0 0 3px rgba(255, 210, 63, 0.4);
}
.slot.is-filled .slot__ghost {
display: none;
}
.slot__sticker {
width: 78%;
height: 78%;
display: grid;
place-items: center;
}
.slot__sticker svg {
width: 100%;
height: 100%;
filter: drop-shadow(0 4px 4px rgba(44, 35, 80, 0.22));
}
.slot.is-gold {
border-color: var(--primary);
background:
radial-gradient(circle at 50% 36%, #fff 0%, #ffe7a0 60%, #ffcf5c 100%);
box-shadow: inset 0 0 0 3px rgba(255, 138, 61, 0.55), 0 0 18px rgba(255, 210, 63, 0.6);
}
@keyframes pop-in {
0% { transform: scale(0) rotate(-30deg); opacity: 0; }
60% { transform: scale(1.25) rotate(10deg); opacity: 1; }
80% { transform: scale(0.92) rotate(-4deg); }
100% { transform: scale(1) rotate(0); }
}
.slot.just-popped .slot__sticker {
animation: pop-in 0.55s cubic-bezier(0.3, 1.4, 0.5, 1) both;
}
/* ---------- reward chest progress ---------- */
.chest {
border-top: 3px dashed #eadff5;
padding-top: 18px;
}
.chest__top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 10px;
}
.chest__title {
margin: 0;
display: inline-flex;
align-items: center;
gap: 8px;
font-family: var(--font-head);
font-weight: 700;
font-size: 1.02rem;
color: var(--ink);
}
.chest__icon {
font-size: 1.3rem;
display: inline-block;
}
.chest__icon.is-ready {
animation: wiggle 0.7s ease-in-out infinite;
}
@keyframes wiggle {
0%, 100% { transform: rotate(-8deg) scale(1.05); }
50% { transform: rotate(8deg) scale(1.15); }
}
.chest__hint {
margin: 0;
font-size: 0.88rem;
font-weight: 700;
color: var(--ink-soft);
}
.chest__bar {
position: relative;
height: 26px;
border-radius: var(--r-pill);
background: #f0e9fb;
border: 3px solid var(--ink);
overflow: hidden;
box-shadow: inset 0 2px 4px rgba(44, 35, 80, 0.12);
}
.chest__fill {
display: block;
height: 100%;
width: 0%;
border-radius: var(--r-pill);
background: linear-gradient(90deg, var(--green) 0%, var(--secondary) 60%, var(--accent) 100%);
transition: width 0.55s cubic-bezier(0.3, 1.2, 0.4, 1);
}
.chest__pct {
position: absolute;
inset: 0;
display: grid;
place-items: center;
font-family: var(--font-head);
font-weight: 800;
font-size: 0.82rem;
color: var(--ink);
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
}
/* ---------- task list ---------- */
.tasklist {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 10px;
}
.task {
position: relative;
}
.task__btn {
display: flex;
align-items: center;
gap: 14px;
width: 100%;
min-height: 56px;
padding: 10px 16px;
text-align: left;
border: var(--border);
border-radius: var(--r-sm);
background: #fbf9ff;
color: var(--ink);
font-weight: 700;
box-shadow: 0 4px 0 rgba(44, 35, 80, 0.14);
transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.2s ease;
}
.task__btn:hover {
transform: translateY(-2px);
background: #fff;
box-shadow: 0 6px 0 rgba(44, 35, 80, 0.16);
}
.task__btn:active {
transform: translateY(2px);
box-shadow: 0 2px 0 rgba(44, 35, 80, 0.16);
}
.task__check {
flex: 0 0 auto;
width: 30px;
height: 30px;
border-radius: 9px;
border: 3px solid var(--ink);
background: #fff;
display: grid;
place-items: center;
font-size: 1rem;
color: transparent;
transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease;
}
.task__emoji {
flex: 0 0 auto;
font-size: 1.5rem;
line-height: 1;
}
.task__label {
flex: 1 1 auto;
}
.task__reward {
flex: 0 0 auto;
font-size: 0.78rem;
font-weight: 800;
color: var(--primary);
font-family: var(--font-head);
}
.task.is-done .task__btn {
background: #effaf0;
border-color: var(--green);
}
.task.is-done .task__check {
background: var(--green);
border-color: var(--green);
color: #14532d;
transform: scale(1.05);
}
.task.is-done .task__label {
text-decoration: line-through;
color: var(--ink-soft);
}
.task.is-done .task__reward {
color: var(--green);
}
.tasks-card__foot {
margin: 16px 0 0;
padding: 10px 14px;
border-radius: var(--r-sm);
background: #fff6e3;
border: 2px dashed #ffd89b;
font-weight: 700;
font-size: 0.92rem;
color: var(--primary);
text-align: center;
}
/* ---------- big button ---------- */
.big-btn {
display: inline-flex;
align-items: center;
gap: 10px;
min-height: 54px;
padding: 0 26px;
border: var(--border);
border-radius: var(--r-pill);
background: var(--primary);
color: #fff;
font-family: var(--font-head);
font-weight: 800;
font-size: 1.05rem;
box-shadow: 0 6px 0 #c9621f;
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.big-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 0 #c9621f;
}
.big-btn:active {
transform: translateY(3px);
box-shadow: 0 3px 0 #c9621f;
}
/* ---------- celebration overlay ---------- */
.celebrate {
position: fixed;
inset: 0;
z-index: 70;
display: grid;
place-items: center;
padding: 20px;
background: rgba(44, 35, 80, 0.55);
backdrop-filter: blur(3px);
animation: fade-in 0.25s ease both;
}
.celebrate[hidden] {
display: none;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
.celebrate__card {
width: min(420px, 92vw);
text-align: center;
background: var(--card);
border: var(--border);
border-radius: 28px;
padding: 28px 26px 30px;
box-shadow: 0 22px 50px rgba(44, 35, 80, 0.35);
animation: card-pop 0.45s cubic-bezier(0.3, 1.4, 0.5, 1) both;
}
@keyframes card-pop {
0% { transform: scale(0.6) translateY(20px); opacity: 0; }
100% { transform: scale(1) translateY(0); opacity: 1; }
}
.celebrate__chest svg {
margin: 0 auto;
animation: bob 2.2s ease-in-out infinite;
}
.celebrate__title {
margin: 6px 0 6px;
font-family: var(--font-head);
font-weight: 800;
font-size: 1.7rem;
color: var(--primary);
}
.celebrate__text {
margin: 0 0 18px;
color: var(--ink-soft);
font-weight: 600;
}
/* ---------- fx layer ---------- */
.fx-layer {
position: fixed;
inset: 0;
z-index: 65;
pointer-events: none;
overflow: hidden;
}
.confetti {
position: absolute;
top: -20px;
width: 12px;
height: 16px;
border-radius: 3px;
will-change: transform, opacity;
animation: fall linear forwards;
}
@keyframes fall {
0% { transform: translateY(-10px) rotate(0); opacity: 1; }
100% { transform: translateY(108vh) rotate(720deg); opacity: 0.9; }
}
.flyer {
position: fixed;
z-index: 66;
width: 64px;
height: 64px;
pointer-events: none;
will-change: transform, opacity;
}
.flyer svg {
width: 100%;
height: 100%;
filter: drop-shadow(0 4px 6px rgba(44, 35, 80, 0.3));
}
/* ---------- toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translate(-50%, 140%);
z-index: 80;
max-width: 90vw;
padding: 12px 22px;
border-radius: var(--r-pill);
background: var(--ink);
color: #fff;
font-family: var(--font-head);
font-weight: 700;
box-shadow: 0 10px 24px rgba(44, 35, 80, 0.4);
transition: transform 0.32s cubic-bezier(0.3, 1.4, 0.5, 1);
}
.toast.is-show {
transform: translate(-50%, 0);
}
/* ---------- focus ---------- */
:focus-visible {
outline: none;
box-shadow: var(--ring);
border-radius: 12px;
}
.task__btn:focus-visible,
.big-btn:focus-visible,
.chip-btn:focus-visible {
box-shadow: var(--ring), 0 4px 0 rgba(44, 35, 80, 0.16);
}
/* ---------- responsive ---------- */
@media (max-width: 820px) {
.layout {
grid-template-columns: 1fr;
}
}
@media (max-width: 480px) {
body { font-size: 16px; }
.board {
grid-template-columns: repeat(3, 1fr);
}
.topbar__tools {
width: 100%;
}
.chip-btn {
flex: 1 1 auto;
justify-content: center;
}
.task__reward {
display: none;
}
}
@media (max-width: 360px) {
.board {
grid-template-columns: repeat(2, 1fr);
}
}
/* ---------- reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
.cloud { display: none; }
}(function () {
"use strict";
var STORAGE_KEY = "stealthis.stickerBoard.v1";
var TOTAL_SLOTS = 12;
var CHEST_GOAL = 6; // stickers per reward chest
/* ---------- sticker artwork (inline SVG, no external assets) ---------- */
// Each sticker is a small self-contained SVG drawing.
var STICKERS = {
star: {
label: "gold star",
svg:
'<svg viewBox="0 0 64 64" role="img" aria-label="Gold star sticker">' +
'<circle cx="32" cy="32" r="30" fill="#fff" stroke="#ffd23f" stroke-width="3"/>' +
'<path d="M32 12l5.6 11.4 12.6 1.8-9.1 8.9 2.1 12.5L32 41.2l-11.2 6 2.1-12.5-9.1-8.9 12.6-1.8z" fill="#ffd23f" stroke="#ff8a3d" stroke-width="2" stroke-linejoin="round"/>' +
'</svg>',
},
heart: {
label: "happy heart",
svg:
'<svg viewBox="0 0 64 64" role="img" aria-label="Heart sticker">' +
'<circle cx="32" cy="32" r="30" fill="#fff" stroke="#ff6f9c" stroke-width="3"/>' +
'<path d="M32 48C18 38 14 30 14 24a9 9 0 0 1 18-2 9 9 0 0 1 18 2c0 6-4 14-18 24z" fill="#ff6f9c"/>' +
'<circle cx="26" cy="26" r="3" fill="#fff" opacity="0.8"/>' +
"</svg>",
},
rocket: {
label: "zooming rocket",
svg:
'<svg viewBox="0 0 64 64" role="img" aria-label="Rocket sticker">' +
'<circle cx="32" cy="32" r="30" fill="#fff" stroke="#5ec5d6" stroke-width="3"/>' +
'<path d="M32 12c8 4 12 12 12 22l-6 5h-12l-6-5c0-10 4-18 12-22z" fill="#a98cff"/>' +
'<circle cx="32" cy="28" r="4.5" fill="#fff"/>' +
'<path d="M26 39l-5 8 6-2zM38 39l5 8-6-2z" fill="#ff8a3d"/>' +
'<path d="M28 45h8l-4 8z" fill="#ffd23f"/>' +
"</svg>",
},
rainbow: {
label: "bright rainbow",
svg:
'<svg viewBox="0 0 64 64" role="img" aria-label="Rainbow sticker">' +
'<circle cx="32" cy="32" r="30" fill="#fff" stroke="#7bd389" stroke-width="3"/>' +
'<path d="M14 44a18 18 0 0 1 36 0" fill="none" stroke="#ff6f9c" stroke-width="5"/>' +
'<path d="M19 44a13 13 0 0 1 26 0" fill="none" stroke="#ffd23f" stroke-width="5"/>' +
'<path d="M24 44a8 8 0 0 1 16 0" fill="none" stroke="#5ec5d6" stroke-width="5"/>' +
'<circle cx="16" cy="44" r="4" fill="#fff" stroke="#5ec5d6" stroke-width="2"/>' +
'<circle cx="48" cy="44" r="4" fill="#fff" stroke="#5ec5d6" stroke-width="2"/>' +
"</svg>",
},
butterfly: {
label: "fluttering butterfly",
svg:
'<svg viewBox="0 0 64 64" role="img" aria-label="Butterfly sticker">' +
'<circle cx="32" cy="32" r="30" fill="#fff" stroke="#a98cff" stroke-width="3"/>' +
'<ellipse cx="22" cy="26" rx="9" ry="11" fill="#ff6f9c"/>' +
'<ellipse cx="42" cy="26" rx="9" ry="11" fill="#5ec5d6"/>' +
'<ellipse cx="23" cy="40" rx="7" ry="8" fill="#ffd23f"/>' +
'<ellipse cx="41" cy="40" rx="7" ry="8" fill="#7bd389"/>' +
'<rect x="30.5" y="20" width="3" height="26" rx="1.5" fill="#2c2350"/>' +
'<path d="M32 20c-2-4-6-5-9-4M32 20c2-4 6-5 9-4" stroke="#2c2350" stroke-width="2" fill="none" stroke-linecap="round"/>' +
"</svg>",
},
sun: {
label: "smiling sun",
svg:
'<svg viewBox="0 0 64 64" role="img" aria-label="Sun sticker">' +
'<circle cx="32" cy="32" r="30" fill="#fff" stroke="#ff8a3d" stroke-width="3"/>' +
'<g stroke="#ff8a3d" stroke-width="3" stroke-linecap="round">' +
'<path d="M32 8v6M32 50v6M8 32h6M50 32h6M15 15l4 4M45 45l4 4M49 15l-4 4M19 45l-4 4"/></g>' +
'<circle cx="32" cy="32" r="14" fill="#ffd23f"/>' +
'<circle cx="27" cy="30" r="2" fill="#2c2350"/>' +
'<circle cx="37" cy="30" r="2" fill="#2c2350"/>' +
'<path d="M27 36q5 5 10 0" stroke="#2c2350" stroke-width="2.4" fill="none" stroke-linecap="round"/>' +
"</svg>",
},
crown: {
label: "shiny crown",
svg:
'<svg viewBox="0 0 64 64" role="img" aria-label="Crown sticker">' +
'<circle cx="32" cy="32" r="30" fill="#fff" stroke="#ffd23f" stroke-width="3"/>' +
'<path d="M16 42l-2-18 11 8 7-12 7 12 11-8-2 18z" fill="#ffd23f" stroke="#ff8a3d" stroke-width="2" stroke-linejoin="round"/>' +
'<rect x="16" y="42" width="32" height="6" rx="2" fill="#ff8a3d"/>' +
'<circle cx="23" cy="32" r="2.4" fill="#ff6f9c"/>' +
'<circle cx="32" cy="30" r="2.4" fill="#5ec5d6"/>' +
'<circle cx="41" cy="32" r="2.4" fill="#7bd389"/>' +
"</svg>",
},
flower: {
label: "cheerful flower",
svg:
'<svg viewBox="0 0 64 64" role="img" aria-label="Flower sticker">' +
'<circle cx="32" cy="32" r="30" fill="#fff" stroke="#ff6f9c" stroke-width="3"/>' +
'<g fill="#ff6f9c">' +
'<circle cx="32" cy="18" r="8"/><circle cx="32" cy="46" r="8"/>' +
'<circle cx="18" cy="32" r="8"/><circle cx="46" cy="32" r="8"/></g>' +
'<circle cx="32" cy="32" r="9" fill="#ffd23f"/>' +
"</svg>",
},
moon: {
label: "sleepy moon",
svg:
'<svg viewBox="0 0 64 64" role="img" aria-label="Moon sticker">' +
'<circle cx="32" cy="32" r="30" fill="#fff" stroke="#a98cff" stroke-width="3"/>' +
'<path d="M40 18a16 16 0 1 0 0 28 13 13 0 0 1 0-28z" fill="#a98cff"/>' +
'<circle cx="46" cy="22" r="2" fill="#ffd23f"/>' +
'<circle cx="50" cy="34" r="1.6" fill="#ffd23f"/>' +
'<circle cx="44" cy="44" r="1.6" fill="#ffd23f"/>' +
"</svg>",
},
cloud: {
label: "puffy cloud",
svg:
'<svg viewBox="0 0 64 64" role="img" aria-label="Cloud sticker">' +
'<circle cx="32" cy="32" r="30" fill="#fff" stroke="#5ec5d6" stroke-width="3"/>' +
'<path d="M22 42a8 8 0 0 1 0-16 11 11 0 0 1 21 2 7 7 0 0 1-1 14z" fill="#5ec5d6"/>' +
'<circle cx="26" cy="34" r="2.2" fill="#fff"/>' +
'<circle cx="34" cy="34" r="2.2" fill="#fff"/>' +
'<path d="M27 39q5 4 10 0" stroke="#fff" stroke-width="2" fill="none" stroke-linecap="round"/>' +
"</svg>",
},
fish: {
label: "bubbly fish",
svg:
'<svg viewBox="0 0 64 64" role="img" aria-label="Fish sticker">' +
'<circle cx="32" cy="32" r="30" fill="#fff" stroke="#7bd389" stroke-width="3"/>' +
'<path d="M14 32c6-10 26-10 32 0-6 10-26 10-32 0z" fill="#5ec5d6"/>' +
'<path d="M46 32l8-8v16z" fill="#7bd389"/>' +
'<circle cx="24" cy="30" r="2.6" fill="#fff"/>' +
'<circle cx="24" cy="30" r="1.2" fill="#2c2350"/>' +
'<circle cx="50" cy="22" r="2" fill="#5ec5d6"/>' +
"</svg>",
},
gift: {
label: "wrapped gift",
svg:
'<svg viewBox="0 0 64 64" role="img" aria-label="Gift sticker">' +
'<circle cx="32" cy="32" r="30" fill="#fff" stroke="#ff8a3d" stroke-width="3"/>' +
'<rect x="16" y="28" width="32" height="22" rx="3" fill="#ff6f9c"/>' +
'<rect x="16" y="22" width="32" height="8" rx="3" fill="#ff8a3d"/>' +
'<rect x="29" y="22" width="6" height="28" fill="#ffd23f"/>' +
'<path d="M32 22c-4-8-12-4-7 0M32 22c4-8 12-4 7 0" fill="#ffd23f"/>' +
"</svg>",
},
};
// The golden reward sticker is special and only earned by unlocking a chest.
var GOLD_STICKER = {
label: "golden trophy",
svg:
'<svg viewBox="0 0 64 64" role="img" aria-label="Golden trophy sticker">' +
'<circle cx="32" cy="32" r="30" fill="#fff7df" stroke="#ff8a3d" stroke-width="3"/>' +
'<path d="M20 14h24v8a12 12 0 0 1-24 0z" fill="#ffd23f" stroke="#ff8a3d" stroke-width="2"/>' +
'<path d="M20 16h-6a8 8 0 0 0 8 8M44 16h6a8 8 0 0 1-8 8" fill="none" stroke="#ff8a3d" stroke-width="3"/>' +
'<rect x="29" y="34" width="6" height="8" fill="#ff8a3d"/>' +
'<rect x="22" y="42" width="20" height="6" rx="2" fill="#ff8a3d"/>' +
'<path d="M32 17l1.6 3.4 3.4.5-2.6 2.4.7 3.5L32 25.5 28.9 27l.7-3.5-2.6-2.4 3.4-.5z" fill="#fff"/>' +
"</svg>",
};
/* ---------- tasks ---------- */
// id is stable so progress persists; sticker is the artwork popped when done.
var TASKS = [
{ id: "teeth", emoji: "🪥", label: "Brush my teeth", sticker: "star" },
{ id: "bed", emoji: "🛏️", label: "Make my bed", sticker: "cloud" },
{ id: "read", emoji: "📖", label: "Read a story", sticker: "moon" },
{ id: "veggies", emoji: "🥦", label: "Eat my veggies", sticker: "flower" },
{ id: "tidy", emoji: "🧸", label: "Tidy my toys", sticker: "heart" },
{ id: "kind", emoji: "🤝", label: "Be kind to a friend", sticker: "rainbow" },
{ id: "water", emoji: "🪴", label: "Water the plant", sticker: "butterfly" },
{ id: "exercise", emoji: "🤸", label: "Wiggle and stretch", sticker: "rocket" },
];
/* ---------- state ---------- */
var state = loadState();
function freshState() {
return {
done: {}, // taskId -> true
board: [], // array of sticker keys, in order earned
chestsUnlocked: 0,
};
}
function loadState() {
try {
var raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return freshState();
var parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== "object") return freshState();
return {
done: parsed.done && typeof parsed.done === "object" ? parsed.done : {},
board: Array.isArray(parsed.board) ? parsed.board : [],
chestsUnlocked:
typeof parsed.chestsUnlocked === "number" ? parsed.chestsUnlocked : 0,
};
} catch (e) {
return freshState();
}
}
function saveState() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
} catch (e) {
/* storage may be unavailable (private mode); demo still works in-memory */
}
}
/* ---------- DOM ---------- */
var $ = function (sel) {
return document.querySelector(sel);
};
var boardEl = $("#board");
var tasklistEl = $("#tasklist");
var earnedCountEl = $("#earned-count");
var slotTotalEl = $("#slot-total");
var chestFillEl = $("#chest-fill");
var chestPctEl = $("#chest-pct");
var chestBarEl = $("#chest-bar");
var chestHintEl = $("#chest-hint");
var chestIconEl = $("#chest-icon");
var footEl = $("#tasks-foot");
var fxLayer = $("#fx-layer");
var celebrateEl = $("#celebrate");
var celebrateText = $("#celebrate-text");
var celebrateClose = $("#celebrate-close");
var toastEl = $("#toast");
slotTotalEl.textContent = String(TOTAL_SLOTS);
chestBarEl.setAttribute("aria-valuemax", String(CHEST_GOAL));
/* ---------- toast helper ---------- */
var toastTimer = null;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-show");
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 2200);
}
var prefersReducedMotion =
window.matchMedia &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
/* ---------- build the empty board ---------- */
function buildBoard() {
boardEl.innerHTML = "";
for (var i = 0; i < TOTAL_SLOTS; i++) {
var li = document.createElement("li");
li.className = "slot";
li.setAttribute("data-index", String(i));
li.setAttribute("role", "listitem");
var ghost = document.createElement("span");
ghost.className = "slot__ghost";
ghost.textContent = "⭐";
ghost.setAttribute("aria-hidden", "true");
li.appendChild(ghost);
boardEl.appendChild(li);
}
}
function stickerData(key) {
if (key === "gold") return GOLD_STICKER;
return STICKERS[key] || STICKERS.star;
}
// Render a sticker into a slot. animate=true plays the bounce/pop.
function fillSlot(index, key, animate) {
var slot = boardEl.children[index];
if (!slot) return;
var data = stickerData(key);
slot.classList.add("is-filled");
if (key === "gold") slot.classList.add("is-gold");
slot.setAttribute("aria-label", "Earned " + data.label + " sticker");
var wrap = slot.querySelector(".slot__sticker");
if (!wrap) {
wrap = document.createElement("span");
wrap.className = "slot__sticker";
slot.appendChild(wrap);
}
wrap.innerHTML = data.svg;
if (animate && !prefersReducedMotion) {
slot.classList.remove("just-popped");
// force reflow so the animation can replay
void slot.offsetWidth;
slot.classList.add("just-popped");
}
}
// paint every already-earned sticker (on load) without animating
function renderEarnedStickers() {
for (var i = 0; i < state.board.length && i < TOTAL_SLOTS; i++) {
fillSlot(i, state.board[i], false);
}
}
/* ---------- task list ---------- */
function buildTasks() {
tasklistEl.innerHTML = "";
TASKS.forEach(function (task) {
var li = document.createElement("li");
li.className = "task";
li.setAttribute("data-id", task.id);
if (state.done[task.id]) li.classList.add("is-done");
var btn = document.createElement("button");
btn.type = "button";
btn.className = "task__btn";
btn.setAttribute("aria-pressed", state.done[task.id] ? "true" : "false");
var check = document.createElement("span");
check.className = "task__check";
check.setAttribute("aria-hidden", "true");
check.textContent = "✓";
var emoji = document.createElement("span");
emoji.className = "task__emoji";
emoji.setAttribute("aria-hidden", "true");
emoji.textContent = task.emoji;
var label = document.createElement("span");
label.className = "task__label";
label.textContent = task.label;
var reward = document.createElement("span");
reward.className = "task__reward";
reward.textContent = "+1 ⭐";
btn.appendChild(check);
btn.appendChild(emoji);
btn.appendChild(label);
btn.appendChild(reward);
li.appendChild(btn);
tasklistEl.appendChild(li);
btn.addEventListener("click", function () {
onTaskClick(task, li, btn, emoji);
});
});
}
function onTaskClick(task, li, btn, emojiEl) {
if (state.done[task.id]) {
// un-check: remove the last matching sticker for this task
uncompleteTask(task, li, btn);
return;
}
if (state.board.length >= TOTAL_SLOTS) {
toast("Your board is full! Press Start over for a new one. 🎉");
return;
}
state.done[task.id] = true;
li.classList.add("is-done");
btn.setAttribute("aria-pressed", "true");
var index = state.board.length;
state.board.push(task.sticker);
saveState();
flySticker(emojiEl, index, task.sticker);
updateProgress();
footEl.textContent = "Great job! You earned a " + stickerData(task.sticker).label + ". ⭐";
}
function uncompleteTask(task, li, btn) {
// find the most recent board sticker matching this task's sticker key
var idx = -1;
for (var i = state.board.length - 1; i >= 0; i--) {
if (state.board[i] === task.sticker) {
idx = i;
break;
}
}
state.done[task.id] = false;
li.classList.remove("is-done");
btn.setAttribute("aria-pressed", "false");
if (idx !== -1) state.board.splice(idx, 1);
saveState();
// rebuild the board cleanly so indices stay correct
buildBoard();
renderEarnedStickers();
updateProgress();
footEl.textContent = "Took a sticker back. You can earn it again any time!";
}
/* ---------- flying sticker animation ---------- */
function flySticker(fromEl, slotIndex, key) {
var targetSlot = boardEl.children[slotIndex];
if (prefersReducedMotion || !fromEl || !targetSlot) {
fillSlot(slotIndex, key, true);
afterFill(slotIndex);
return;
}
var from = fromEl.getBoundingClientRect();
var to = targetSlot.getBoundingClientRect();
var flyer = document.createElement("div");
flyer.className = "flyer";
flyer.innerHTML = stickerData(key).svg;
flyer.style.left = from.left + from.width / 2 - 32 + "px";
flyer.style.top = from.top + from.height / 2 - 32 + "px";
fxLayer.appendChild(flyer);
var dx = to.left + to.width / 2 - (from.left + from.width / 2);
var dy = to.top + to.height / 2 - (from.top + from.height / 2);
var anim = flyer.animate(
[
{ transform: "translate(0,0) scale(0.7) rotate(-15deg)", opacity: 0.9 },
{ transform: "translate(" + dx * 0.5 + "px," + (dy * 0.5 - 40) + "px) scale(1.3) rotate(10deg)", opacity: 1, offset: 0.6 },
{ transform: "translate(" + dx + "px," + dy + "px) scale(0.8) rotate(0deg)", opacity: 1 },
],
{ duration: 620, easing: "cubic-bezier(0.3,1.1,0.4,1)", fill: "forwards" }
);
anim.onfinish = function () {
flyer.remove();
fillSlot(slotIndex, key, true);
afterFill(slotIndex);
};
// safety fallback if Web Animations API is unavailable
if (!flyer.animate) {
flyer.remove();
fillSlot(slotIndex, key, true);
afterFill(slotIndex);
}
}
function afterFill(slotIndex) {
// count how many stickers earned so far toward a fresh chest
var earnedTotal = state.board.length;
var chestsEarned = Math.floor(earnedTotal / CHEST_GOAL);
if (chestsEarned > state.chestsUnlocked) {
state.chestsUnlocked = chestsEarned;
saveState();
unlockChest();
}
}
/* ---------- progress to next chest ---------- */
function updateProgress() {
var earned = state.board.length;
earnedCountEl.textContent = String(earned);
var into = earned % CHEST_GOAL;
// when the board total is a clean multiple, the bar should read full just before reset
var shownInto = into === 0 && earned > 0 ? CHEST_GOAL : into;
var pct = Math.round((shownInto / CHEST_GOAL) * 100);
chestFillEl.style.width = pct + "%";
chestPctEl.textContent = pct + "%";
chestBarEl.setAttribute("aria-valuenow", String(shownInto));
var remaining = CHEST_GOAL - shownInto;
if (earned >= TOTAL_SLOTS) {
chestHintEl.textContent = "Board complete — amazing!";
chestIconEl.textContent = "🏆";
chestIconEl.classList.remove("is-ready");
} else if (remaining <= 0) {
chestHintEl.textContent = "Chest ready to open!";
chestIconEl.textContent = "🎉";
chestIconEl.classList.add("is-ready");
} else {
chestHintEl.textContent =
"Earn " + remaining + (remaining === 1 ? " sticker" : " stickers") + " to unlock";
chestIconEl.textContent = "🎁";
chestIconEl.classList.remove("is-ready");
}
}
/* ---------- chest unlock + celebration ---------- */
function unlockChest() {
confettiBurst();
// award a special golden sticker if there's room
if (state.board.length < TOTAL_SLOTS) {
var goldIndex = state.board.length;
state.board.push("gold");
saveState();
fillSlot(goldIndex, "gold", true);
}
updateProgress();
var unlocked = state.chestsUnlocked;
celebrateText.textContent =
"You filled reward chest #" +
unlocked +
"! Here is a shiny golden trophy sticker for your board. Keep collecting!";
openCelebrate();
}
function openCelebrate() {
celebrateEl.hidden = false;
celebrateClose.focus();
document.addEventListener("keydown", onCelebrateKey);
}
function closeCelebrate() {
celebrateEl.hidden = true;
document.removeEventListener("keydown", onCelebrateKey);
toast("Chest claimed! 🌟");
}
function onCelebrateKey(e) {
if (e.key === "Escape") closeCelebrate();
// simple focus trap: keep focus on the close button
if (e.key === "Tab") {
e.preventDefault();
celebrateClose.focus();
}
}
celebrateClose.addEventListener("click", closeCelebrate);
celebrateEl.addEventListener("click", function (e) {
if (e.target === celebrateEl) closeCelebrate();
});
/* ---------- confetti ---------- */
function confettiBurst() {
if (prefersReducedMotion) return;
var colors = ["#ff8a3d", "#5ec5d6", "#ffd23f", "#ff6f9c", "#7bd389", "#a98cff"];
var count = 70;
for (var i = 0; i < count; i++) {
var c = document.createElement("span");
c.className = "confetti";
c.style.left = Math.random() * 100 + "vw";
c.style.background = colors[i % colors.length];
c.style.animationDuration = 2.4 + Math.random() * 1.8 + "s";
c.style.animationDelay = Math.random() * 0.4 + "s";
c.style.transform = "rotate(" + Math.random() * 360 + "deg)";
if (Math.random() > 0.5) c.style.borderRadius = "50%";
fxLayer.appendChild(c);
(function (node) {
setTimeout(function () {
node.remove();
}, 4600);
})(c);
}
}
/* ---------- easy-read toggle ---------- */
var fontToggle = $("#font-toggle");
fontToggle.addEventListener("click", function () {
var on = document.body.classList.toggle("easy-read");
fontToggle.setAttribute("aria-pressed", on ? "true" : "false");
toast(on ? "Easy-read font on 🔤" : "Easy-read font off");
});
/* ---------- reset ---------- */
var resetBtn = $("#reset-board");
resetBtn.addEventListener("click", function () {
state = freshState();
saveState();
buildBoard();
buildTasks();
updateProgress();
footEl.textContent = "Fresh board! Finish a task to earn your first sticker.";
toast("New empty board ready! ✨");
});
/* ---------- init ---------- */
function init() {
buildBoard();
renderEarnedStickers();
buildTasks();
updateProgress();
if (state.board.length > 0) {
footEl.textContent =
"Welcome back! You have " +
state.board.length +
(state.board.length === 1 ? " sticker" : " stickers") +
" so far. Keep going!";
}
}
init();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Storybook — Sticker Reward Board</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="#board-main">Skip to the sticker board</a>
<div class="sky" aria-hidden="true">
<span class="cloud cloud--1"></span>
<span class="cloud cloud--2"></span>
<span class="cloud cloud--3"></span>
<span class="sparkle sparkle--1">✦</span>
<span class="sparkle sparkle--2">✧</span>
<span class="sparkle sparkle--3">✦</span>
</div>
<main id="board-main" class="app" role="main">
<header class="topbar">
<div class="brand">
<span class="brand__mark" aria-hidden="true">
<svg viewBox="0 0 48 48" width="46" height="46" role="img" aria-label="">
<rect x="3" y="3" width="42" height="42" rx="12" fill="#5ec5d6" />
<path d="M24 9l4.2 8.5 9.4 1.4-6.8 6.6 1.6 9.3L24 30.9l-8.4 4.4 1.6-9.3-6.8-6.6 9.4-1.4z" fill="#ffd23f" stroke="#ff8a3d" stroke-width="1.6" stroke-linejoin="round" />
</svg>
</span>
<div class="brand__text">
<p class="brand__kicker">Storybook Rewards</p>
<h1 class="brand__title">Sticker Adventure</h1>
</div>
</div>
<div class="topbar__tools">
<button id="font-toggle" class="chip-btn" type="button" aria-pressed="false">
<span aria-hidden="true">🔤</span>
<span class="chip-btn__label">Easy-read</span>
</button>
<button id="reset-board" class="chip-btn" type="button">
<span aria-hidden="true">↺</span>
<span class="chip-btn__label">Start over</span>
</button>
</div>
</header>
<div class="layout">
<!-- STICKER BOARD -->
<section class="board-card" aria-labelledby="board-heading">
<div class="board-card__head">
<h2 id="board-heading" class="board-card__title">My Sticker Board</h2>
<p class="board-card__count">
<strong id="earned-count">0</strong> / <span id="slot-total">12</span> stickers
</p>
</div>
<ul class="board" id="board" role="list" aria-label="Sticker board, empty slots fill as you finish tasks"></ul>
<!-- Reward chest progress -->
<section class="chest" aria-labelledby="chest-heading">
<div class="chest__top">
<h3 id="chest-heading" class="chest__title">
<span class="chest__icon" id="chest-icon" aria-hidden="true">🎁</span>
Next reward chest
</h3>
<p class="chest__hint" id="chest-hint">Earn 6 stickers to unlock</p>
</div>
<div
class="chest__bar"
role="progressbar"
id="chest-bar"
aria-valuemin="0"
aria-valuemax="6"
aria-valuenow="0"
aria-label="Progress to the next reward chest"
>
<span class="chest__fill" id="chest-fill"></span>
<span class="chest__pct" id="chest-pct">0%</span>
</div>
</section>
</section>
<!-- TASKS TODAY -->
<section class="tasks-card" aria-labelledby="tasks-heading">
<div class="tasks-card__head">
<h2 id="tasks-heading" class="tasks-card__title">Tasks Today</h2>
<p class="tasks-card__sub">Check a task to pop a sticker on your board!</p>
</div>
<ul class="tasklist" id="tasklist" role="list"></ul>
<p class="tasks-card__foot" id="tasks-foot" role="status" aria-live="polite">
Tap a task when you finish it.
</p>
</section>
</div>
</main>
<!-- celebration overlay -->
<div class="celebrate" id="celebrate" role="dialog" aria-modal="true" aria-labelledby="celebrate-title" hidden>
<div class="celebrate__card">
<div class="celebrate__chest" aria-hidden="true">
<svg viewBox="0 0 120 100" width="150" height="125" role="img" aria-label="">
<ellipse cx="60" cy="90" rx="46" ry="8" fill="#000" opacity="0.08" />
<path d="M16 44 Q60 14 104 44 L104 50 L16 50 Z" fill="#ffb24d" stroke="#b5651d" stroke-width="3" />
<rect x="16" y="48" width="88" height="40" rx="6" fill="#ffd23f" stroke="#b5651d" stroke-width="3" />
<rect x="14" y="44" width="92" height="10" rx="4" fill="#ff8a3d" stroke="#b5651d" stroke-width="3" />
<rect x="52" y="54" width="16" height="22" rx="3" fill="#b5651d" />
<circle cx="60" cy="62" r="4" fill="#ffd23f" />
<path d="M60 30 l3 7 7 1 -5 5 1.2 7 -6.2-3.4 -6.2 3.4 1.2-7 -5-5 7-1z" fill="#fff" opacity="0.9" />
</svg>
</div>
<h2 id="celebrate-title" class="celebrate__title">Reward Unlocked!</h2>
<p class="celebrate__text" id="celebrate-text">You filled the chest. Here is a special golden sticker!</p>
<button id="celebrate-close" class="big-btn" type="button">
<span aria-hidden="true">🌟</span> Keep going
</button>
</div>
</div>
<!-- flying-sticker + confetti layer -->
<div class="fx-layer" id="fx-layer" aria-hidden="true"></div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Sticker Reward Board
A cheerful reward board for little ones, built in the Storybook design system with soft bright colors, thick playful borders, and rounded-everything shapes. A grid of twelve dashed, candy-striped slots waits to be filled, and a Tasks Today checklist sits alongside it with friendly chores like Brush my teeth, Read a story, and Be kind to a friend. Every sticker — stars, rainbows, butterflies, crowns, and a golden trophy — is hand-drawn as inline SVG, so there are no external images at all.
Tap a task and its emoji literally flies across the screen into the next open slot, where a collectible sticker pops on with a springy bounce. A progress bar fills toward the next reward chest; reach the goal and confetti rains down while a celebration dialog awards a shiny golden trophy sticker for the board. Tapping a finished task again gently takes its sticker back, the chest icon wiggles when it is ready to open, and a toast() helper cheers each milestone.
Everything is keyboard friendly with focus-visible rings, the celebration dialog traps focus and closes on Escape, and reduced-motion preferences quiet the flying stickers and confetti. An easy-read toggle swaps in a dyslexia-friendly font with looser spacing, the layout collapses from two columns to one and reflows the sticker grid down to ~360px, and earned stickers, checked tasks, and unlocked chests all persist to localStorage so progress is waiting on the next visit.
Illustrative kids’ UI only — fictional stories, characters, and audio.