Variants
Four tones cover the most common product decisions without creating a sprawling button taxonomy.
SenButton keeps the interaction vocabulary compact: variants, sizes, disabled state, and loading state. That restraint makes product flows easier to scan and harder to misuse.
A clear surface, a clear next step, no extra decoration.
Four tones cover the most common product decisions without creating a sprawling button taxonomy.
Size maps to density. Use small inside compact toolbars, medium for forms, and large for high-confidence calls to action.
Preserve layout while the action is in progress.
Keep unavailable actions visible without inviting interaction.
Native button props pass through for normal React event handling.
This is the full portable implementation, including variants, sizes, loading state, disabled handling, and native button prop passthrough.
import React from 'react';
export type SenButtonVariant = 'primary' | 'secondary' | 'danger' | 'success';
export type SenButtonSize = 'sm' | 'md' | 'lg';
export interface SenButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: SenButtonVariant;
size?: SenButtonSize;
loading?: boolean;
className?: string;
children: React.ReactNode;
}
const variantStyles: Record<SenButtonVariant, string> = {
primary: 'bg-blue-500 text-white hover:bg-blue-600',
secondary: 'bg-purple-500 text-white hover:bg-purple-600',
danger: 'bg-red-500 text-white hover:bg-red-600',
success: 'bg-green-500 text-white hover:bg-green-600',
};
const sizeStyles: Record<SenButtonSize, string> = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-5 py-3 text-lg',
};
export function SenButton({
variant = 'primary',
size = 'md',
loading = false,
className = '',
children,
...props
}: SenButtonProps) {
const isDisabled = loading || props.disabled;
const isOnlyDisabled = !loading && props.disabled;
return (
<button
className={[
'relative inline-flex cursor-pointer items-center justify-center gap-2 rounded font-bold transition-colors duration-150',
variantStyles[variant],
sizeStyles[size],
isDisabled && 'cursor-not-allowed',
isOnlyDisabled && 'bg-gray-400 text-gray-700 hover:bg-gray-400',
className
].filter(Boolean).join(' ')}
disabled={isDisabled}
{...props}
>
{loading && (
<span className="absolute inset-0 flex items-center justify-center">
<svg width="16" height="16" fill="none" viewBox="0 0 16 16" className="animate-spin text-inherit"><circle cx="8" cy="8" r="7" stroke="currentColor" strokeWidth="2" opacity="0.25"/><path d="M15 8A7 7 0 1 1 8 1" stroke="currentColor" strokeWidth="2"/></svg>
</span>
)}
<span className={loading ? 'opacity-0' : ''}>{children}</span>
</button>
);
}