OtpInput

Data Input

input-otp 기반의 비공식 인증번호 입력 컴포넌트.

Usage

SMS·이메일 인증번호처럼 고정 자릿수 코드를 입력받는 폼

import

import

import { OtpInput } from '@mildang/design-system/unofficial/OtpInput';

OtpInput 하나만 가져오면 OtpInput.Root · OtpInput.Slot · OtpInput.Separator 를 그 아래에서 쓸 수 있다.

예제를 복사해 쓸 때 필요한 준비

@mildang/styled-system 은 이 저장소에서 Panda 가 생성하는 산출물이다. 저장소 안에서는 turbo run ship 이후 쓸 수 있고, 패키지 소비자는 자기 Panda 산출물이나 다른 레이아웃 수단으로 바꿔야 한다.

Anatomy

tsx

import { OtpInput } from '@mildang/design-system/unofficial/OtpInput';

export default function Example() {
  return (
    <OtpInput>
      <OtpInput.Slot />
      <OtpInput.Separator />
    </OtpInput>
  );
}

부품

필수 여부

반복

위치

OtpInput.Root

필수

OtpInput.Slot

선택

직접 배치하지 않는다 — Root가 slots 배열을 순회하며 자동으로 렌더한다.

OtpInput.Separator

선택

직접 배치하지 않는다 — separator prop으로 내용을 바꾸거나 null로 숨긴다.

  • 실사용에서는 OtpInput.Root 하나만 쓰면 된다 — Slot·Separator는 Root가 내부에서 자동으로 조합한다.
  • Slot·Separator는 커스텀 렌더링이 필요할 때 재사용할 수 있도록 노출된 하위 컴포넌트다.

API Reference

OtpInput.Slot

문자 한 칸을 보여주는 슬롯이다. Root가 내부에서 자동으로 렌더한다.

Prop

Type

Default

className

string

지정 안 함

disabled

boolean

false

size

OtpInputSize

md

status

OtpInputStatus

default

OtpInput.Separator

슬롯 그룹 사이 구분자다. Root가 groupSize 기준으로 자동으로 렌더한다.

Prop

Type

Default

className

string

지정 안 함

disabled

boolean

false

status

OtpInputStatus

default

예제

기본 사용

6자리 인증번호를 입력받는 기본형이다. 입력값을 아래에 함께 보여준다.







import { OtpInput, type OtpInputRootProps } from '@mildang/design-system/unofficial/OtpInput';
import { useState } from 'react';
import { Text } from '@mildang/design-system/Text';
import { css } from '@mildang/styled-system/css';

const demoColumnStyle = css({
  display: 'flex',
  flexDirection: 'column',
  gap: '16',
  minWidth: 0,
});

const ControlledOtpInput = (args: OtpInputRootProps) => {
  const [value, setValue] = useState(args.value ?? '');

  return (
    <div className={demoColumnStyle}>
      <OtpInput.Root {...args} value={value} onChange={setValue} />
      <Text variant="caption-lg" color="neutral.text.low">
        입력값: {value || '-'}
      </Text>
    </div>
  );
};

const STORY_DEFAULT_ARGS = { ...({}), ...({}) } as OtpInputRootProps;

const OtpInputBasicExampleRender = (args: OtpInputRootProps) => <ControlledOtpInput {...args} />;

export default function OtpInputBasicExample(props: Partial<OtpInputRootProps>) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as OtpInputRootProps;
  return OtpInputBasicExampleRender(mergedProps);
}

Sizes

sm·md·lg 세 크기의 슬롯을 나란히 비교한다.

코드

import { Text } from '@mildang/design-system/Text';
import { OtpInput } from '@mildang/design-system/unofficial/OtpInput';
import { css } from '@mildang/styled-system/css';

const demoColumnStyle = css({
  display: 'flex',
  flexDirection: 'column',
  gap: '16',
  minWidth: 0,
});

const sampleRowStyle = css({
  display: 'flex',
  alignItems: 'center',
  gap: '16',
});

const labelStyle = css({
  width: '80',
  flexShrink: 0,
});

const OtpInputSizesExample = () => (
    <div className={demoColumnStyle}>
      {(['sm', 'md', 'lg'] as const).map((size) => (
        <div key={size} className={sampleRowStyle}>
          <Text className={labelStyle} variant="caption-lg" color="neutral.text.low">
            {size}
          </Text>
          <OtpInput.Root size={size} defaultValue="123" aria-label={`${size} 인증번호`} />
        </div>
      ))}
    </div>
  );

export default OtpInputSizesExample;

상태

disabled·error·warning·success 네 상태를 나란히 비교한다.

코드

import { Text } from '@mildang/design-system/Text';
import { OtpInput } from '@mildang/design-system/unofficial/OtpInput';
import { css } from '@mildang/styled-system/css';

const demoColumnStyle = css({
  display: 'flex',
  flexDirection: 'column',
  gap: '16',
  minWidth: 0,
});

const sampleRowStyle = css({
  display: 'flex',
  alignItems: 'center',
  gap: '16',
});

const labelStyle = css({
  width: '80',
  flexShrink: 0,
});

