Back to Blog

Build an Animated Button in Tauri | React + Tailwind CSS Tutorial

Sandy LaneSandy Lane

Video: Build an Animated Button in Tauri | React + Tailwind CSS Tutorial by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

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.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.