1 · The concept
Children's museum sites usually fail in one of two directions: clip-art chaos (kids love it, parents don't trust it with their credit card) or beige institutional calm (parents trust it, kids never asked to go). Curio's brief was to hold both: joyful and bouncy on the surface, strict grid and real information architecture underneath.
- For kids: saturated colors, chunky toy-like type, shapes you can literally grab and fling, cartoon squash-and-stretch on every hover.
- For parents: a clean "grown-up details" block (hours / tickets / parking / sensory info), honest prices, an access-admission line, AA-contrast text everywhere.
The headline — "Go ahead, touch everything." — is the whole positioning in four words: it inverts the classic museum rule, and it doubles as an instruction for the page itself (the hero shapes are touchable).
2 · Palette
Four saturated hues on warm paper-white, harmonized by giving each one a darkened "text-safe" twin. The bright versions are for fills and decoration; the deep versions are for anything carrying text.
#FFF7EC paper
#22254A ink
#FFC531 sunshine
#2745E6 cobalt
#FF6957 coral
#2EA35C grass
#1B2050 navy
The contrast rules that keep it credible
- Yellow and coral and grass green never carry white body text. Yellow gets ink text; coral and green step down to
#C0301Fand#1B7A43when white text sits on them (both clear 4.5:1). - Cobalt
#2745E6is the one bright that passes AA with white (≈6.9:1), so it's the primary button color. - Every section alternates background temperature — paper → white → paper → yellow → navy — so the page reads as chapters, not confetti.
3 · Type
- Display: Baloo 2 (700/800) — chunky, rounded, almost toy-like, but a real text face with proper spacing. It does headlines, buttons, prices, and chips.
- Body: Nunito Sans (400–800) — a clean humanist sans whose slightly rounded terminals shake hands with Baloo instead of fighting it. All parent-info runs in it at 600 weight for warmth.
Rule of thumb: if a kid would read it out loud, it's Baloo. If a parent needs to trust it, it's Nunito Sans.
4 · The spring-physics system
All motion on the page comes from one idea: everything behaves like it has mass and rubber in it. Three techniques deliver that.
4a · Poke-able hero shapes (hand-rolled spring integrator)
Each floating SVG shape stores a home position (percentages of the hero stage), a live position, and a velocity. A single requestAnimationFrame loop runs a damped spring toward home plus a pointer-repel force:
// per frame, per shape
vx += (homeX - x) * K; // spring pull (K ≈ 0.045)
vy += (homeY - y) * K;
if (pointerNear) { // repel inside a 90px radius
const f = (1 - d / RADIUS) * PUSH;
vx += (dx / d) * f * 6; vy += (dy / d) * f * 6;
}
vx *= DAMP; vy *= DAMP; // damping ≈ 0.86 → bouncy, settles in ~1s
x += vx; y += vy;
Squash-and-stretch falls out of the same numbers for free: scale the shape by its speed —
const speed = Math.min(14, Math.hypot(vx, vy));
const stretch = 1 + speed * 0.014 + squishPulse * 0.18;
const squash = 1 - speed * 0.012 - squishPulse * 0.12;
el.style.transform =
`translate(${x}px,${y}px) translate(-50%,-50%)
rotate(${tilt + vx * 0.6}deg) scale(${stretch},${squash})`;
Dragging uses Pointer Events with setPointerCapture; on release the shape keeps its throw velocity, so kids can fling it and watch it boing back. Tuning notes: K up = snappier, DAMP down = wobblier. 0.045 / 0.86 lands on "excited puppy."
4b · Squash-and-stretch hovers (GSAP keyframes)
Buttons don't scale uniformly — they squash like cartoon rubber. A three-step GSAP keyframe sequence with an elastic settle:
gsap.fromTo(btn, {scaleX:1, scaleY:1}, { keyframes: [
{scaleX:1.08, scaleY:0.90, duration:0.12, ease:"power2.out"}, // squash
{scaleX:0.96, scaleY:1.05, duration:0.14}, // overshoot
{scaleX:1.00, scaleY:1.00, duration:0.5,
ease:"elastic.out(1.2, 0.4)"} // settle
]});
The trick that sells it: transform-origin: 50% 100% so buttons squash down onto their shadow like they're pressing on a table. Cards get the same idea but with a random ±1.2° wobble so no two hovers feel identical.
4c · Springy scroll reveals
GSAP ScrollTrigger with back.out(1.5–1.7) for text (one confident overshoot) and elastic.out(1, 0.55) for photos and stickers (a proper boing). Card grids stagger 0.11s with a per-card random rotation, so a row of cards lands like tossed playing cards, not a marching band.
4d · The wavy marquee
The scrolling ticker splits every headline into per-character spans, each with a negative animation-delay offset into a shared sine-bob keyframe — the phase shift across characters is what makes the strip read as a traveling wave. Horizontal motion is a rAF translate that wraps at one sequence-width (three copies of the sequence = seamless loop). The whole bar is rotated −1.3° because straight things aren't fun.
Accessibility: everything above checks prefers-reduced-motion first — reduced-motion users get static shapes at their home positions and instant reveals.
5 · Imagery — art direction
Seven photos generated with Higgsfield (soul_2 model, 2K). Each one came from a carefully art-directed brief — lighting, lens, palette words, composition and mood specified per scene, with the same bright, joyful documentary grade repeated across all seven so the photos agree with the CSS — and iterated until every frame felt like the same museum on the same sunny day. The full prompt library is proprietary to the studio; it's available to clients on request.
Post-processing: everything resized to ≤1600px wide and recompressed to JPEG quality ~78 with ffmpeg — the whole image folder stays well under a megabyte per photo.
6 · Layout discipline (the invisible part)
- One container:
width: min(1180px, 100% - 48px)everywhere. Bleed elements (marquee, doodles) escape it; content never does. overflow-x: cliponhtmlandbodyso the rotated marquee and off-canvas doodles can't cause horizontal scroll.- Rotation is used constantly but never exceeds ~6° on content and 2° on photos — past that, playful turns to sloppy.
- The hand-drawn SVG doodles (squiggles, stars, orbit lines) are inline, stroke-based with
stroke-linecap: round, and alwayspointer-events: none+aria-hidden. - Breakpoints at 1020 / 920 / 880 / 760 / 560px; the four-card visit grid steps 4 → 2 → 1.
7 · Deploying to Netlify
The site is a static folder — no build step — so deployment is one command:
# one-time
npm i -g netlify-cli
netlify login
# from the curio-museum/ folder
netlify deploy --dir . --prod
Or skip the CLI entirely: drag the curio-museum folder onto app.netlify.com/drop. Because fonts come from Google Fonts and GSAP from jsDelivr, there's nothing else to configure. If you add a custom domain, turn on Netlify's asset optimization but exclude JS minification — it's already minimal, and the physics loop is easier to debug unminified.