Yuzu
a shy mascot that hides on click and peeks back in.
Yuzu is the little mascot on capy.ai. It lives on the right edge of the screen and acts like a small shy animal. It peeks in when the page loads, blinks, leans toward your cursor, squishes when you press it, and bolts when you click.
the slide in
Think about peeking around a corner to check if someone's there. You edge out a little, pause while you scan the room, then step out once it feels safe. Never one speed. Yuzu enters the same way.
A smooth glide works for most things on a page. It just can't act shy. So yuzu got its own speed curve:
CustomEase.create("peekIn", "0.165, 0.84, 0.82, 0.78");
gsap.fromTo(
el,
{ xPercent: 120 },
{ xPercent: 0, duration: 1.4, ease: "peekIn" },
);
The tail of the curve is bent so the speed never reaches zero. Yuzu inches out for most of the 1.4 seconds, then pushes the last bit and arrives still moving.
That leftover motion needs somewhere to go. Soft things don't just stop, they squash and stretch. So the arrival spills into a wobble:
keyframes: [
{ scaleX: 1.15, duration: 0.12 },
{ scaleX: 0.95, duration: 0.1 },
{ scaleX: 1, duration: 0.15, ease: "easeOutQuad" },
];
Stretch past normal, pull back, settle. That wobble is the "hey, i'm here."
the blink
A toy blinks on a timer. A pet blinks whenever it feels like it. The difference is randomness, so the wait between blinks is a dice roll:
const scheduleBlink = () => {
gsap.delayedCall(gsap.utils.random(3, 7), blink);
};
The blink itself is just the eyes squeezed to a line and back, 0.08 seconds each way. Fast enough that you feel it more than see it.
Hover over yuzu and it blinks once and leans toward you a little, like a pet noticing your hand before you pet it:
gsap.to(el, { xPercent: 12, duration: 0.3, ease: "easeOutQuart" });
There's a small detail that's easy to miss. Yuzu's face is sideways, so its eyes close left to right, not top to bottom. Squeeze them the normal way and it stops reading as a blink:
gsap.to(eyes, { scaleX: 0.1, duration: 0.08, yoyo: true, repeat: 1 });
// scaleX, not scaleY: the face is sideways
the squish
Press a gummy bear against a table. It doesn't shrink. It flattens where you push and bulges where you don't. To get that on screen, one decision matters before any animation: which part of yuzu stays pinned when it changes shape.
gsap.set(el, { transformOrigin: "center right" });
Yuzu's right edge is glued to the screen, so that edge stays pinned. Now pressing it squishes it against its own wall:
gsap.to(el, { scaleX: 0.87, scaleY: 1.08, duration: 0.15 });
Press yuzu and it flattens against the edge. The width it loses bulges into height, just like the gummy bear. That slight height change, done with scale, is what makes yuzu look squeezed.
Both clips below run the exact same press. In the first clip, yuzu is pinned to the wall, so it squishes against it. In the second, it's pinned at its own center, so it shrinks from the center, which looks unnatural.
the hide and come back
Startle a cat and watch the timing. The escape is instant. The hiding takes as long as it takes. The return is slow and careful. Fear is fast, curiosity is slow.
That's the whole design of the click:
returnTl
.to(el, { scaleX: 1, scaleY: 1, xPercent: 120, duration: 0.3 }) // bolt, fast
.to(el, { xPercent: 120, duration: 0.8 }) // hide, do nothing
.to(el, { xPercent: 0, duration: 1.4, ease: "peekIn" }); // creep back out
0.3 seconds to bolt. Then 0.8 seconds of nothing, which is yuzu deciding whether you can be trusted. Then the same slow peek-in from the start, wobble included.
The pause is the part people feel. Animating nothing for 0.8 seconds is still animating.
Every detail here is almost too small to notice on its own. Together, they're the whole character. That's the thing about small details: none of them matter until all of them do.