Skip to content
← ALL EXPERIMENTS

02 / SVG / 2026-04-21

Darth the Cat

A scroll-companion pet who gets upset if you leave him behind.

How it works

// 01 DPI and pixel density

The widget runs one requestAnimationFrame loop that updates CSS custom properties on the cat wrapper. The cat itself is a single SVG element with the body, head, ears, tail, legs, and eyes as separate sub-paths. Animation is procedural: tail rotation is a sine wave with pose-dependent frequency and amplitude, walking is a sine bounce on Y plus linear translation on X along a patrol baseline, and blinking is an interval timer that closes the eye opacity to 0.05 for 180 milliseconds every 4 to 7 seconds.

// 02 Bleed and cut tolerance

State is held in a ref, not React state, so the hot loop does not cause re-renders. React state tracks only what affects rendering: the pose, whether Darth is abandoned, whether he is perched at the top of the viewport, and the list of Z glyphs to render while sleeping.

// 03 Safe zone and trim accuracy

Abandonment is detected by comparing window.scrollY to the stored y-coordinate of his home baseline plus a buffer. A single setTimeout tracks the scroll-idle window; every scroll event resets it, and if it fires while he is abandoned, his state flips to perched, which swaps his CSS position from absolute to fixed and runs a short slide-in animation. Scrolling back above his home clears both abandonment and perched, resetting him to idle at his home x.

// 04

Reduced motion shortcuts all of it. The walk patrol, body bounce, tail sway, and Z emitter all gate on the reduced-motion flag. Catch-up still happens because it is a functional behavior, but the slide animation duration drops to zero so he teleports instead.

OBJECT / darth.catSVG
......
......

// why this exists

scroll-driven state machines with restraint

Darth is a tiny black cat who lives at the bottom of a simulated hero. He is roughly the cap-height of a capital letter in the headline above him, which puts him somewhere between a glyph and a character. Most of the time he does very little. He sits at his post, tail ticking in small arcs, eyes closing for a blink every few seconds. Every now and then he paces back and forth along the baseline. Stay long enough and he lies down, because that is what cats do.

The trick is what happens when you scroll past him and keep going. He stays where he is, as cats do. After a moment, a tiny toast appears in the corner of the window telling you that you have left Darth. If you stop scrolling for long enough, he catches up. Darth repositions to the top-right of the viewport, sits down, and watches you with narrow pink eyes until you scroll back to get him.

Companion UI is a design idea that has been around for a long time and almost always fails. The early example everyone knows is Microsoft Clippy, which interrupted your work to ask if you were writing a letter. The pattern since then has mostly been to avoid the whole category, because the failure mode is annoyance and the success mode is hard to define. This widget is an experiment in the third option, which is restraint. Darth does nothing until you give him a reason to do something. When he acts, it is small: a tiny toast, a quiet animation, a disapproving look. The idea is that a companion is mostly valuable as presence and context, not as an interaction surface. If he was talking to you every ten seconds, he would be Clippy. Because he is mostly silent, the moments when he does act carry weight.

This is a lab prototype, not a shipped feature. The behaviors here are tuned for a single demo page with a long scroll surface. A sitewide version has a bigger design problem to solve: does Darth follow you across routes, does he remember where he was, does he respect reduced motion by sitting motionless instead of napping. Those questions are worth answering if the concept reads well in the lab first. The point of building this small is to see whether anyone likes it before investing in the cross-page plumbing.

Frequently asked questions

Why a cat?

A cat is small enough to feel like a presence rather than a feature. It has an established vocabulary (walk, nap, scowl) that reads instantly without explanation. And the metaphor of having to come back for him maps directly to the scroll behavior this widget wants to demonstrate.

Does he follow me across the site?

No, not in this lab prototype. This widget runs on a single demo page. Sitewide Darth would need multi-page persistence (localStorage for his last seen position, animation continuity across route changes), which is a separate build if the lab version reads well.

Can I dismiss him?

Not yet. The design intent is that he is quiet enough not to need dismissal. If the reception ends up being that he is annoying even at this restrained level, a dismiss control would be easy to add later.

Is this accessible?

The cat is decorative and marked aria-hidden. The toast uses role=status and aria-live=polite so a screen reader announces it when it appears, without interrupting ongoing speech.

What happens on mobile?

Same behaviors, same timings. The cat is sized in viewport-relative units so he stays readable at 320 to 1440 pixel widths. The toast uses a fixed position with enough margin that it does not overlap the site's existing chat bubble in the opposite corner.

How does reduced-motion behave?

Walk, bounce, tail sway, and Z emitter all disable. The cat renders as a static standing silhouette with slow blinking. Abandonment detection and catch-up still work, but the slide-in animation drops to zero duration, so he teleports instead.

What are the performance costs?

One rAF loop and one scroll listener. The loop updates CSS custom properties on a wrapper div, not React state, so there are no per-frame re-renders. Total JavaScript payload for the widget is small: no sprite sheets, no external assets, one SVG and one event handler.

Would you ship this on the real site?

Maybe. The lab is where it lives until we see whether people find it charming or annoying. Charming plus on-brand with the terminal-pet aesthetic would justify a sitewide build with the route-persistence work. Annoying at any level would not.