Prime Button
An interactive button component that manages async action states (loading, success, error) with built-in layout animations and auto-reset functionality.
Basic Usage
A straightforward implementation of the button's core state transitions. This example handles the actionState externally to trigger the loading and success animations.
Variants & Sizes
Demonstrates the flexibility of the component across different visual styles and sizes. The button inherits standard Shadcn variants (destructive, outline, secondary) while maintaining its animation physics.
Custom Text & Error Handling
Shows how to customize the feedback text for specific contexts (e.g., "Joining..." vs "Uploading...") and visualize error states when an action fails.
Installation
CLI (Recommended)
npx shadcn@latest add https://satisui.xyz/r/prime-button.jsonManual
- Install the required dependencies:
npm install motion
# or
yarn add motion
# or
pnpm add motion- Copy and paste the following code into your project:
'use client';
import * as React from 'react';
import { AnimatePresence, motion, type Variants } from 'motion/react';
import { Loader2, Check, X } from 'lucide-react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
'relative inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 select-none overflow-hidden',
{
variants: {
variant: {
default:
'bg-primary text-primary-foreground shadow-sm hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline:
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
const textVariants: Variants = {
hidden: {
y: 12,
opacity: 0,
filter: 'blur(4px)',
},
visible: {
y: 0,
opacity: 1,
filter: 'blur(0px)',
transition: {
type: 'spring',
stiffness: 500,
damping: 25,
mass: 0.5,
},
},
exit: {
y: -12,
opacity: 0,
filter: 'blur(4px)',
transition: {
duration: 0.15,
},
},
};
type ActionState = 'idle' | 'loading' | 'success' | 'error';
interface PrimeButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
/** Current interactive state of the button. */
actionState?: ActionState;
/** Text to display during the 'loading' state. */
loadingText?: string;
/** Text to display during the 'success' state. */
successText?: string;
/** Text to display during the 'error' state. */
errorText?: string;
/** Duration (in ms) before the button reverts from success/error back to idle. */
resetDelay?: number;
}
/**
* An interactive button component that manages async action states (loading, success, error)
* with built-in layout animations and auto-reset functionality.
*/
export const PrimeButton = React.forwardRef<
HTMLButtonElement,
PrimeButtonProps
>(
(
{
className,
variant,
size,
actionState = 'idle',
loadingText = 'Processing',
successText = 'Saved',
errorText = 'Error',
resetDelay = 2500,
children,
onClick,
...props
},
ref
) => {
const [internalState, setInternalState] =
React.useState<ActionState>(actionState);
React.useEffect(() => {
setInternalState(actionState);
}, [actionState]);
React.useEffect(() => {
if (
(internalState === 'success' || internalState === 'error') &&
resetDelay
) {
const timer = setTimeout(() => {
setInternalState('idle');
}, resetDelay);
return () => clearTimeout(timer);
}
}, [internalState, resetDelay]);
const stateStyles = {
idle: '',
loading: 'cursor-wait opacity-90',
success:
'bg-emerald-600 text-white hover:bg-emerald-600 border-emerald-600 ring-offset-emerald-600',
error:
'bg-red-600 text-white hover:bg-red-600 border-red-600 ring-offset-red-600',
};
const isIconOnly = size === 'icon';
return (
<motion.button
ref={ref}
// Automates width animations when content changes (e.g., "Save" -> "Saving...")
layout
className={cn(
buttonVariants({ variant, size, className }),
stateStyles[internalState],
'shadow-[inset_0_1px_0_0_rgba(255,255,255,0.1)]'
)}
onClick={onClick}
whileTap={internalState === 'idle' ? { scale: 0.98 } : undefined}
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
{...(props as any)} // Cast required to resolve conflict between Motion props and HTMLButton attributes
>
<AnimatePresence mode='popLayout' initial={false}>
{internalState === 'loading' ? (
<motion.span
key='loading'
variants={textVariants}
initial='hidden'
animate='visible'
exit='exit'
className='flex items-center gap-2'
>
<Loader2 className='h-4 w-4 animate-spin' />
{!isIconOnly && <span>{loadingText}</span>}
</motion.span>
) : internalState === 'success' ? (
<motion.span
key='success'
variants={textVariants}
initial='hidden'
animate='visible'
exit='exit'
className='flex items-center gap-2 font-semibold'
>
<Check className='h-4 w-4' strokeWidth={3} />
{!isIconOnly && <span>{successText}</span>}
</motion.span>
) : internalState === 'error' ? (
<motion.span
key='error'
variants={textVariants}
initial='hidden'
animate='visible'
exit='exit'
className='flex items-center gap-2 font-semibold'
>
<X className='h-4 w-4' strokeWidth={3} />
{!isIconOnly && <span>{errorText}</span>}
</motion.span>
) : (
<motion.span
key='idle'
variants={textVariants}
initial='hidden'
animate='visible'
exit='exit'
className='flex items-center gap-2'
>
{children}
</motion.span>
)}
</AnimatePresence>
</motion.button>
);
}
);
PrimeButton.displayName = 'PrimeButton';Usage
The PrimeButton component handles state transitions internally for visual feedback, but requires the actionState prop to be controlled by the parent.
import { useState } from 'react';
import { PrimeButton } from '@/components/ui/prime-button';
export function PrimeButtonDemo() {
const [status, setStatus] = useState<
'idle' | 'loading' | 'success' | 'error'
>('idle');
const handleClick = () => {
setStatus('loading');
// Simulate an async action
setTimeout(() => {
setStatus('success');
}, 2000);
};
return (
<div className='flex flex-col items-center gap-4'>
{/* Default Usage */}
<PrimeButton actionState={status} onClick={handleClick}>
Save Changes
</PrimeButton>
{/* Customized Usage */}
<PrimeButton
variant='outline'
actionState={status}
loadingText='Uploading...'
successText='Complete!'
onClick={handleClick}
>
Upload File
</PrimeButton>
</div>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
actionState | 'idle' | 'loading' | 'success' | 'error' | 'idle' | Current interactive state of the button. |
loadingText | string | 'Processing' | Text to display during the 'loading' state. |
successText | string | 'Saved' | Text to display during the 'success' state. |
errorText | string | 'Error' | Text to display during the 'error' state. |
resetDelay | number | 2500 | Duration (in ms) before the button reverts from success/error back to idle. |
variant | string | 'default' | Visual style variant (default, destructive, outline, secondary, ghost, link). |
size | string | 'default' | Button size (default, sm, lg, icon). |