Back to Dictionary
Action Feedback
●○○Ready to Use
Use sparingly

Button Loading State

Choose button loading for async actions whose feedback belongs exactly where the user acted.

Use

Preventing duplicate submissions while keeping feedback anchored to the control the user clicked.

Avoid

Avoid for instant local toggles, links, or actions where disabling the button would hide available recovery.

Safer Alternative

Spinner / Loading Circle

Risk Level

Use carefully

Timing

Show only after roughly 250-300ms to avoid flashing on fast actions; keep width stable throughout.

Easing

Use a quick ease-out opacity or icon swap; the spinner itself should rotate linearly.

Risk

feels slow

Live Demo

What It Is

A button that transitions into a loading state upon being clicked, usually by replacing text with a spinner or adding a spinner next to the text while disabling further clicks.

Decision Guidance

Choose button loading for async actions whose feedback belongs exactly where the user acted.

Best For

Preventing duplicate submissions while keeping feedback anchored to the control the user clicked.

Avoid When

Avoid for instant local toggles, links, or actions where disabling the button would hide available recovery.

Timing

Show only after roughly 250-300ms to avoid flashing on fast actions; keep width stable throughout.

Easing

Use a quick ease-out opacity or icon swap; the spinner itself should rotate linearly.

Risk Tags

feels slow

When to Use

  • Form submissions
  • Saving data
  • Async actions that take >300ms

When NOT to Use

  • Instant client-side actions
  • Navigation links

Configuration Tips

  • 01Disable the button while loading to prevent double submissions
  • 02Ensure the button does not resize when the text swaps to a spinner

You've Seen It In

StripeVercelLinear

AI Implementation Prompts

Move from a fast scaffold to production details, then tune timing and edge states.

Create a submit button that shows a loading spinner when clicked and disables itself.

Reference Implementation

The same implementation approach used by the live preview, kept compact enough to inspect and adapt.

'use client';
import { motion, AnimatePresence } from 'framer-motion';
import { Loader2 } from 'lucide-react';
import { useState, useEffect } from 'react';

export function ButtonLoadingStateDemo({ isPlaying = false }: { isPlaying?: boolean }) {
    const [isLoading, setIsLoading] = useState(false);

    useEffect(() => {
        if (isPlaying) {
            setIsLoading(true);
            const timer = setTimeout(() => setIsLoading(false), 2000);
            return () => clearTimeout(timer);
        } else {
            setIsLoading(false);
        }
    }, [isPlaying]);

    return (
        <button
            onClick={() => setIsLoading(true)}
            disabled={isLoading}
            className="relative flex h-12 w-40 items-center justify-center rounded-xl bg-stone-900 text-sm font-medium text-white shadow-sm transition-all hover:bg-stone-800 disabled:cursor-not-allowed disabled:bg-stone-900"
        >
            <AnimatePresence mode="popLayout" initial={false}>
                {isLoading ? (
                    <motion.div
                        key="spinner"
                        initial={{ opacity: 0, scale: 0.5 }}
                        animate={{ opacity: 1, scale: 1 }}
                        exit={{ opacity: 0, scale: 0.5 }}
                        transition={{ duration: 0.2 }}
                        className="flex items-center justify-center"
                    >
                        <Loader2 className="h-5 w-5 animate-spin text-stone-300" />
                    </motion.div>
                ) : (
                    <motion.span
                        key="text"
                        initial={{ opacity: 0, y: 10 }}
                        animate={{ opacity: 1, y: 0 }}
                        exit={{ opacity: 0, y: -10 }}
                        transition={{ duration: 0.2 }}
                    >
                        Save Changes
                    </motion.span>
                )}
            </AnimatePresence>
        </button>
    );
}