The concept
Pillow is a fictional bedtime-stories app for kids aged 3β8, with sleepy tales read by four soft characters: Miso the cloud-cat, Bruno the moon-bear, Pip the star-bunny and Luna the pillow-owl.
The visual language is claymorphism β the design school where every element looks like it was pinched out of soft 3D clay or marshmallow. Its tells: very large border radii (30px and up), a specific three-part shadow that fakes volume, a pastel palette on a warm cream base, and chunky rounded type (here, Baloo 2). Where neumorphism whispers, claymorphism giggles β it is the perfect register for a product whose whole job is being squeezable and calm at the same time.
Six values, no exceptions. Deep plum does all the reading; the pastels do all the playing.
The techniques
The clay shadow recipe
One box-shadow with three layers does all the heavy lifting. An outer drop shadow lifts the shape off the page; an inset light from the top-left fakes a soft highlight; an inset dark from the bottom-right fakes thickness. Tint both insets with the palette's plum so the "clay" feels dyed, not gray.
.clay {
box-shadow:
0 22px 36px -16px rgba(74,63,102,.28), /* outer drop */
inset 6px 8px 14px rgba(255,255,255,.65), /* top-left light */
inset -8px -12px 18px rgba(74,63,102,.14); /* bot-right dark */
}
The same recipe at smaller offsets (.clay-sm, .clay-part) scales it down to pills and character body parts, so every element β from the nav to Bruno's ears β reads as one material.
The spring math (squish physics)
Every button and character card is driven by a hand-rolled damped spring in a requestAnimationFrame loop β no library, about fifteen lines. One number p tracks "how squashed": pressing sets the target to 1 with heavy damping (a firm, dough-like press), releasing sets it to 0 with low damping, so the value overshoots past rest into negative territory β which renders as a stretch. Squash-and-stretch, the first law of animation, for free.
const k = 340; // stiffness
const c = pressed ? 30 : 13; // damping: firm in, bouncy out
const a = -k * (p - target) - c * v; // Hooke's law + friction
v += a * dt;
p += v * dt;
el.style.transform =
`scale(${1 + 0.16 * p}, ${1 - 0.22 * p})`; // squashβ¦and stretch
Two details make it feel physical: transform-origin: 50% 100% so things squash into the ground rather than into their own middle, and clamping dt so a background tab can't explode the spring. Try it:
Characters from pure CSS
Miso, Bruno, Pip and Luna are stacked rounded spans β zero images, zero SVG. A body is a div with asymmetric border-radius; ears are rotated rounded rectangles; a crescent moon is a circle with a second, offset circle "biting" it. Each part carries the small clay-shadow recipe so the characters are made of the same material as the page.
The life comes from layered micro-animation: the whole character bobs (translateY, 5.4s), the body breathes (a slightly taller scale, 4.6s), and eyelid divs slide down over overflow:hidden eyes to blink:
.eye { overflow: hidden; border-radius: 999px; }
.lid { position: absolute; inset: -2px; background: var(--lav);
transform: translateY(-105%);
animation: blink 5.2s linear infinite; }
@keyframes blink {
0%, 91% { transform: translateY(-105%); } /* open */
93.5%, 95.5% { transform: translateY(0); } /* blink */
98%, 100% { transform: translateY(-105%); }
}
Every character gets a different animation-delay and blink duration so the cast never moves in unison β unison reads as robotic, offsets read as alive. On hover, ears wiggle around their base rotation (stored in a --r custom property so the keyframe can compose with it) and cheeks blush by fading in.
Star sprinkles & parallax sky
Releasing a squished character spawns a dozen tiny star divs at the press point. Each gets random --dx/--dy/--rot custom properties, plays one shared CSS keyframe outward, then removes itself on animationend (with a timeout as a safety net). The hero sky is four absolutely-positioned layers with a data-depth attribute; one rAF-batched handler translates them by scroll and pointer position at different rates, while the clouds also drift on their own slow keyframes and the moon arcs on a 90-second rotation around a transform-origin far below it.
And prefers-reduced-motion gets a genuinely calm page: blinks stop mid-open, clouds park, springs and sprinkles never run β but nothing disappears and nothing breaks.
How it was made
Pillow was built by Claude (Fable 5) writing vanilla HTML, CSS and JavaScript by hand β no frameworks, no build step, no images, one Google Font. The characters, the physics and the copy were all written line by line in a single pass of design-as-code.
It is one room in a larger building: the Fable Showcase, twenty-five wildly different demo sites exploring what a language model can do in web design. Wander the rest at fable-25-dhb.pages.dev.
Steal this
- The three-layer clay shadow. Outer drop + inset top-left light + inset bottom-right dark, with both insets tinted your text color. Works on any palette; instantly de-flattens a design.
- Springs over transitions for anything touchable. Fifteen lines of Hooke's law in rAF beats every easing keyword, because the overshoot depends on how the user actually pressed. Low damping out, high damping in.
- Squash into the floor.
transform-origin: 50% 100%is the difference between a button that scales and a button that feels like it has weight. - Desynchronize your idle animations. Same keyframes, different delays and durations per instance. A cast that breathes in unison is a screensaver; offset, it's alive.
- Store base rotations in custom properties.
--ron each ear lets one wiggle keyframe compose with any resting angle β one animation, every character.