Checkbox

Interactive UI Explorer

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

Live Preview
이용약관에 동의합니다 (필수)
이메일 수신 동의
SMS 수신 동의
잠긴 옵션

Checkbox

애니메이션이 적용된 커스텀 체크박스 컴포넌트입니다.
실제 input은 숨기고 시각적 요소로 체크 상태를 표현하며, 체크 시 부드러운 애니메이션이 적용됩니다.

기본 체크박스

라벨과 함께 사용하는 기본 체크박스

Default
<Checkbox label="이용약관" checked={checked} onChange={...} />
Live Preview
Checked
<Checkbox label="기본 체크" checked={true} onChange={...} />
Live Preview
No Label
<Checkbox checked={checked} onChange={...} />
Live Preview

States

체크박스의 다양한 상태 (disabled, error, helper text)

Disabled
<Checkbox label="비활성화" disabled />
Live Preview
Checked Disabled
<Checkbox label="체크 비활성" checked disabled />
Live Preview
Error
<Checkbox label="에러 상태" error="필수 항목입니다." />

필수 항목입니다.

Live Preview
Helper Text
<Checkbox label="도움말 포함" helperText="선택 사항입니다." />

선택 사항입니다.

Live Preview
Implementation

제작 코드

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

Checkboxtypescript
"use client";

/**
 * Checkbox Component
 *
 * Premium animated checkbox with polished interactions.
 * Features smooth scaling, subtle hover ripple, and refined typography.
 */
import { InputHTMLAttributes, forwardRef, useId, useState, useEffect } from "react";
import { CheckIcon } from "@/components/Icon/check";
import { cn } from "@/lib/cn";

export interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type"> {
    label?: string;
    error?: string;
    helperText?: string;
    /** 다크 배경에서 사용 시 hover 효과·텍스트 색상을 어두운 테마에 맞게 조정 */
    variant?: "default" | "dark";
}

export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
    ({ className, label, error, helperText, id, disabled, checked, onChange, variant = "default", ...props }, ref) => {
        const generatedId = useId();
        const checkboxId = id ?? `checkbox-${generatedId.replace(/:/g, "-")}`;

        const [isChecked, setIsChecked] = useState(checked || false);

        useEffect(() => {
            setIsChecked(checked || false);
        }, [checked]);

        const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
            setIsChecked(e.target.checked);
            onChange?.(e);
        };

        const activeClasses = (checked: boolean, error?: string) => {
            if (error) return "";
            if (checked) return "shadow-[0_0_0_4px_rgba(15,23,42,0.06)] scale-105";
            return "";
        };

        return (
            <div className={cn("group/checkbox inline-flex flex-col items-center justify-center gap-1.5", className)}>
                <label
                    htmlFor={checkboxId}
                    className={cn(
                        "relative flex items-center gap-3 cursor-pointer select-none px-1 py-0.5 rounded-lg transition-all duration-300",
                        disabled
                            ? "opacity-40 cursor-not-allowed"
                            : variant === "dark"
                                ? "hover:bg-transparent"
                                : "hover:bg-slate-50"
                    )}
                >
                    {/* Invisible native input */}
                    <input
                        ref={ref}
                        type="checkbox"
                        id={checkboxId}
                        className="sr-only peer"
                        disabled={disabled}
                        checked={isChecked}
                        onChange={handleChange}
                        aria-invalid={error ? "true" : "false"}
                        aria-describedby={error ? `${checkboxId}-error` : helperText ? `${checkboxId}-helper` : undefined}
                        {...props}
                    />

                    {/* Visual box with ripple */}
                    <div className="relative flex items-center justify-center shrink-0">
                        {/* Hover ripple */}
                        <div
                            className={cn(
                                "absolute inset-0 -m-2 rounded-md transition-all duration-300 scale-0 group-hover/checkbox:scale-100 group-hover/checkbox:bg-slate-200/50",
                                isChecked && "group-hover/checkbox:bg-slate-900/5",
                                disabled && "hidden"
                            )}
                        />

                        {/* Outer square */}
                        <div
                            className={cn(
                                "w-5 h-5 rounded border-2 flex items-center justify-center transition-all duration-300",
                                error
                                    ? "border-rose-400 bg-rose-50/40"
                                    : isChecked
                                        ? "border-slate-900 bg-white"
                                        : "border-slate-300 bg-white hover:border-slate-400",
                                activeClasses(isChecked, error),
                                error
                                    ? "peer-focus-visible:ring-2 peer-focus-visible:ring-rose-500/20"
                                    : "peer-focus-visible:ring-2 peer-focus-visible:ring-violet-500/20"
                            )}
                        >
                            <CheckIcon
                                className={cn(
                                    "w-3 h-3 transition-all duration-200",
                                    isChecked ? "scale-100 opacity-100" : "scale-0 opacity-0",
                                    error ? "text-rose-600" : "text-slate-900"
                                )}
                            />
                        </div>
                    </div>

                    {/* Label text */}
                    {label && (
                        <span
                            className={cn(
                                "text-sm font-medium transition-colors duration-300",
                                variant === "dark"
                                    ? "text-slate-200"
                                    : isChecked ? "text-slate-900" : "text-slate-500",
                                disabled
                                    ? variant === "dark" ? "text-slate-400" : "text-slate-400"
                                    : variant === "dark"
                                        ? ""
                                        : "group-hover/checkbox:text-slate-800"
                            )}
                        >
                            {label}
                        </span>
                    )}
                </label>

                {/* Helper / error messages */}
                {(error || helperText) && (
                <div className="ml-[32px] space-y-1">
                    {error && (
                        <p id={`${checkboxId}-error`} className="text-[11px] font-bold text-rose-600 flex items-center gap-1 animate-in fade-in slide-in-from-left-1">
                            <span className="w-1 h-1 rounded-full bg-rose-600" />
                            {error}
                        </p>
                    )}
                    {helperText && !error && (
                        <p id={`${checkboxId}-helper`} className="text-[11px] font-medium text-slate-500 leading-tight">
                            {helperText}
                        </p>
                    )}
                </div>
                )}
            </div>
        );
    }
);

Checkbox.displayName = "Checkbox";