-50% CODE

BLOOM50

|

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^18.0.0
tailwindcss^3.0.0