Getting Started
All Components
Playground
Override each label or the simulated save duration and the snippet below updates to match.
<SaveButton onSave={handleSave} />Props
Installation
npx shadcn@latest add https://tentui.com/r/animated-save-button.json
Usage
import { SaveButton } from "@/components/animated-save-button";
export default function Page() {
const handleSave = async () => {
await fetch("/api/save", { method: "POST" });
};
return <SaveButton onSave={handleSave} />;
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
onSave | () => void | Promise<void> | — | Called when the button is clicked. Awaited before success state. |
labels | { idle?: string; loading?: string; success?: string } | — | Override button text for each state. |
className | string | — | Extra classes for the outer wrapper. |
How it works
Clicking transitions through idle → loading → success → idle. The onSave callback is awaited — if it throws, the component still advances to success (handle errors in your callback before rethrowing). Each state change animates the button text character by character using AnimatePresence mode="popLayout" so individual letters spring in and out independently. A badge in the top-right corner appears on state change: a spinner during loading, a check on success.
Patterns
Custom labels
<SaveButton
labels={{ idle: "Publish", loading: "Publishing", success: "Published" }}
onSave={handlePublish}
/>Fire and forget (no async)
<SaveButton onSave={() => console.log("saved")} />