Button

Interactive UI Explorer

아래에서 각 컴포넌트의 다양한 Variants States를 문서화된 형태로 테스트해볼 수 있습니다.

Live Preview

Button

다양한 variant와 size를 지원하는 버튼 컴포넌트입니다.

Variants

버튼의 다양한 스타일 variant

Primary (Default)
<Button variant="primary">Primary</Button>
Live Preview
Secondary
<Button variant="secondary">Secondary</Button>
Live Preview
Outline
<Button variant="outline">Outline</Button>
Live Preview
Ghost
<Button variant="ghost">Ghost</Button>
Live Preview
Danger
<Button variant="danger">Danger</Button>
Live Preview

Sizes

버튼의 다양한 크기

Small
<Button size="sm">Small</Button>
Live Preview
Medium
<Button size="md">Medium</Button>
Live Preview
Large
<Button size="lg">Large</Button>
Live Preview

States

버튼의 다양한 상태 (loading, disabled)

Loading
<Button isLoading>Loading</Button>
Live Preview
Disabled
<Button disabled>Disabled</Button>
Live Preview
Normal
<Button>Normal</Button>
Live Preview
Implementation

제작 코드

이 컴포넌트가 실제로 어떻게 구현되어 있는지 — 본체 .tsx 파일을 그대로 보여줍니다. variant 매핑·접근성 처리·forwardRef 패턴 등 디테일을 그대로 확인할 수 있어요.

Buttontypescript
/**
 * Button 컴포넌트
 *
 * 그라데이션, 그림자, 호버/포커스 효과를 적용한 variant별 스타일링.
 */
import { ButtonHTMLAttributes, forwardRef } from "react";
import { cn } from "@/lib/cn";

export type ButtonVariant =
  | "primary"
  | "secondary"
  | "outline"
  | "ghost"
  | "danger"
  | "premium"
  | "neon";
export type ButtonSize = "sm" | "md" | "lg";

export interface ButtonProps
  extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: ButtonVariant;
  size?: ButtonSize;
  isLoading?: boolean;
}

const variantStyles: Record<ButtonVariant, string> = {
  primary:
    "bg-slate-900 text-white border border-slate-800 shadow-sm shadow-slate-900/20 transition-all duration-300 hover:bg-slate-800 hover:shadow-md hover:shadow-slate-900/40 hover:-translate-y-0.5 active:translate-y-0 active:scale-[0.98] focus:ring-2 focus:ring-slate-400/50 focus:ring-offset-2 focus:ring-offset-white",
  secondary:
    "bg-white/70 backdrop-blur-md text-slate-800 border border-slate-200/50 shadow-sm shadow-slate-200/50 transition-all duration-300 hover:bg-white hover:border-slate-300 hover:shadow-md hover:shadow-slate-300/50 hover:-translate-y-0.5 active:translate-y-0 active:scale-[0.98] focus:ring-2 focus:ring-slate-300 focus:ring-offset-2 focus:ring-offset-white",
  outline:
    "bg-transparent text-slate-700 border-2 border-slate-300 transition-all duration-300 hover:border-slate-800 hover:text-slate-900 hover:bg-slate-50 hover:shadow-sm hover:-translate-y-0.5 active:translate-y-0 active:scale-[0.98] focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:ring-offset-white",
  ghost:
    "bg-transparent text-slate-600 transition-all duration-300 hover:text-slate-900 hover:bg-slate-100/80 active:scale-[0.98] focus:ring-2 focus:ring-slate-200 focus:ring-offset-2 focus:ring-offset-white",
  danger:
    "bg-red-500 text-white border border-red-600 shadow-sm shadow-red-500/20 transition-all duration-300 hover:bg-red-600 hover:border-red-700 hover:shadow-[0_0_15px_rgba(239,68,68,0.5)] hover:-translate-y-0.5 active:translate-y-0 active:scale-[0.98] focus:ring-2 focus:ring-red-500/50 focus:ring-offset-2 focus:ring-offset-white",
  premium:
    "relative overflow-hidden bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500 text-white font-bold tracking-wide shadow-[0_0_20px_rgba(168,85,247,0.4)] ring-1 ring-white/30 transition-all duration-500 hover:shadow-[0_0_30px_rgba(168,85,247,0.6)] hover:scale-[1.03] active:scale-[0.97] before:absolute before:inset-0 before:bg-gradient-to-t before:from-black/20 before:to-transparent before:opacity-0 hover:before:opacity-100 after:absolute after:inset-0 after:bg-gradient-to-tr after:from-transparent after:via-white/40 after:to-transparent after:-translate-x-[150%] hover:after:translate-x-[150%] after:transition-transform after:duration-[1.5s] after:ease-in-out",
  neon:
    "bg-transparent text-cyan-600 border border-cyan-500/50 shadow-[0_0_10px_rgba(6,182,212,0.1)] transition-all duration-300 hover:bg-cyan-500/10 hover:border-cyan-500 hover:shadow-[0_0_20px_rgba(6,182,212,0.6),inset_0_0_10px_rgba(6,182,212,0.3)] hover:text-cyan-500 hover:-translate-y-0.5 active:translate-y-0 active:scale-[0.98] focus:ring-2 focus:ring-cyan-500/50 focus:ring-offset-2 focus:ring-offset-white",
};

const sizeStyles: Record<ButtonSize, string> = {
  sm: "px-3 py-1.5 text-sm rounded-xl gap-1.5",
  md: "px-4 py-2.5 text-base rounded-xl gap-2",
  lg: "px-6 py-3 text-lg rounded-2xl gap-2.5",
};

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  (
    {
      className,
      variant = "primary",
      size = "md",
      isLoading,
      disabled,
      children,
      ...props
    },
    ref
  ) => {
    const hasRenderableChildren =
      children != null && children !== false && children !== true;
    return (
      <button
        ref={ref}
        className={cn(
          "inline-flex items-center justify-center font-semibold tracking-tight antialiased",
          "focus:outline-none disabled:pointer-events-none disabled:opacity-50",
          "cursor-pointer select-none",
          variantStyles[variant],
          sizeStyles[size],
          className
        )}
        disabled={disabled || isLoading}
        {...props}
      >
        {isLoading && (
          <svg
            className="relative z-10 h-4 w-4 shrink-0 animate-spin motion-reduce:hidden"
            xmlns="http://www.w3.org/2000/svg"
            fill="none"
            viewBox="0 0 24 24"
            aria-hidden
          >
            <circle
              className="opacity-25"
              cx="12"
              cy="12"
              r="10"
              stroke="currentColor"
              strokeWidth="4"
            />
            <path
              className="opacity-75"
              fill="currentColor"
              d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
            />
          </svg>
        )}
        {hasRenderableChildren ? (
          <span className="relative z-10 inline-flex items-center justify-center gap-2">
            {children}
          </span>
        ) : null}
      </button>
    );
  }
);

Button.displayName = "Button";