BigBlocks Docs

LoadingButton

A button component with built-in loading states and animations, perfect for forms and async operations

LoadingButton

A versatile button component that handles loading states automatically, providing visual feedback during asynchronous operations like form submissions, API calls, and blockchain transactions.

View Component Preview →

Installation

npx bigblocks add loading-button

Import

import { LoadingButton } from 'bigblocks';

Props

PropTypeRequiredDefaultDescription
loadingbooleanNofalseShows loading spinner and optionally loading text
loadingTextstringNo-Text to display when loading (replaces children)
childrenReactNodeYes-Button content
disabledbooleanNofalseDisables the button (auto-disabled when loading)
...RadixButtonPropsvariousNo-All Radix Button props are supported

Inherited Radix Button Props

The LoadingButton extends Radix UI's Button component, inheriting all its props:

PropTypeDefaultDescription
variant'classic' | 'solid' | 'soft' | 'surface' | 'outline' | 'ghost''solid'Button visual style
size'1' | '2' | '3' | '4''2'Button size
colorstring-Theme color
highContrastbooleanfalseHigh contrast mode
radius'none' | 'small' | 'medium' | 'large' | 'full'-Border radius
asChildbooleanfalseRender as child element

Basic Usage

import { LoadingButton } from 'bigblocks';
import { useState } from 'react';

export default function BasicExample() {
  const [loading, setLoading] = useState(false);

  const handleClick = async () => {
    setLoading(true);
    try {
      await someAsyncOperation();
    } finally {
      setLoading(false);
    }
  };

  return (
    <LoadingButton loading={loading} onClick={handleClick}>
      Submit
    </LoadingButton>
  );
}

Advanced Usage

Complete Form Integration

import { LoadingButton } from 'bigblocks';
import { useState } from 'react';
import { useRouter } from 'next/navigation';

export default function LoginForm() {
  const [loading, setLoading] = useState(false);
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const router = useRouter();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password })
      });
      
      if (response.ok) {
        router.push('/dashboard');
      } else {
        const error = await response.json();
        alert(error.message || 'Login failed');
      }
    } catch (error) {
      console.error('Login error:', error);
      alert('Network error. Please try again.');
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
        required
        className="w-full p-2 border rounded"
      />
      
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
        required
        className="w-full p-2 border rounded"
      />
      
      <LoadingButton
        type="submit"
        loading={loading}
        loadingText="Signing in..."
        variant="solid"
        size="3"
        className="w-full"
      >
        Sign In
      </LoadingButton>
    </form>
  );
}

Bitcoin Transaction Example

import { LoadingButton } from 'bigblocks';
import { useState } from 'react';
import { useBitcoinAuth } from 'bigblocks';

export default function SendBitcoinButton() {
  const [loading, setLoading] = useState(false);
  const { sendBSV } = useBitcoinAuth();

  const handleSend = async () => {
    setLoading(true);
    
    try {
      const result = await sendBSV({
        to: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa',
        amount: 0.001, // BSV
        currency: 'BSV'
      });
      
      console.log('Transaction ID:', result.txid);
      alert(`Sent successfully! TX: ${result.txid}`);
    } catch (error) {
      console.error('Send failed:', error);
      alert(error.message || 'Failed to send BSV');
    } finally {
      setLoading(false);
    }
  };

  return (
    <LoadingButton
      loading={loading}
      loadingText="Broadcasting transaction..."
      onClick={handleSend}
      variant="solid"
      color="orange"
    >
      Send 0.001 BSV
    </LoadingButton>
  );
}

Common Patterns

With Icons

import { LoadingButton } from 'bigblocks';
import { DownloadIcon, SaveIcon, TrashIcon } from 'lucide-react';

