Get 50% off your purchase! Enter code "BLOOM50" at checkout.

Smart Loading Button

Intelligent loading button with progress indication, success states, and error handling.

ReactTailwind CSSTypeScriptResponsiveButton

Live Preview

Interactive
Smart Loading Button.tsxLive Code
"use client";

import { useState } from "react";

export default function SmartLoadingButton() {
  const [state, setState] = useState<"idle" | "loading" | "success" | "error">(
    "idle"
  );
  const [progress, setProgress] = useState(0);

  const handleClick = async () => {
    setState("loading");
    setProgress(0);

    const interval = setInterval(() => {
      setProgress((prev) => {
        const newProgress = prev + Math.random() * 15;

        if (newProgress >= 100) {
          clearInterval(interval);
          setState("success");
          setTimeout(() => {
            setState("idle");
            setProgress(0);
          }, 2000);
          return 100;
        }
        return newProgress;
      });
    }, 150);

    setTimeout(() => {
      if (state === "loading") {
        clearInterval(interval);
        setState("error");
        setTimeout(() => {
          setState("idle");
          setProgress(0);
        }, 2000);
      }
    }, 8000);
  };

  const getButtonContent = () => {
    switch (state) {
      case "loading":
        return (
          <div className="flex items-center justify-center gap-2">
            <div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin flex-shrink-0" \/>
            <span className="hidden sm:inline">Processing...<\/span>
            <span className="text-xs opacity-75 hidden md:inline">
              {Math.round(progress)}%
            <\/span>
          <\/div>
        );
      case "success":
        return (
          <div className="flex items-center justify-center gap-2">
            <svg
              className="w-4 h-4 flex-shrink-0"
              fill="none"
              stroke="currentColor"
              viewBox="0 0 24 24"
            >
              <path
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth={2}
                d="M5 13l4 4L19 7"
              \/>
            <\/svg>
            <span className="hidden sm:inline">Success!<\/span>
            <span className="sm:hidden">\u2713<\/span>
          <\/div>
        );
      case "error":
        return (
          <div className="flex items-center justify-center gap-2">
            <svg
              className="w-4 h-4 flex-shrink-0"
              fill="none"
              stroke="currentColor"
              viewBox="0 0 24 24"
            >
              <path
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth={2}
                d="M6 18L18 6M6 6l12 12"
              \/>
            <\/svg>
            <span className="hidden sm:inline">Try Again<\/span>
            <span className="sm:hidden">Retry<\/span>
          <\/div>
        );
      default:
        return (
          <span className="px-2">
            <span className="hidden sm:inline">Submit Form<\/span>
            <span className="sm:hidden">Submit<\/span>
          <\/span>
        );
    }
  };

  const getButtonClasses = () => {
    const baseClasses =
      "relative overflow-hidden px-4 sm:px-6 py-2 sm:py-3 rounded-lg font-medium transition-all duration-300 min-w-[120px] sm:min-w-[160px] md:min-w-[180px] text-sm sm:text-base focus:outline-none focus:ring-2 focus:ring-offset-2";

    switch (state) {
      case "success":
        return `${baseClasses} bg-green-600 text-white focus:ring-green-500 transform scale-105`;
      case "error":
        return `${baseClasses} bg-red-600 hover:bg-red-700 text-white focus:ring-red-500`;
      case "loading":
        return `${baseClasses} bg-purple-400 cursor-not-allowed text-white`;
      default:
        return `${baseClasses} bg-purple-600 hover:bg-purple-700 active:bg-purple-800 text-white focus:ring-purple-500 hover:transform hover:scale-[1.02]`;
    }
  };

  return (
    <div className="flex flex-col items-center gap-4 w-full max-w-sm mx-auto">
      <div className="flex justify-center w-full">
        <div className="relative">
          <button
            onClick={handleClick}
            disabled={state === "loading"}
            className={getButtonClasses()}
            aria-label={
              state === "loading"
                ? `Loading ${Math.round(progress)}%`
                : "Submit form"
            }
          >
            {state === "loading" && (
              <div
                className="absolute left-0 top-0 h-full bg-purple-700 transition-all duration-200 ease-out rounded-lg"
                style={{ width: `${progress}%` }}
              \/>
            )}

            <span className="relative z-10 flex items-center justify-center">
              {getButtonContent()}
            <\/span>
          <\/button>

          {state === "loading" && (
            <div className="mt-2 w-full bg-gray-200 rounded-full h-1.5 sm:hidden">
              <div
                className="bg-purple-600 h-1.5 rounded-full transition-all duration-200"
                style={{ width: `${progress}%` }}
              \/>
            <\/div>
          )}
        <\/div>
      <\/div>

      <div className="text-center min-h-[20px] text-xs sm:text-sm">
        {state === "idle" && (
          <p className="text-gray-500">
            <span className="hidden sm:inline">
              Click to simulate an async operation
            <\/span>
            <span className="sm:hidden">Click to start<\/span>
          <\/p>
        )}
        {state === "loading" && (
          <div className="space-y-1">
            <p className="text-purple-600 font-medium">
              <span className="hidden sm:inline">
                Processing your request...
              <\/span>
              <span className="sm:hidden">Processing...<\/span>
            <\/p>
            <p className="text-gray-500 text-xs hidden sm:block">
              Progress: {Math.round(progress)}% complete
            <\/p>
          <\/div>
        )}
        {state === "success" && (
          <p className="text-green-600 font-medium">
            <span className="hidden sm:inline">
              Operation completed successfully!
            <\/span>
            <span className="sm:hidden">Success!<\/span>
          <\/p>
        )}
        {state === "error" && (
          <p className="text-red-600 font-medium">
            <span className="hidden sm:inline">
              Something went wrong. Please try again.
            <\/span>
            <span className="sm:hidden">Error occurred<\/span>
          <\/p>
        )}
      <\/div>

      <div className="flex gap-2 text-xs">
        <button
          onClick={() => {
            setState("idle");
            setProgress(0);
          }}
          className="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-gray-600 transition-colors"
          disabled={state === "loading"}
        >
          Reset
        <\/button>
        <button
          onClick={() => {
            setState("error");
            setTimeout(() => setState("idle"), 2000);
          }}
          className="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-gray-600 transition-colors"
          disabled={state === "loading"}
        >
          <span className="hidden sm:inline">Simulate Error<\/span>
          <span className="sm:hidden">Error<\/span>
        <\/button>
      <\/div>
    <\/div>
  );
}

How to use

  1. 1. Copy the component code
  2. 2. Paste it into your React/Next.js project
  3. 3. Make sure you have Tailwind CSS configured
  4. 4. Customize colors, spacing, and content as needed

Dependencies

react
tailwindcss