Long-running background jobs are a fact of life in video processing. When a user uploads a 30-minute video, your AI pipeline might need two or three minutes to download, transcribe, and clip it. The worst thing you can do is show a spinner and leave people wondering if something broke.
In ClipCrafter — an AI-powered tool that turns long videos into short, shareable clips — we recently overhauled our processing UX. Here's how we did it with React, TypeScript, and a little personality.
The problem
Our original processing screen was a single progress bar with a generic "Processing…" label. Users had no idea which stage the pipeline was in, how long it would take, or whether the page was even still working. Support messages spiked every time processing took longer than 90 seconds.
Solution 1 — Rotating whimsical messages
We created a dedicated module — loadingMessages.ts — that maps each pipeline stage to an array of light-hearted messages:
// src/lib/loadingMessages.ts
const stageMessages: Record<ProcessingStage, string[]> = {
downloading: [
"Summoning your video from the internet…",
"Convincing the server to share…",
"Downloading at the speed of vibes…",
],
transcribing: [
"Eavesdropping scientifically…",
"Teaching the AI to listen…",
"Turning sound waves into words…",
],
analyzing: [
"Asking the AI what slaps…",
"Finding the best moments…",
"Separating signal from filler…",
],
// ... more stages
};
A useEffect hook cycles through the array for the current stage every three seconds, giving the interface a living, breathing feel:
useEffect(() => {
const messages = stageMessages[currentStage];
const interval = setInterval(() => {
setMessageIndex((i) => (i + 1) % messages.length);
}, 3000);
return () => clearInterval(interval);
}, [currentStage]);
The psychological impact is real — users see movement and humour, which makes the wait feel shorter.
Solution 2 — A stepped progress indicator
Instead of one bar, we render five dots connected by lines — one per pipeline stage. The active dot pulses; completed dots turn green. The component is pure CSS and state:
const stages = ["download", "transcribe", "analyze", "clip", "export"];
{stages.map((stage, i) => (
<div key={stage} className="flex items-center">
<div className={cn(
"h-3 w-3 rounded-full",
i < currentIndex ? "bg-green-500" :
i === currentIndex ? "bg-blue-500 animate-pulse" :
"bg-zinc-700"
)} />
{i < stages.length - 1 && (
<div className={cn(
"h-0.5 w-8",
i < currentIndex ? "bg-green-500" : "bg-zinc-700"
)} />
)}
</div>
))}
Paired with a time estimate — "Usually 2–3 min for a 30 min video" — users know exactly where they are and roughly when to expect results.
Bonus — Fixing the clip drag glitch
While we were in the code, we fixed a nasty drag interaction bug. Our clip timeline let users drag handles to adjust start and end times, but every pixel of movement triggered a React state update and a database PATCH. The video player jumped erratically and the UI felt laggy.
The fix was ref-based tracking:
const dragValueRef = useRef(currentTime);
const onMouseMove = (e: MouseEvent) => {
dragValueRef.current = pixelToTime(e.clientX);
debouncedSeek(dragValueRef.current);
};
const onMouseUp = () => {
setClipTime(dragValueRef.current);
updateClipOnServer(dragValueRef.current);
};
By keeping the drag value in a ref and only committing on mouseup, we eliminated re-renders during the drag and reduced API calls from dozens to exactly one.
Takeaways
- Loading copy matters. Whimsical, rotating messages humanize the wait and reduce perceived latency.
- Show progress structure, not just a bar. Stepped indicators give users a mental model of where they are in the pipeline.
-
Use refs for drag interactions. React state during
mousemoveis a performance trap — keep the hot loop in a ref and sync state on commit.
If you're building a video or media processing tool and want to skip straight to the fun part, give ClipCrafter a try — drop in a YouTube link and get clips in minutes.
Built with Next.js, TypeScript, Inngest, and Tailwind CSS.
Top comments (0)