Cat-ch Me If You Can: Rive's Get Started Button in SVG
rive.app does brilliant work. The illustrations are gorgeous, the tooling is powerful, and they have some genuinely great use cases, from app UI to marketing sites and games.
I still wanted to see if I could recreate one of those playful interactions on my own site in plain HTML, SVG, CSS, and JavaScript. Part curiosity, part accessibility exercise: could I get close to the feel of the original while keeping a real link with real link text?
The homepage Get started button is the one I picked: a cat paw shoots out from behind the pill and tries to grab your cursor.
Try it
Hover the left or right half for a side reach. Move below the button and the flap opens for an underhand swipe. On touch, tap once for a full cycle. Arrow keys work too: left, right, and down.
If you use prefers-reduced-motion, the paw animation is skipped and the link behaves like a plain CTA. Hurrah for accessibility!
How Rive handles this (and how I approached it in SVG)
Rive's web runtime is open source in rive-app/rive-wasm. You point a <canvas> at a .riv file, load WebAssembly compiled from their C++ core, and let a requestAnimationFrame loop paint vectors each frame. Pointer inputs feed state machines. For rich, interactive animation at scale, that is exactly the right tool.
For this one button on my site, I wanted to work through the same problem in SVG instead: semantic markup, a timeline I could unit test, and prefers-reduced-motion built in from the start. Its the same problem, just a different medium.
If you strip away the .riv file, the cat button interaction is simpler than it looks:
- A paw that emerges from behind the button
- Side reach vs bottom reach (with a flap)
- A two-second cycle: out, pat, retract
- A layer swap when the paw crosses the button edge so it can pat on top of the label
Most of the magic is in that last point. The stacking matters more than the paw art.
The bit that took me the longest
The paw has to hide behind the button, then pat on top of the label when it crosses the edge. That means two full-size SVG scenes sharing the same viewBox: one behind the label, one in front that only appears when the pat needs to sit above the text.
I spent an embarrassing amount of time with the paw always on top, which made it look like it teleported the instant you hovered. It needs to emerge from behind the edge first, then hand off to the front layer only when isPawOverButton() is true.
<a class="get-started-cat" href="…"> <span class="get-started-cat__btn-back"></span> <svg class="get-started-cat__scene">…arm, paw-behind, button…</svg> <span class="get-started-cat__label">Get started</span> <svg class="get-started-cat__scene get-started-cat__scene--front">…arm, paw-front…</svg> </a>
Reach zones
Pointer position picks the mode. Side hovers root the arm at the left or right edge. Bottom hovers root it behind the centre so the paw can emerge upward through the flap.
export function computeReach(cursorX, cursorY) { const side = cursorX < STAGE.center ? "left" : "right"; const zone = cursorY > STAGE.btnBottom ? "bottom" : "side"; const rootX = zone === "bottom" ? STAGE.center : side === "left" ? STAGE.btnLeft : STAGE.btnRight; const paw = clampPawToHover(cursorX, cursorY); return { rootX, rootY: STAGE.btnMiddleY, ...paw, side, zone }; }
The bugs that made it feel alive
The fake fourth swipe. Retract started from the last scattered pat offset instead of the reach target, so it looked like a bonus swipe. I fixed it by centring the final pat on the cursor.
The stepped button height. The bottom flap was tied to paw extension, which moves in three segments. Giving flapT its own smooth curve fixed the stutter.
Flap before paw. On bottom hovers the button should open first, then the paw comes out. Sequencing those separately got me much closer to the original.
Palm up vs back of paw. Side reaches show pink pads. Bottom reaches flip to the back of the paw with claw nubs. It is a small detail, but it makes the motion much easier to read.
Swipe timeline
One computePawFrame(elapsed, reach, patOffsets) function returns everything for each frame. A requestAnimationFrame loop writes SVG attributes:
const paint = () => { const d = buildArmPath(rootX, rootY, pawX, pawY); armRef.setAttribute("d", d); pawRef.setAttribute( "transform", `translate(${pawX} ${pawY}) rotate(${angleDeg + 90}) scale(${scale})` ); // toggle behind/front layers, update flap metrics on bottom zone };
The cycle runs out in three segments, pat with a little scatter before settling on the reach point, then in as a mirror of the reach. Cosine easing throughout, with a quick scale dip on each pat.
Bottom flap (CSS + JS)
JavaScript sets --flap and --btn-h each frame. CSS handles the tilt:
.get-started-cat { aspect-ratio: 340 / 150; --flap: 0; --btn-h: 29.33%; } .get-started-cat[data-zone="bottom"] .get-started-cat__label { transform: perspective(900px) rotateX(calc(var(--flap) * -10deg)); }
Fork it on CodePen
Here is a vanilla HTML, CSS, and JS version you can fork and poke at:
Open the pen. Source files also live in codepen/get-started-cat/ in this repo.
If you are rebuilding something similar, I would start with the layer sandwich and a fake paw that only translates on pointer move, then add zones, then the timeline, then the art.
More things I built for fun
I like making playful, interactive bits like this when I have the time. A few others on the site:
- Theme Toggler with Audio: a dial switch with click sounds and a little sunrise/sunset chord when you flip light and dark mode
- SVG Liquid Thermometer with Animated Bubbles: click to fill a thermometer and watch bubbles rise through the liquid
- Circular Draggable Audio Player in React: scrub around a ring to control playback
There is plenty more in the playground if you fancy a rummage.
Questions or your own take on it? Get in touch.