Build an Animated Button in Tauri | React + Tailwind CSS Tutorial
Video: Build an Animated Button in Tauri | React + Tailwind CSS Tutorial by Taught by Celeste AI - AI Coding Coach
Tailwind transitions for free. Framer Motion when you need physics. Same patterns as any web app — Tauri inherits them all.
Tauri's frontend is the web. That means every web animation technique works: CSS transitions, CSS animations, Framer Motion, GSAP, Lottie, anything you would do in a regular React app. This lesson is a practical tour of the simplest options for adding life to buttons.
Tailwind transitions
The cheapest animation: just a Tailwind transition class.
<button className="px-6 py-3 bg-blue-500 hover:bg-blue-600 active:scale-95 transition transform duration-150 rounded-lg text-white">
Click me
</button>
What's happening:
hover:bg-blue-600— color change on hover.active:scale-95— shrink slightly on click.transition transform duration-150— animate the changes over 150ms.
That's it. Three classes, smooth interactive feedback, no JS.
Loading state
When a button triggers an async operation, show progress:
function SaveButton() {
const [saving, setSaving] = useState(false);
async function handle() {
setSaving(true);
try {
await invoke("save_data");
} finally {
setSaving(false);
}
}
return (
<button
disabled={saving}
onClick={handle}
className="px-6 py-3 bg-blue-500 hover:bg-blue-600 disabled:opacity-50 transition rounded-lg text-white flex items-center gap-2"
>
{saving && (
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
</svg>
)}
{saving ? "Saving..." : "Save"}
</button>
);
}
animate-spin is a Tailwind class for an infinite rotation. The button is disabled during the call so users can't double-click.
Success / error feedback
After a save, a brief visual confirmation:
const [status, setStatus] = useState<"idle" | "saving" | "success" | "error">("idle");
async function handle() {
setStatus("saving");
try {
await invoke("save_data");
setStatus("success");
setTimeout(() => setStatus("idle"), 1500);
} catch {
setStatus("error");
setTimeout(() => setStatus("idle"), 1500);
}
}
return (
<button className={`
px-6 py-3 rounded-lg text-white transition-colors
${status === "saving" ? "bg-blue-500" : ""}
${status === "success" ? "bg-green-500" : ""}
${status === "error" ? "bg-red-500" : ""}
${status === "idle" ? "bg-blue-500 hover:bg-blue-600" : ""}
`}>
{status === "saving" && "Saving..."}
{status === "success" && "Saved!"}
{status === "error" && "Failed"}
{status === "idle" && "Save"}
</button>
);
The button becomes a small visual log of what just happened. Simple, no heavy animation library required.
CSS keyframes for richer animation
For something more elaborate — a button that pulses on success — define a keyframe in your CSS:
@keyframes pulse-success {
0% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.6); }
70% { box-shadow: 0 0 0 12px rgba(34, 197, 94, 0); }
100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); }
}
.pulse-success {
animation: pulse-success 1s ease-out;
}
Apply the class when the button is in the success state. The keyframe expands a green ring around the button briefly.
Framer Motion for richer interactions
Pure CSS handles most cases. For physics-based animation (springs, gestures, layout transitions), reach for Framer Motion:
npm install motion
import { motion } from "motion/react";
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 20 }}
className="px-6 py-3 bg-blue-500 rounded-lg text-white"
>
Click me
</motion.button>
whileHover and whileTap are reactive to the user's interaction; the spring transition gives a natural feel. Much more pleasant than linear interpolation for interactive elements.
Tauri-specific: window-level animation
Buttons that interact with the window itself can produce satisfying effects. A "Move to back" button that animates a window-fade:
import { getCurrentWindow } from "@tauri-apps/api/window";
const win = getCurrentWindow();
async function fadeAndMinimize() {
document.body.style.transition = "opacity 0.3s";
document.body.style.opacity = "0";
await new Promise((r) => setTimeout(r, 300));
await win.minimize();
document.body.style.opacity = "1";
}
Visual flair on operations that don't usually have any.
Dark mode support
If your app supports dark mode (and it should), use Tailwind's dark variant for hover and active states:
<button className="px-6 py-3 bg-blue-500 dark:bg-blue-600 hover:bg-blue-600 dark:hover:bg-blue-700 transition">
Click me
</button>
Each state needs its own dark variant. Tedious to write; you usually centralise in a button component.
A reusable styled button
type Variant = "primary" | "secondary" | "danger";
function Button({
variant = "primary",
loading = false,
children,
...props
}: {
variant?: Variant;
loading?: boolean;
children: React.ReactNode;
} & React.ButtonHTMLAttributes<HTMLButtonElement>) {
const colors = {
primary: "bg-blue-500 hover:bg-blue-600",
secondary: "bg-slate-200 hover:bg-slate-300 text-slate-900",
danger: "bg-red-500 hover:bg-red-600",
};
return (
<button
{...props}
disabled={props.disabled || loading}
className={`${colors[variant]} px-4 py-2 rounded text-white transition disabled:opacity-50 flex items-center gap-2 ${props.className ?? ""}`}
>
{loading && <Spinner />}
{children}
</button>
);
}
Ship one component; use it everywhere. Updates to the design land in one place.
Common mistakes
Long animations. 1+ second animations slow the user down. 100–300ms is usually right for interactive feedback.
Animating layout properties. Animating width, height, padding, etc., causes layout reflows and stutters. Animate transform and opacity for smooth GPU-accelerated changes.
Overdoing animation. Bouncy springs on every button gets exhausting. Reserve elaborate animation for moments — onboarding, success states — not constant background motion.
Ignoring prefers-reduced-motion. Users with vestibular sensitivities request reduced motion via OS settings. Respect it:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
What's next
Next lesson: a clipboard manager. Combines tray icon + global shortcut + clipboard plugin into a small utility app.
Recap
Tauri's frontend is the web; every web animation technique works. Tailwind for cheap transitions. CSS keyframes for richer effects. Framer Motion for physics. Disable buttons during async work. Show progress, success, and error states. Respect prefers-reduced-motion.
Next: clipboard manager.