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.
Installation
npx bigblocks add loading-button
Import
import { LoadingButton } from 'bigblocks';
Props
Prop | Type | Required | Default | Description |
---|---|---|---|---|
loading | boolean | No | false | Shows loading spinner and optionally loading text |
loadingText | string | No | - | Text to display when loading (replaces children) |
children | ReactNode | Yes | - | Button content |
disabled | boolean | No | false | Disables the button (auto-disabled when loading) |
...RadixButtonProps | various | No | - | All Radix Button props are supported |
Inherited Radix Button Props
The LoadingButton extends Radix UI's Button component, inheriting all its props:
Prop | Type | Default | Description |
---|---|---|---|
variant | 'classic' | 'solid' | 'soft' | 'surface' | 'outline' | 'ghost' | 'solid' | Button visual style |
size | '1' | '2' | '3' | '4' | '2' | Button size |
color | string | - | Theme color |
highContrast | boolean | false | High contrast mode |
radius | 'none' | 'small' | 'medium' | 'large' | 'full' | - | Border radius |
asChild | boolean | false | Render 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
- Always handle errors: Use try/catch/finally to ensure loading state is cleared
- Provide loading text: Give users context about what's happening
- Disable during loading: Prevent duplicate submissions (automatic)
- Show success feedback: Update button text or show confirmation
- 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
Related Components
- AuthButton - Authentication-specific button
- SendBSVButton - Bitcoin sending with loading states
- QuickSendButton - Simplified BSV sending
- Modal - Often used with loading buttons
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>
);
}