export default function IconButtons() {
  const [downloading, setDownloading] = useState(false);
  const [saving, setSaving] = useState(false);
  const [deleting, setDeleting] = useState(false);

  return (
    <div className="flex gap-4">
      <LoadingButton
        loading={downloading}
        loadingText="Downloading..."
        onClick={handleDownload}
        variant="outline"
      >
        <DownloadIcon className="w-4 h-4 mr-2" />
        Download
      </LoadingButton>

      <LoadingButton
        loading={saving}
        loadingText="Saving..."
        onClick={handleSave}
        variant="solid"
        color="green"
      >
        <SaveIcon className="w-4 h-4 mr-2" />
        Save Changes
      </LoadingButton>

      <LoadingButton
        loading={deleting}
        loadingText="Deleting..."
        onClick={handleDelete}
        variant="soft"
        color="red"
      >
        <TrashIcon className="w-4 h-4 mr-2" />
        Delete
      </LoadingButton>
    </div>
  );
}

Multiple Actions with Different States

import { LoadingButton } from 'bigblocks';
import { useState } from 'react';

export default function MultiActionForm() {
  const [saveLoading, setSaveLoading] = useState(false);
  const [publishLoading, setPublishLoading] = useState(false);
  const [deleteLoading, setDeleteLoading] = useState(false);

  const handleSave = async () => {
    setSaveLoading(true);
    await saveDocument();
    setSaveLoading(false);
  };

  const handlePublish = async () => {
    setPublishLoading(true);
    await publishDocument();
    setPublishLoading(false);
  };

  const handleDelete = async () => {
    if (!confirm('Are you sure?')) return;
    
    setDeleteLoading(true);
    await deleteDocument();
    setDeleteLoading(false);
  };

  return (
    <div className="flex gap-2">
      <LoadingButton
        loading={saveLoading}
        loadingText="Saving..."
        onClick={handleSave}
        variant="outline"
      >
        Save Draft
      </LoadingButton>

      <LoadingButton
        loading={publishLoading}
        loadingText="Publishing..."
        onClick={handlePublish}
        variant="solid"
        color="blue"
      >
        Publish
      </LoadingButton>

      <LoadingButton
        loading={deleteLoading}
        loadingText="Deleting..."
        onClick={handleDelete}
        variant="ghost"
        color="red"
      >
        Delete
      </LoadingButton>
    </div>
  );
}

Error Handling Pattern

import { LoadingButton } from 'bigblocks';
import { useState } from 'react';

export default function ErrorHandlingExample() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [success, setSuccess] = useState(false);

  const handleSubmit = async () => {
    setLoading(true);
    setError(null);
    setSuccess(false);
    
    try {
      const response = await fetch('/api/submit', {
        method: 'POST',
        body: JSON.stringify({ data: 'example' })
      });
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }
      
      setSuccess(true);
      setTimeout(() => setSuccess(false), 3000); // Clear after 3s
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error');
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="space-y-2">
      <LoadingButton
        loading={loading}
        loadingText="Processing..."
        onClick={handleSubmit}
        variant={error ? 'soft' : 'solid'}
        color={error ? 'red' : success ? 'green' : undefined}
      >
        {error ? 'Retry' : success ? 'Success!' : 'Submit'}
      </LoadingButton>
      
      {error && (
        <p className="text-red-500 text-sm">{error}</p>
      )}
    </div>
  );
}

Async Validation

import { LoadingButton } from 'bigblocks';
import { useState } from 'react';

