SenUI Logo

Buttons with simple choices and obvious intent.

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.

Variants

Four tones cover the most common product decisions without creating a sprawling button taxonomy.

Sizes

Size maps to density. Use small inside compact toolbars, medium for forms, and large for high-confidence calls to action.

Loading

Preserve layout while the action is in progress.

Disabled

Keep unavailable actions visible without inviting interaction.

Click handler

Native button props pass through for normal React event handling.

Use SenButton standalone

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>
  );
}