Wobble Toy Co. ← back to the toyshop

How do you make a website wobble?

Concept, code and craft notes for Wobble Toy Co. — site 11 of 15 in an exhibition of websites designed and built autonomously by Claude Fable 5. The shelf below is live, by the way.

01The concept

Wobble Toy Co. is a fictional toy company with one rule: the website is itself the toy. Most sites describe a product; this one hands it to you. A single Matter.js physics engine governs the whole page — the hero rains toys into a pile, the logo is a rigid body you can fling across the screen, the navigation hangs from springs and falls when you use it, and the playground is a sandbox with a gravity slider and an earthquake button.

The creative direction is Pixar-short energy with real physics. Everything is chunky, rounded and outlined like vinyl toys. What separates it from a physics demo is animation craft: every collision drives squash-and-stretch scaled by impact velocity, so the toys read as soft, eager things rather than rigid bodies. The one deliberately static section — the grown-ups' corner — is the punchline: "This section does not bounce."

Under prefers-reduced-motion, the engine never starts: the pile renders pre-settled, the nav docks into the header, and every piece of content stays reachable.

02Palette & type

milk
#FFF8EC
sunshine
#FFD23F
bubblegum
#FF5D8F
grape
#7B4DFF
aqua
#00D4C0
ink
#33255B

Milk is the ground; the four toy colors are used at full saturation with darkened same-hue outlines (25–30% deeper), which is what gives the shapes their die-cut vinyl look. Ink is a grape-tinted near-black so even the neutrals feel like the brand.

Fredoka — display, variable 300–700. Round terminals, chunky counters: the typographic equivalent of a foam ball.

Nunito — body, 400–900. Rounded but disciplined; set at 17px/1.6 with weight 600+ so long copy keeps the toy-store warmth without going mushy.

03Signature technique: physics as interface

One engine, many islands. The page runs a single Matter.js engine. Each interactive section (hero, three product vignettes, playground, order panel) is a zone: a coordinate island offset 50,000px from its neighbors in the shared world, with its own walls matched to its DOM container. Zones detach their composite from the world when they scroll offscreen, idle bodies sleep, and a global cap (~80 dynamic bodies) keeps the frame budget honest.

DOM as renderer. There is no canvas. Every body is an absolutely-positioned element receiving exactly one transform write per frame — translate, rotate, and the squash. Sleeping bodies skip the write entirely.

The Pixar sauce. On every collision, the impact speed (relative velocity projected on the contact normal) becomes a squash amount, and the normal's angle becomes the squash axis. GSAP animates the recovery with an elastic ease, and the render loop composes it into the same transform — so toys flatten against the surface they hit and jiggle back:

Events.on(engine, 'collisionStart', (ev) => {
  for (const pair of ev.pairs) {
    const { bodyA, bodyB, collision } = pair
    // impact = relative velocity projected on the contact normal
    const rel = Vector.sub(bodyA.velocity, bodyB.velocity)
    const impact = Math.abs(Vector.dot(rel, collision.normal))
    if (impact < 1.4) continue
    const th = Math.atan2(collision.normal.y, collision.normal.x)
    squashBody(bodyA, impact, th)
    squashBody(bodyB, impact, th)
  }
})

function squashBody(body, impact, th) {
  const toy = body.plugin?.toy
  if (!toy || toy.noSquash) return
  const amount = Math.min(0.09 + impact * 0.024, 0.42)
  toy.sq.th = th
  toy.sq.a = amount   // flatten instantly on contact…
  gsap.to(toy.sq, {   // …and jiggle back elastically
    a: 0, duration: 0.62,
    ease: 'elastic.out(1.1, 0.28)', overwrite: true,
  })
}

// per frame, composed into ONE transform write:
// translate3d(x, y, 0)
//   rotate(th) scale(1 − a, 1 + a·0.65) rotate(−th)  ← squash, world-aligned
//   rotate(bodyAngle)                                 ← physics rotation

Navigation is bodies too. Each menu pill is a rectangle hung from a soft Constraint (the dotted string is an SVG line redrawn each frame from the body's rotated attachment point). A click — defined as a press under 350 ms that moved less than 10 px, so dragging still works — removes the constraint. The pill tumbles into the pile, and the page scroll-tweens after it. Blobby, the purple blob, is an honest soft body: a ring of ten particles laced with constraints, skinned with a closed Catmull-Rom spline rendered as one SVG path.

04Asset pipeline

Zero AI-generated images. Zero raster assets in the design. Per the brief, every toy is procedural SVG built by a factory function (src/toys.js) — five species (ball, star, block, ring, spring) × four brand colors × optional kawaii face, each with a physics hull matched to its silhouette. This keeps every shape crisp at any size and means a "product photo" can also be a rigid body.

  • Fonts — Fredoka (variable 300–700) and Nunito, self-hosted woff2 via the repo's getfont tool.
  • Favicon — hand-written SVG: sunshine star, ink face, grape ground.
  • OG image — the only raster file on the site: a staged frame of the real hero pile, captured from the built site with Playwright and compressed to JPEG.

05Iteration diary

Pass 1 — structure. The first screenshots caught three honest physics bugs: falling toys were landing on top of the hanging nav pills and piling up at the top of the phone screen (fixed with collision groups — toys and pills no longer collide until a pill is detached); reveal animations never fired for anyone who jump-scrolled (fixed with a scroll fallback alongside the IntersectionObserver); and the "page is ready" signal fired on a timer rather than when the pile had physically settled (now it polls until every body is asleep). The logo also learned it could balance upright on its end — its hull is now a full capsule, so it can't.

Pass 2 — craft. Wrote an interaction harness that drives the real site: drag-and-fling, pill-click navigation, spawning, zero-gravity + shake, the order easter egg. It caught The Wobbler rendering half-sunk into its floor — Matter positions compound bodies by center of mass, and the renderer drew the element there instead of at the visual center (fixed with a rotating render offset). Added the one-time camera thud when the logo lands, and jelly hovers on every chunky button.

Pass 3 — hostile critic. Contrast audit: white-on-bubblegum failed WCAG at small sizes, so pill and button text on pink switched to ink (4.7:1). The weakest screen — the order panel's sparse right column — got a proper cardboard crate illustration. Two details for the people who linger: tap any empty patch of hero sky and a new toy drops where you tapped, and the grape words in the headline squash letter-by-letter under your cursor. Silenced Matter's frame-delta warning by clamping the engine step to 16.7 ms.

06Credits

Designed & built autonomously by Claude Fable 5 (Anthropic) — concept, copy, design system, physics, and QA.

Play with the site → See all fifteen →

Live at wobble-toy-co.pages.dev · one site of fifteen at fable-fifteen.pages.dev