The premise is pretty simple: Most support chat is, in its current state, painful at best. It's either reaching out via an email, an archaic system that requires account creation, live support, or AI. This project focuses mainly on existing live support.
If you haven't played around with it yet, you can try it here.
Motion and attention (as a byproduct) were really my two goals throughout all of this. Seems fairly obvious now that I made a dummy example, but why can't you see your spot in the queue, how long you've waited, and other pertinent information after submitting a request? And why can't that adapt real time and classify your request with helpful information for the support folks prior to the request being triaged?
The project is open source if you'd like to take a look at the code, but thought a few things in particular were worth calling out in case you're building something similar:
The float
I first tried the path of least resistance which was some combination of Tailwind animate properties but there was this jarring pause after the float up, then back down, and the rotation felt stiff. It didn't feel natural. It didn't feel like the request was "floating", or being processed in the air, which was the goal.
I ended up using two sine waves (shoutout to both high school math and Bruno Simon's Three.js Journey course) in a useEffect. One to control Y translation and the other to control the Y rotation, both over a 6 second period.
useEffect(() => {
if (isFloating && !prefersReduced) {
const start = performance.now();
const period = 6000;
const yAmplitude = 8;
const rotAmplitude = 0.8;
function tick() {
const t = (performance.now() - start) / period;
const sine = Math.sin(t * 2 * Math.PI);
floatY.set(-sine * yAmplitude);
floatRotate.set(Math.sin(t * 2 * Math.PI + 0.8) * rotAmplitude);
rafRef.current = requestAnimationFrame(tick);
}
rafRef.current = requestAnimationFrame(tick);
} else {
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
rafRef.current = null;
animate(floatY, 0, springs.snappy);
animate(floatRotate, 0, springs.snappy);
}
return () => {
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
rafRef.current = null;
};
}, [isFloating, prefersReduced, floatY, floatRotate]);I love the way it hangs for a bit while the other animations run within the card. To me, it's important that the software I write is performant, but I want it to be equally fun.
The sweep
Most sweep animations I've used/seen are pretty bland on the structure and timing. To be fair, most people aren't spending too much time looking at the flash sweeps. Usually, it's a linear flash or highlight, but I wanted a more "charge and release" style animation, emulating a classification of sorts.
Instead of using Motion's useAnimate hook in more recent versions, I opted for Motion's module level animate() value, as it gives me explicit control over cancellation in the timing of the animation, which I wanted for the controls section in case users wanted to stop it mid-animation.
const timeout = setTimeout(async () => {
await animate(x, "-25%", { duration: 0.7, ease: "easeIn" }).finished;
if (cancelled) return;
await animate(x, "150%", { duration: 0.35, ease: "linear" }).finished;
if (cancelled) return;
onFinish?.();
await animate(x, "150%", { duration: 0.35, ease: "linear" }).finished;
if (cancelled) return;
await animate(x, "200%", { duration: 0.6, ease: "easeOut" }).finished;
}, startDelay);Overall, a really fun animation to create. Link to all of the resources below:
Demo: query-classification.vercel.app
Github: github.com/garrettprince/query-classification
And check out my LinkedIn post for the video demonstrating Query Classification.
DM or email me with any questions! Thanks for reading!
