← recess the guide · how it was built
Design showcase · build notes

How Recess bounces.

Recess is the one site in the Parable showcase built on something else entirely — SvelteKit — because its whole personality is motion, and Svelte's built-in spring physics is the cleanest way to make a page feel alive and bouncy.

Fredoka + Nunito SvelteKit adapter-static spring() & tweened()

The idea

A learning app for four-to-eight-year-olds has to earn one feeling before it earns a single word of copy: delight. Kids trust things that are round, soft, and responsive — things that squash when you press them and bounce when you win. So the brief was never "explain the curriculum." It was "make the page itself feel like a toy," and then let the honest, evidence-flavoured pitch live quietly in the parents' corner. Two audiences, two voices, one bouncy world.

The stack — why SvelteKit

Signature technique #1 — a spring-driven mascot

Pip is authored entirely in SVG — a circle body, two big eyes, little limbs. What makes him feel alive is that his pupils and whole body are driven by springs that chase the cursor. You never set a position directly; you set a target, and the spring eases there with a natural overshoot:

import { spring } from 'svelte/motion';

// two springs give Pip his life: the pupils and a whole-body lean
const pupil = spring({ x: 0, y: 0 }, { stiffness: 0.12, damping: 0.5 });
const lean  = spring({ x: 0, y: 0, r: 0 }, { stiffness: 0.08, damping: 0.55 });

function onMove(e) {
  const r  = host.getBoundingClientRect();
  const dx = (e.clientX - (r.left + r.width  / 2)) / (innerWidth  / 2);
  const dy = (e.clientY - (r.top  + r.height * 0.46)) / (innerHeight / 2);
  pupil.set({ x: clamp(dx) * 8, y: clamp(dy) * 6 });  // eyes chase the cursor
  lean.set({  x: clamp(dx) * 6, y: clamp(dy) * 3, r: clamp(dx) * 3 });
}

He blinks on a random timer, glances around when the pointer goes still, and — the payoff — does a happy-bounce when you answer the mini-lesson correctly. Every one of those springs checks the user's motion preference first:

// every spring in the site respects "reduce motion" by jumping instantly
export function springTo(store, value) {
  return store.set(value, prefersReduced() ? { hard: true } : {});
}

// Pip's happy-bounce, called by the mini-lesson on a correct answer
const hop = spring(0, { stiffness: 0.11, damping: 0.28 });
export async function celebrate() {
  if (prefersReduced()) return;   // stay calm
  await hop.set(1);               // spring up — overshoots, wobbles
  hop.set(0);                     // …then settles back down
}

Signature technique #2 — the winding lesson path

Straight from the Duolingo-ABC playbook: a dotted SVG trail snakes down the page with lesson nodes on it — done (a green ✓), current (a pulsing yellow star with Pip's "Start here" bubble), and locked (a padlock). Each node is its own component driven by a tweened store with a backOut easing — the same bouncy overshoot a spring gives, but time-based, so a throttled background tab can never make it diverge. A scroll trigger flips the nodes on with a staggered delay so they pop into place one after another. The decorative SVG is mirrored by a visually-hidden ordered list, so a screen reader hears the journey as real content.

Signature technique #3 — a playable mini-lesson

"Count the apples" is a real, working lesson embedded in the marketing page. Tap the right number and you get a canvas confetti burst, Pip's happy-bounce, and a warm word; tap a wrong one and the button gives a gentle wobble and a "count again, you've got this" — never a buzzer, never a loss. The choices are real <button> elements: fully keyboard-operable, and every result is announced through an aria-live region for screen-reader users.

Details that matter

Ship it on GitHub Pages

The trick with a framework site on project Pages is two settings: send the build into /docs, and prefix every path with the repo name. adapter-static does the first; paths.base does the second (and dev keeps it empty so localhost still works):

import adapter from '@sveltejs/adapter-static';
const dev = process.argv.includes('dev');

export default {
  kit: {
    // write the prerendered site straight into /docs for GitHub Pages
    adapter: adapter({ pages: 'docs', assets: 'docs', fallback: undefined, strict: true }),
    // every asset + link is prefixed with the repo name in production
    paths: { base: dev ? '' : '/recess-learn' }
  }
};

After that it's build, commit the docs/ folder, and point Pages at it:

npm run build             # → docs/index.html + docs/guide/index.html
git add -A && git commit -m "Recess — kids' learning app"
gh repo create bswxyz/recess-learn --public --source . --push
# GitHub → Settings → Pages → Branch: main · Folder: /docs
# live at https://bswxyz.github.io/recess-learn/

A static/.nojekyll file rides along into docs/ so Pages serves SvelteKit's _app/ folder untouched. That's the whole deploy.


Recess is a design-showcase concept — there's no real curriculum, accounts, or app behind it. See the repository README for the full demo-vs-real map.

← Back to Recess