Button
A versatile, production-ready button system with 8 visual variants, 4 sizes, loading spinner states, icon slots, and spring tap feedback.
Live preview from the library's source
Install
From EasyUI
npx shadcn@latest add Surajmaurya1/easyui/buttonFrom this collection
npx shadcn@latest add https://allcomps.vercel.app/r/easyui--button.jsonCode
'use client';
import React, { forwardRef } from 'react';
import { motion, type HTMLMotionProps } from 'framer-motion';
import { Loader2 } from 'lucide-react';
import { cn } from '../../lib/utils';
import { motionTransitions } from '../../lib/motion-tokens';
export type ButtonVariant =
| 'default'
| 'primary'
| 'secondary'
| 'outline'
| 'ghost'
| 'destructive'
| 'success'
| 'link'
| 'gradient';
export type ButtonSize = 'sm' | 'md' | 'lg' | 'icon';
export interface ButtonProps
extends Omit<HTMLMotionProps<'button'>, 'children'> {
/** Visual presentation style */
variant?: ButtonVariant;
/** Dimension scale */
size?: ButtonSize;
/** Loading state displaying an animated spinner */
isLoading?: boolean;
/** Text or element displayed during loading */
loadingText?: string;
/** Icon placed before the button children */
leftIcon?: React.ReactNode;
/** Icon placed after the button children */
rightIcon?: React.ReactNode;
/** Expand button to fill 100% of container width */
fullWidth?: boolean;
/** Button content */
children?: React.ReactNode;
}
const variantStyles: Record<ButtonVariant, string> = {
default:
'bg-[var(--text-primary)] text-[var(--bg)] hover:opacity-90 shadow-xs border border-transparent font-medium cursor-pointer',
primary:
'bg-[var(--text-primary)] text-[var(--bg)] hover:opacity-90 shadow-xs border border-transparent font-medium cursor-pointer',
secondary:
'bg-[var(--surface)] border border-[var(--border)] text-[var(--text-primary)] hover:bg-[var(--surface-hover)] hover:border-[var(--border-hover)] cursor-pointer',
outline:
'bg-transparent border border-[var(--border)] text-[var(--text-primary)] hover:bg-[var(--surface-hover)] hover:border-[var(--border-hover)] cursor-pointer',
ghost:
'bg-transparent text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--surface-hover)] border border-transparent cursor-pointer',
destructive:
'bg-rose-500/10 border border-rose-500/30 text-rose-500 dark:text-rose-400 hover:bg-rose-500 hover:text-white cursor-pointer',
success:
'bg-emerald-500/10 border border-emerald-500/30 text-emerald-600 dark:text-emerald-400 hover:bg-emerald-500 hover:text-white cursor-pointer',
link:
'bg-transparent text-[var(--text-primary)] underline-offset-4 hover:underline p-0 h-auto border-0 focus-ring shadow-none inline-flex cursor-pointer',
gradient:
'bg-[var(--surface)] border border-[var(--border)] text-[var(--text-primary)] hover:bg-[var(--surface-hover)] hover:border-[var(--border-hover)] relative overflow-hidden cursor-pointer',
};
const sizeStyles: Record<ButtonSize, string> = {
sm: 'h-8 px-3 text-xs rounded-md gap-1.5',
md: 'h-10 px-4.5 text-sm rounded-lg gap-2',
lg: 'h-12 px-6 text-base rounded-lg gap-2.5',
icon: 'h-10 w-10 p-0 rounded-lg justify-center shrink-0',
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
variant = 'primary',
size = 'md',
isLoading = false,
loadingText,
leftIcon,
rightIcon,
fullWidth = false,
disabled,
className,
children,
type = 'button',
...props
},
ref
) => {
const isDisabled = disabled || isLoading;
return (
<motion.button
ref={ref}
type={type}
disabled={isDisabled}
whileTap={isDisabled ? undefined : { scale: 0.97 }}
transition={motionTransitions.springSnappy}
aria-busy={isLoading}
className={cn(
'relative inline-flex items-center justify-center font-medium select-none focus-ring transition-colors duration-150',
variantStyles[variant],
variant !== 'link' && sizeStyles[size],
fullWidth && 'w-full',
isDisabled && 'opacity-30 cursor-not-allowed pointer-events-none',
className
)}
{...props}
>
{/* Subtle shimmer gradient for gradient variant */}
{variant === 'gradient' && (
<div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/5 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-1000 ease-in-out pointer-events-none"
aria-hidden="true"
/>
)}
{/* Loading Spinner or Left Icon */}
{isLoading ? (
<Loader2 className={cn('animate-spin shrink-0', size === 'sm' ? 'w-3 h-3' : 'w-4 h-4')} />
) : (
leftIcon && <span className="shrink-0">{leftIcon}</span>
)}
{/* Button Content */}
{isLoading && loadingText ? (
<span>{loadingText}</span>
) : (
children && <span>{children}</span>
)}
{/* Right Icon */}
{!isLoading && rightIcon && <span className="shrink-0">{rightIcon}</span>}
</motion.button>
);
}
);
Button.displayName = 'Button';
Other buttons
The same kind of component from the other libraries in the collection.