Button 컴포넌트

#button#component

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

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

export interface ButtonProps
  extends ButtonHTMLAttributesHTMLButtonElement {
  variant?: ButtonVariant;
  size?: ButtonSize;
  isLoading?: boolean;
}

const variantStyles: RecordButtonVariant, string = {
  primary:
    "relative overflow-hidden border-0 bg-gradient-to-br from-slate-700 via-slate-800 to-slate-900 text-white shadow-lg shadow-slate-900/40 ring-1 ring-slate-500/30 transition-all duration-200 hover:shadow-xl hover:shadow-slate-800/50 hover:ring-slate-400/40 hover:brightness-110 active:scale-[0.98] active:brightness-95 focus:ring-2 focus:ring-slate-400/60 focus:ring-offset-2 focus:ring-offset-slate-900 before:absolute before:inset-0 before:bg-gradient-to-b before:from-white/10 before:to-transparent before:opacity-0 before:transition-opacity hover:before:opacity-100",
  secondary:
    "border border-slate-400/40 bg-gradient-to-b from-slate-100 to-slate-200 text-slate-800 shadow-md shadow-slate-300/50 ring-1 ring-slate-300/50 transition-all duration-200 hover:border-slate-400/60 hover:shadow-lg hover:shadow-slate-400/40 hover:from-slate-200 hover:to-slate-300 active:scale-[0.98] focus:ring-2 focus:ring-slate-400/50 focus:ring-offset-2 focus:ring-offset-white",
  outline:
    "border-2 border-slate-600 bg-transparent text-slate-900 transition-all duration-200 hover:border-slate-700 hover:bg-slate-100 hover:text-slate-900 hover:shadow-md active:scale-[0.98] focus:ring-2 focus:ring-slate-400/50 focus:ring-offset-2 focus:ring-offset-slate-100",
  ghost:
    "border-0 bg-transparent text-slate-900 transition-all duration-200 hover:bg-slate-100 hover:text-slate-900 hover:shadow-sm active:scale-[0.98] focus:ring-2 focus:ring-slate-400/40 focus:ring-offset-2 focus:ring-offset-slate-100",
  danger:
    "relative overflow-hidden border-0 bg-gradient-to-br from-red-500 via-red-600 to-red-700 text-white shadow-lg shadow-red-900/40 ring-1 ring-red-400/40 transition-all duration-200 hover:shadow-xl hover:shadow-red-800/50 hover:ring-red-300/50 hover:brightness-110 active:scale-[0.98] active:brightness-95 focus:ring-2 focus:ring-red-400/60 focus:ring-offset-2 focus:ring-offset-slate-900 before:absolute before:inset-0 before:bg-gradient-to-b before:from-white/15 before:to-transparent before:opacity-0 before:transition-opacity hover:before:opacity-100",
};

const sizeStyles: RecordButtonSize, 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 = forwardRefHTMLButtonElement, ButtonProps(
  (
    {
      className,
      variant = "primary",
      size = "md",
      isLoading,
      disabled,
      children,
      ...props
    },
    ref
  ) = {
    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
        )}
        span className="relative z-10 inline-flex items-center justify-center gap-2"
          {children}
        /span
      /button
    );
  }
);

Button.displayName = "Button";



Button 컴포넌트


그라데이션, 그림자, 호버/포커스 효과가 적용된 재사용 버튼 컴포넌트입니다.


---


사용법


tsx

import { Button } from "@/components/Button";


// 기본 (primary, md)

Button저장하기/Button


// variant / size 지정

Button variant="secondary" size="lg"다음/Button


// 로딩 상태

Button isLoading제출 중.../Button


---


Props


Prop 타입 기본값 설명

--------------------------

variant "primary" "secondary" "outline" "ghost" "danger" "primary" 버튼 스타일 종류

size "sm" "md" "lg" "md" 버튼 크기

isLoading boolean false true면 스피너 표시 + 비활성화

disabled boolean - HTML button disabled (표준)

className string - 추가 Tailwind 등 클래스

기타 - - button에 전달되는 모든 속성 지원 (onClick, type, aria-* 등)


---


Variant별 설명


Variant 용도 특징

---------------------

primary 메인 액션 (저장, 제출 등) 다크 그라데이션, 강한 그림자, 호버 시 밝기 증가

secondary 보조 액션 밝은 그라데이션, 테두리, 덜 강조

outline 보조/선택 가능 액션 투명 배경 + 테두리, 호버 시 연한 배경

ghost 툴바, 목록 내 액션 투명, 호버 시만 배경 표시

danger 삭제위험 액션 빨간 그라데이션, primary와 같은 스타일 톤


---


Size별 설명


Size 패딩텍스트 용도

--------------------------

sm px-3 py-1.5, text-sm, rounded-xl 테이블 행, 컴팩트 UI

md px-4 py-2.5, text-base, rounded-xl 일반 폼카드 버튼

lg px-6 py-3, text-lg, rounded-2xl CTA, 랜딩용 큰 버튼


---



47

댓글