Input

Interactive UI Explorer

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

Live Preview
홍길동
user@bad

유효한 이메일이 아닙니다

수정 불가

Input

다양한 상태를 지원하는 입력 필드 컴포넌트입니다.

Basic Input

기본 입력 필드

Email
<Input label="이메일" placeholder="your@email.com" />
Live Preview
Password
<Input label="비밀번호" type="password" />
Live Preview

States

입력 필드의 다양한 상태 (error, disabled, helper text)

Error
<Input label="에러 상태" error="이 필드는 필수입니다." />

이 필드는 필수입니다.

Live Preview
Disabled
<Input label="비활성화" disabled />
Live Preview
Helper Text
<Input label="도움말" helperText="도움말 텍스트가 여기에 표시됩니다." />

도움말 텍스트가 여기에 표시됩니다.

Live Preview
Implementation

제작 코드

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

Inputtypescript
/**
 * Input 컴포넌트
 *
 * Select·Textarea·Checkbox·Radio와 동일 디자인 토큰:
 *  - 기본 border slate-300 → hover slate-400
 *  - focus: violet-500 border + violet-500/20 ring
 *  - error: rose-400 border, focus 시 rose-500 + rose-500/20 ring
 *  - placeholder slate-400, body text slate-900
 *  - rounded-xl + shadow-sm
 */
import { InputHTMLAttributes, forwardRef, useId } from 'react';
import { cn } from '@/lib/cn';

export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
  label?: string;
  error?: string;
  helperText?: string;
  fullWidth?: boolean;
}

export const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ className, label, error, helperText, fullWidth, id, ...props }, ref) => {
    const generatedId = useId();
    const inputId = id ?? `input-${generatedId.replace(/:/g, '-')}`;

    return (
      <div className={cn('space-y-1.5', fullWidth && 'w-full')}>
        {label && (
          <label
            htmlFor={inputId}
            className="block text-xs font-semibold text-slate-700"
          >
            {label}
          </label>
        )}
        <input
          ref={ref}
          id={inputId}
          className={cn(
            'w-full rounded-xl border bg-white px-4 py-2 text-sm text-slate-900 shadow-sm transition-colors',
            'placeholder:text-slate-400',
            'focus:outline-none',
            'disabled:cursor-not-allowed disabled:bg-slate-50 disabled:opacity-60',
            'read-only:bg-slate-50 read-only:cursor-default',
            error
              ? 'border-rose-400 focus:border-rose-500 focus:ring-2 focus:ring-rose-500/20'
              : 'border-slate-300 hover:border-slate-400 focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20',
            className,
          )}
          aria-invalid={error ? 'true' : 'false'}
          aria-describedby={error ? `${inputId}-error` : helperText ? `${inputId}-helper` : undefined}
          {...props}
        />
        {error && (
          <p id={`${inputId}-error`} className="text-xs font-medium text-rose-600">
            {error}
          </p>
        )}
        {helperText && !error && (
          <p id={`${inputId}-helper`} className="text-xs text-slate-500">
            {helperText}
          </p>
        )}
      </div>
    );
  },
);

Input.displayName = 'Input';