const OtpInputStatusExample = () => (
    <div className={demoColumnStyle}>
      <div className={sampleRowStyle}>
        <Text className={labelStyle} variant="caption-lg" color="neutral.text.low">
          default
        </Text>
        <OtpInput.Root defaultValue="123456" aria-label="기본 인증번호" />
      </div>
      <div className={sampleRowStyle}>
        <Text className={labelStyle} variant="caption-lg" color="critical.text.base">
          error
        </Text>
        <OtpInput.Root error defaultValue="123456" aria-label="오류 인증번호" />
      </div>
      <div className={sampleRowStyle}>
        <Text className={labelStyle} variant="caption-lg" color="warning.text.base">
          warning
        </Text>
        <OtpInput.Root warning defaultValue="123456" aria-label="경고 인증번호" />
      </div>
      <div className={sampleRowStyle}>
        <Text className={labelStyle} variant="caption-lg" color="positive.text.base">
          success
        </Text>
        <OtpInput.Root success defaultValue="123456" aria-label="성공 인증번호" />
      </div>
    </div>
  );

export default OtpInputStatusExample;

4자리 코드

maxLength를 4로 줄이고 groupSize를 해제한 짧은 코드 형태다.







import { OtpInput, type OtpInputRootProps } from '@mildang/design-system/unofficial/OtpInput';
import { useState } from 'react';
import { Text } from '@mildang/design-system/Text';
import { css } from '@mildang/styled-system/css';

const demoColumnStyle = css({
  display: 'flex',
  flexDirection: 'column',
  gap: '16',
  minWidth: 0,
});

const ControlledOtpInput = (args: OtpInputRootProps) => {
  const [value, setValue] = useState(args.value ?? '');

  return (
    <div className={demoColumnStyle}>
      <OtpInput.Root {...args} value={value} onChange={setValue} />
      <Text variant="caption-lg" color="neutral.text.low">
        입력값: {value || '-'}
      </Text>
    </div>
  );
};

const STORY_DEFAULT_ARGS = { ...({}), ...({
    maxLength: 4,
    groupSize: undefined,
    'aria-label': '4자리 인증번호',
  }) } as OtpInputRootProps;

const OtpInputFourDigitsExampleRender = (args: OtpInputRootProps) => <ControlledOtpInput {...args} />;

export default function OtpInputFourDigitsExample(props: Partial<OtpInputRootProps>) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as OtpInputRootProps;
  return OtpInputFourDigitsExampleRender(mergedProps);
}

구분선 없이

separator를 null로 주어 슬롯을 구분선 없이 붙여서 보여준다.







import { OtpInput, type OtpInputRootProps } from '@mildang/design-system/unofficial/OtpInput';
import { useState } from 'react';
import { Text } from '@mildang/design-system/Text';
import { css } from '@mildang/styled-system/css';

const demoColumnStyle = css({
  display: 'flex',
  flexDirection: 'column',
  gap: '16',
  minWidth: 0,
});

const ControlledOtpInput = (args: OtpInputRootProps) => {
  const [value, setValue] = useState(args.value ?? '');

  return (
    <div className={demoColumnStyle}>
      <OtpInput.Root {...args} value={value} onChange={setValue} />
      <Text variant="caption-lg" color="neutral.text.low">
        입력값: {value || '-'}
      </Text>
    </div>
  );
};

const STORY_DEFAULT_ARGS = { ...({}), ...({
    separator: null,
    'aria-label': '구분선 없는 인증번호',
  }) } as OtpInputRootProps;

const OtpInputWithoutSeparatorExampleRender = (args: OtpInputRootProps) => <ControlledOtpInput {...args} />;

export default function OtpInputWithoutSeparatorExample(props: Partial<OtpInputRootProps>) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as OtpInputRootProps;
  return OtpInputWithoutSeparatorExampleRender(mergedProps);
}

전체 너비

fullWidth로 컨테이너 너비에 맞춰 슬롯이 늘어난다.







import { css } from '@mildang/styled-system/css';
import { OtpInput, type OtpInputRootProps } from '@mildang/design-system/unofficial/OtpInput';
import { useState } from 'react';
import { Text } from '@mildang/design-system/Text';

const demoColumnStyle = css({
  display: 'flex',
  flexDirection: 'column',
  gap: '16',
  minWidth: 0,
});

const ControlledOtpInput = (args: OtpInputRootProps) => {
  const [value, setValue] = useState(args.value ?? '');

  return (
    <div className={demoColumnStyle}>
      <OtpInput.Root {...args} value={value} onChange={setValue} />
      <Text variant="caption-lg" color="neutral.text.low">
        입력값: {value || '-'}
      </Text>
    </div>
  );
};

const STORY_DEFAULT_ARGS = { ...({}), ...({
    fullWidth: true,
    'aria-label': '전체 너비 인증번호',
  }) } as OtpInputRootProps;

const OtpInputFullWidthExampleRender = (args: OtpInputRootProps) => (
    <div className={css({ width: '100%' })}>
      <ControlledOtpInput {...args} />
    </div>
  );

export default function OtpInputFullWidthExample(props: Partial<OtpInputRootProps>) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as OtpInputRootProps;
  return OtpInputFullWidthExampleRender(mergedProps);
}