Switch

Interactive UI Explorer

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

Live Preview

Switch

즉시 적용되는 ON/OFF 토글. Checkbox와 의미 분리 — 폼 제출 후 적용되는 선택은 Checkbox, 즉시 반영되는 설정은 Switch.

Sizes

sm · md · lg 3단계.

SM
<Switch size="sm" checked onChange={...} />
Live Preview
MD
<Switch size="md" checked onChange={...} />
Live Preview
LG
<Switch size="lg" checked onChange={...} />
Live Preview

With label & description

라벨/설명 슬롯 — 설정 화면에서 즐겨 쓰는 패턴.

Source Code
<Switch
  checked={notif}
  onChange={setNotif}
  label="알림 받기"
  description="새 댓글이 달리면 이메일로 알려드립니다."
/>
Live Preview
Implementation

제작 코드

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

Switchtypescript
'use client';

/**
 * Switch — 즉시 적용되는 ON/OFF 토글.
 *
 * Checkbox와 의미 분리: 폼 제출 후 적용되는 선택 = Checkbox, 즉시 반영되는 설정 = Switch.
 * - sizes: sm / md / lg
 * - label / description 슬롯 (좌측 텍스트)
 * - controlled (checked + onChange)
 */

import { forwardRef, useId, type InputHTMLAttributes, type ReactNode } from 'react';
import { cn } from '@/lib/cn';

export type SwitchSize = 'sm' | 'md' | 'lg';

export interface SwitchProps
  extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'onChange' | 'checked' | 'type'> {
  checked: boolean;
  onChange: (checked: boolean) => void;
  size?: SwitchSize;
  /** 좌측에 노출할 라벨 텍스트(또는 노드). */
  label?: ReactNode;
  /** label 아래 부가 설명. */
  description?: ReactNode;
  /** label/description 영역 클릭으로도 토글되게 할지 (default: true) */
  labelClickable?: boolean;
}

const sizeStyles: Record<SwitchSize, { track: string; thumb: string; translate: string }> = {
  sm: { track: 'h-5 w-9', thumb: 'h-4 w-4', translate: 'translate-x-4' },
  md: { track: 'h-6 w-11', thumb: 'h-5 w-5', translate: 'translate-x-5' },
  lg: { track: 'h-7 w-[3.25rem]', thumb: 'h-6 w-6', translate: 'translate-x-6' },
};

export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
  (
    {
      checked,
      onChange,
      size = 'md',
      label,
      description,
      labelClickable = true,
      disabled,
      className,
      id,
      ...rest
    },
    ref,
  ) => {
    const reactId = useId();
    const inputId = id ?? `switch-${reactId}`;
    const sz = sizeStyles[size];

    const toggle = () => {
      if (disabled) return;
      onChange(!checked);
    };

    return (
      <label
        htmlFor={inputId}
        className={cn(
          'inline-flex items-center gap-3 select-none',
          disabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer',
          className,
        )}
      >
        <span className="relative inline-flex shrink-0 items-center">
          <input
            ref={ref}
            id={inputId}
            type="checkbox"
            role="switch"
            checked={checked}
            disabled={disabled}
            aria-checked={checked}
            onChange={(e) => onChange(e.target.checked)}
            className="peer sr-only"
            {...rest}
          />
          <span
            aria-hidden
            className={cn(
              'inline-flex items-center rounded-full transition-colors duration-200 ease-out',
              'peer-focus-visible:ring-2 peer-focus-visible:ring-slate-300 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-white',
              sz.track,
              checked ? 'bg-slate-900' : 'bg-slate-200',
            )}
          >
            <span
              className={cn(
                'inline-block rounded-full bg-white shadow ring-1 ring-slate-200/80 transition-transform duration-200 ease-out',
                sz.thumb,
                'translate-x-0.5',
                checked && sz.translate,
              )}
            />
          </span>
        </span>

        {(label || description) && (
          <span
            className={cn(
              'flex flex-col leading-tight',
              !labelClickable && 'pointer-events-none',
            )}
            onClick={labelClickable ? undefined : (e) => e.preventDefault()}
            onKeyDown={(e) => {
              if (e.key === ' ' || e.key === 'Enter') {
                e.preventDefault();
                toggle();
              }
            }}
          >
            {label && <span className="text-sm font-semibold text-slate-900">{label}</span>}
            {description && (
              <span className="mt-0.5 text-xs text-slate-500">{description}</span>
            )}
          </span>
        )}
      </label>
    );
  },
);

Switch.displayName = 'Switch';