export default function AsyncValidationForm() {
  const [validating, setValidating] = useState(false);
  const [submitting, setSubmitting] = useState(false);
  const [isValid, setIsValid] = useState<boolean | null>(null);

  const validateForm = async () => {
    setValidating(true);
    try {
      const response = await fetch('/api/validate', {
        method: 'POST',
        body: JSON.stringify({ /* form data */ })
      });
      const { valid } = await response.json();
      setIsValid(valid);
      return valid;
    } finally {
      setValidating(false);
    }
  };

  const handleSubmit = async () => {
    const valid = await validateForm();
    if (!valid) return;
    
    setSubmitting(true);
    try {
      await submitForm();
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <div className="space-y-4">
      <LoadingButton
        loading={validating}
        loadingText="Validating..."
        onClick={validateForm}
        variant="outline"
        disabled={submitting}
      >
        Validate
      </LoadingButton>

      <LoadingButton
        loading={submitting}
        loadingText="Submitting..."
        onClick={handleSubmit}
        variant="solid"
        disabled={validating || isValid === false}
      >
        Submit
      </LoadingButton>
    </div>
  );
}

Authentication Requirements

While LoadingButton doesn't require authentication itself, it's commonly used within authenticated contexts:

import { 
  BitcoinAuthProvider, 
  BitcoinQueryProvider,
  LoadingButton,
  useBitcoinAuth
} from 'bigblocks';

function AuthenticatedAction() {
  const [loading, setLoading] = useState(false);
  const { isAuthenticated, signMessage } = useBitcoinAuth();

  const handleAction = async () => {
    if (!isAuthenticated) {
      alert('Please sign in first');
      return;
    }
    
    setLoading(true);
    try {
      const signature = await signMessage('Authorize action');
      await performAuthenticatedAction(signature);
    } finally {
      setLoading(false);
    }
  };

  return (
    <LoadingButton
      loading={loading}
      loadingText="Authorizing..."
      onClick={handleAction}
      disabled={!isAuthenticated}
    >
      Perform Secure Action
    </LoadingButton>
  );
}

function App() {
  return (
    <BitcoinQueryProvider>
      <BitcoinAuthProvider config={{ apiUrl: '/api' }}>
        <AuthenticatedAction />
      </BitcoinAuthProvider>
    </BitcoinQueryProvider>
  );
}

API Integration

LoadingButton is commonly used with API calls:

// API endpoint example
app.post('/api/process', async (req, res) => {
  try {
    // Simulate processing
    await new Promise(resolve => setTimeout(resolve, 2000));
    
    res.json({ success: true, data: processedData });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// Frontend usage
const handleProcess = async () => {
  setLoading(true);
  try {
    const response = await fetch('/api/process', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
    
    if (!response.ok) throw new Error('Processing failed');
    
    const result = await response.json();
    console.log('Processed:', result);
  } finally {
    setLoading(false);
  }
};

Styling

LoadingButton uses Radix Themes styling system:

// Different variants
<LoadingButton variant="classic" loading={loading}>Classic</LoadingButton>
<LoadingButton variant="solid" loading={loading}>Solid</LoadingButton>
<LoadingButton variant="soft" loading={loading}>Soft</LoadingButton>
<LoadingButton variant="surface" loading={loading}>Surface</LoadingButton>
<LoadingButton variant="outline" loading={loading}>Outline</LoadingButton>
<LoadingButton variant="ghost" loading={loading}>Ghost</LoadingButton>

// Different sizes
<LoadingButton size="1" loading={loading}>Small</LoadingButton>
<LoadingButton size="2" loading={loading}>Medium</LoadingButton>
<LoadingButton size="3" loading={loading}>Large</LoadingButton>
<LoadingButton size="4" loading={loading}>Extra Large</LoadingButton>

// Colors
<LoadingButton color="orange" loading={loading}>Bitcoin Orange</LoadingButton>
<LoadingButton color="blue" loading={loading}>Blue</LoadingButton>
<LoadingButton color="green" loading={loading}>Success</LoadingButton>
<LoadingButton color="red" loading={loading}>Danger</LoadingButton>

// Custom styling
<LoadingButton
  loading={loading}
  className="custom-class"
  style={{ minWidth: '200px' }}
>
  Custom Styled
</LoadingButton>

Error Handling

Common Error Scenarios

import { LoadingButton } from 'bigblocks';

export default function ErrorHandling() {
  const [loading, setLoading] = useState(false);

  const handleAction = async () => {
    setLoading(true);
    
    try {
      await riskyOperation();
    } catch (error) {
      if (error.code === 'NETWORK_ERROR') {
        alert('Network error. Check your connection.');
      } else if (error.code === 'AUTH_REQUIRED') {
        alert('Please sign in to continue.');
      } else if (error.code === 'RATE_LIMITED') {
        alert('Too many requests. Please wait.');
      } else {
        alert('An unexpected error occurred.');
      }
      console.error('Operation failed:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <LoadingButton
      loading={loading}
      loadingText="Processing..."
      onClick={handleAction}
    >
      Perform Action
    </LoadingButton>
  );
}

Accessibility

LoadingButton includes built-in accessibility features:

  • ARIA attributes: Automatically sets aria-busy when loading
  • Disabled state: Prevents interaction during loading
  • Keyboard support: Full keyboard navigation
  • Screen readers: Loading state announced to assistive technology
// The component automatically handles:
// - aria-busy="true" when loading
// - aria-disabled="true" when disabled or loading
// - role="button"
// - tabIndex for keyboard navigation

Performance Optimization

Debounced Actions

import { LoadingButton } from 'bigblocks';
import { useCallback, useState } from 'react';
import { debounce } from 'lodash';

export default function DebouncedButton() {
  const [loading, setLoading] = useState(false);

  const performAction = async () => {
    setLoading(true);
    try {
      await apiCall();
    } finally {
      setLoading(false);
    }
  };

  const debouncedAction = useCallback(
    debounce(performAction, 500),
    []
  );

  return (
    <LoadingButton
      loading={loading}
      onClick={debouncedAction}
    >
      Debounced Action
    </LoadingButton>
  );
}

Best Practices

  1. Always handle errors: Use try/catch/finally to ensure loading state is cleared
  2. Provide loading text: Give users context about what's happening
  3. Disable during loading: Prevent duplicate submissions (automatic)
  4. Show success feedback: Update button text or show confirmation
  5. Consider timeout: Add maximum loading time for better UX
// Best practice example
const handleSubmit = async () => {
  setLoading(true);
  
  const timeout = setTimeout(() => {
    setLoading(false);
    alert('Operation timed out. Please try again.');
  }, 30000); // 30 second timeout
  
  try {
    const result = await performOperation();
    clearTimeout(timeout);
    setSuccess(true);
    
    // Reset success state after delay
    setTimeout(() => setSuccess(false), 3000);
  } catch (error) {
    clearTimeout(timeout);
    handleError(error);
  } finally {
    setLoading(false);
  }
};

Troubleshooting

Button not showing loading state

  • Verify loading prop is being passed correctly
  • Check that state is updating (use React DevTools)
  • Ensure async operation isn't completing instantly

Loading text not appearing

  • LoadingText replaces children when loading is true
  • Check that loadingText prop has a value
  • Verify loading state is true

Button remains disabled

  • Loading automatically disables the button
  • Check that loading state is reset in finally block
  • Verify no other disabled condition

Styling issues

  • LoadingButton uses Radix Themes styling
  • Ensure BitcoinThemeProvider is wrapping the component
  • Check for CSS conflicts with custom classes

API Reference

LoadingButton Props

interface LoadingButtonProps extends RadixButtonProps {
  loading?: boolean;       // Shows spinner and disables button
  loadingText?: string;    // Text shown when loading
}

// Inherited from RadixButtonProps:
// - variant: 'classic' | 'solid' | 'soft' | 'surface' | 'outline' | 'ghost'
// - size: '1' | '2' | '3' | '4'
// - color: string (theme color)
// - highContrast: boolean
// - radius: 'none' | 'small' | 'medium' | 'large' | 'full'
// - disabled: boolean
// - asChild: boolean
// - All standard button HTML attributes

Usage with React Hook Form

import { useForm } from 'react-hook-form';
import { LoadingButton } from 'bigblocks';

function FormExample() {
  const { handleSubmit, formState: { isSubmitting } } = useForm();

  const onSubmit = async (data) => {
    await processFormData(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* form fields */}
      
      <LoadingButton
        type="submit"
        loading={isSubmitting}
        loadingText="Submitting..."
      >
        Submit Form
      </LoadingButton>
    </form>
  );
}