/**
* 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, 랜딩용 큰 버튼
---