Button
Interactive UI Explorer
아래에서 각 컴포넌트의 다양한 Variants와 States를 문서화된 형태로 테스트해볼 수 있습니다.
Live Preview
Button
다양한 variant와 size를 지원하는 버튼 컴포넌트입니다.
Variants
버튼의 다양한 스타일 variant
Primary (Default)
Live Preview
Secondary
Live Preview
Outline
Live Preview
Ghost
Live Preview
Danger
Live Preview
Sizes
버튼의 다양한 크기
Small
Live Preview
Medium
Live Preview
Large
Live Preview
States
버튼의 다양한 상태 (loading, disabled)
Loading
Live Preview
Disabled
Live Preview
Normal
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";