Switch
Interactive UI Explorer
아래에서 각 컴포넌트의 다양한 Variants와 States를 문서화된 형태로 테스트해볼 수 있습니다.
Live Preview
Switch
즉시 적용되는 ON/OFF 토글. Checkbox와 의미 분리 — 폼 제출 후 적용되는 선택은 Checkbox, 즉시 반영되는 설정은 Switch.
Sizes
sm · md · lg 3단계.
SM
Live Preview
MD
Live Preview
LG
Live Preview
With label & description
라벨/설명 슬롯 — 설정 화면에서 즐겨 쓰는 패턴.
Source Code
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';