Field

Field는 폼 요소들을 감싸는 컨테이너 컴포넌트로, 라벨, 설명, 에러 메시지, 성공 메시지 등을 제공합니다.
'use client';

import { Box, Field, Text, TextInput } from '@vapor-ui/core';

export default function DefaultField() {
    return (
        <Box width="300px">
            <Field.Root name="username">
                <Box render={<Field.Label />} flexDirection="column">
                    <Text typography="subtitle2" foreground="normal-200">
                        Field.Label
                    </Text>
                    <TextInput placeholder="사용자명을 입력하세요" />
                </Box>
                <Field.Description>Field.Description</Field.Description>
                <Field.Error match={true}>Field.Error</Field.Error>
                <Field.Success>Field.Success</Field.Success>
            </Field.Root>
        </Box>
    );
}

Property


Invalid State

invalid prop을 사용하여 필드의 유효하지 않은 상태를 표시할 수 있습니다.

'use client';

import { Box, Field, TextInput } from '@vapor-ui/core';

export default function FieldInvalid() {
    // 공식 문서에 따라 custom error 타입 객체 반환
    const validate = (value: unknown) => {
        const email = String(value);
        const EMAIL_REGEX =
            /^[a-zA-Z0-9]{1}[a-zA-Z0-9-_.]*@[a-zA-Z0-9-]+.[a-zA-Z0-9-_]+(.[a-zA-Z0-9-_]+)?$/;

        if (!EMAIL_REGEX.test(email)) return '올바른 이메일 형식을 입력해주세요.';

        return null;
    };

    return (
        <Box width="300px">
            <Field.Root name="email" validationMode="onChange" validate={validate}>
                <Box render={<Field.Label />} flexDirection="column">
                    이메일
                    <TextInput type="email" placeholder="이메일을 입력하세요" />
                </Box>

                <Field.Error />
            </Field.Root>
        </Box>
    );
}

Validation Mode

validationMode prop을 사용하여 언제 검증을 수행할지 제어할 수 있습니다. onChange는 입력할 때마다, onBlur는 포커스를 잃을 때 검증을 수행합니다.

'use client';

import { Box, Field, TextInput, VStack } from '@vapor-ui/core';

export default function FieldValidationMode() {
    const validateEmail = (value: unknown) => {
        const stringValue = String(value || '');
        const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

        if (!EMAIL_REGEX.test(stringValue)) return '올바른 이메일 형식을 입력해주세요.';

        return null;
    };

    return (
        <VStack gap="$200">
            <Field.Root name="name" validationMode="onChange">
                <Box render={<Field.Label />} flexDirection="column">
                    이름 (onChange 검증)
                    <TextInput required placeholder="이름을 입력하세요" />
                </Box>

                <Field.Description>입력할 때마다 실시간으로 검증됩니다.</Field.Description>
                <Field.Error />
            </Field.Root>

            <Field.Root name="email" validationMode="onBlur" validate={validateEmail}>
                <Box render={<Field.Label />} flexDirection="column">
                    이메일 (onBlur 검증)
                    <TextInput type="email" placeholder="이메일을 입력하세요" />
                </Box>

                <Field.Description>입력 필드를 벗어날 때 검증됩니다.</Field.Description>
                <Field.Error />
            </Field.Root>
        </VStack>
    );
}

Match

Field.Errormatch prop을 사용하여 특정 조건에 따라 에러 메시지 표시를 제어할 수 있습니다.

'use client';

import { useState } from 'react';

import { Box, Field, TextInput, VStack } from '@vapor-ui/core';

export default function FieldMatch() {
    const [password, setPassword] = useState('');
    const [confirmPassword, setConfirmPassword] = useState('');

    return (
        <VStack gap="$200" width="300px">
            <Field.Root name="password" validationMode="onBlur">
                <Box render={<Field.Label />} flexDirection="column">
                    비밀번호
                    <TextInput
                        type="password"
                        required
                        minLength={8}
                        value={password}
                        onValueChange={(value) => setPassword(value)}
                        placeholder="비밀번호를 입력하세요"
                    />
                </Box>

                <Field.Description>최소 8자 이상 입력해주세요.</Field.Description>
                <Field.Error match="valueMissing">비밀번호를 입력해주세요.</Field.Error>
                <Field.Error match="tooShort">비밀번호는 최소 8자 이상이어야 합니다.</Field.Error>
            </Field.Root>

            <Field.Root name="confirmPassword">
                <Box render={<Field.Label />} flexDirection="column">
                    비밀번호 확인
                    <TextInput
                        type="password"
                        value={confirmPassword}
                        onValueChange={(value) => setConfirmPassword(value)}
                        placeholder="비밀번호를 다시 입력하세요"
                        pattern={password}
                    />
                </Box>

                <Field.Error match="patternMismatch">비밀번호가 일치하지 않습니다.</Field.Error>
            </Field.Root>
        </VStack>
    );
}

Examples


With Description

Field.Description을 사용하여 필드에 대한 추가 설명을 제공할 수 있습니다.

'use client';

import { Box, Field, TextInput } from '@vapor-ui/core';

export default function FieldDescription() {
    return (
        <Box width="300px">
            <Field.Root name="email">
                <Box render={<Field.Label />} flexDirection="column">
                    이메일 주소
                    <TextInput type="email" placeholder="example@email.com" />
                </Box>
                <Field.Description>
                    회원가입 시 사용할 이메일 주소를 입력해주세요.
                </Field.Description>
            </Field.Root>
        </Box>
    );
}

With Validation

Field.Error와 Field.Success를 사용하여 유효성 검사 결과를 표시할 수 있습니다.

'use client';

import { Box, Field, TextInput } from '@vapor-ui/core';

export default function FieldValidation() {
    return (
        <Box width="300px">
            <Field.Root name="password" validationMode="onChange">
                <Box render={<Field.Label />} flexDirection="column">
                    비밀번호
                    <TextInput type="password" required placeholder="비밀번호를 입력하세요" />
                </Box>
                <Field.Error match="valueMissing">비밀번호를 입력해주세요.</Field.Error>
                <Field.Success>✓ 사용 가능한 비밀번호입니다.</Field.Success>
            </Field.Root>
        </Box>
    );
}

With Checkbox

Checkbox 컴포넌트와 Field를 함께 사용하는 예제입니다.

'use client';

import { Box, Checkbox, Field } from '@vapor-ui/core';

export default function FieldCheckbox() {
    return (
        <Field.Root name="agreement">
            <Box render={<Field.Label />} alignItems="center">
                <Checkbox.Root />
                이용약관에 동의합니다
            </Box>
            <Field.Description>서비스 이용을 위해 이용약관에 동의해주세요.</Field.Description>
        </Field.Root>
    );
}

With Switch

Switch 컴포넌트와 Field를 함께 사용하는 예제입니다.

'use client';

import { Box, Field, Switch } from '@vapor-ui/core';

export default function FieldSwitch() {
    return (
        <Field.Root name="notifications">
            <Box render={<Field.Label />} alignItems="center">
                <Switch.Root />
                알림 수신 동의
            </Box>
            <Field.Description>
                새로운 소식과 업데이트를 이메일로 받아보실 수 있습니다.
            </Field.Description>
        </Field.Root>
    );
}

With RadioGroup

RadioGroup과 Field를 함께 사용하여 선택지가 있는 필드를 만드는 예제입니다.

'use client';

import { Field, Radio, RadioGroup } from '@vapor-ui/core';

export default function FieldRadioGroup() {
    return (
        <Field.Root name="gender" className="v-space-y-3">
            <Field.Label>성별</Field.Label>
            <RadioGroup.Root className="v-space-y-2">
                <div className="v-flex v-items-center v-gap-2">
                    <Radio.Root value="male">
                        <Radio.Indicator />
                    </Radio.Root>
                    <Field.Label>남성</Field.Label>
                </div>
                <div className="v-flex v-items-center v-gap-2">
                    <Radio.Root value="female">
                        <Radio.Indicator />
                    </Radio.Root>
                    <Field.Label>여성</Field.Label>
                </div>
                <div className="v-flex v-items-center v-gap-2">
                    <Radio.Root value="other">
                        <Radio.Indicator />
                    </Radio.Root>
                    <Field.Label>기타</Field.Label>
                </div>
            </RadioGroup.Root>
            <Field.Description>개인정보 보호를 위해 선택사항입니다.</Field.Description>
        </Field.Root>
    );
}

With Different Input Types

다양한 폼 컨트롤(TextInput, Checkbox, Switch, Select 등)과 Field를 함께 사용하는 예제입니다.

'use client';

import { Box, Checkbox, Field, Select, Switch, TextInput, VStack } from '@vapor-ui/core';

export default function FieldWithInputs() {
    return (
        <VStack gap="$200" width="300px">
            <Field.Root name="email">
                <Box render={<Field.Label />} flexDirection="column">
                    이메일
                    <TextInput type="email" placeholder="example@domain.com" />
                </Box>
                <Field.Description>알림을 받을 이메일 주소를 입력하세요.</Field.Description>
            </Field.Root>

            {/* Checkbox with Field */}
            <Field.Root name="newsletter">
                <Box render={<Field.Label />} alignItems="center">
                    <Checkbox.Root />
                    뉴스레터 구독
                </Box>
                <Field.Description>최신 소식과 업데이트를 이메일로 받아보세요.</Field.Description>
            </Field.Root>

            {/* Switch with Field */}
            <Field.Root name="notifications">
                <Box render={<Field.Label />} alignItems="center">
                    <Switch.Root />
                    푸시 알림
                </Box>
                <Field.Description>중요한 알림을 즉시 받아보세요.</Field.Description>
            </Field.Root>

            {/* Select with Field */}
            <Field.Root name="country">
                <Select.Root placeholder="국가를 선택하세요">
                    <Box render={<Field.Label htmlFor="country-select" />} flexDirection="column">
                        국가
                        <Select.Trigger id="country-select" />
                    </Box>
                    <Select.Popup>
                        <Select.Item value="kr">대한민국</Select.Item>
                        <Select.Item value="us">미국</Select.Item>
                        <Select.Item value="jp">일본</Select.Item>
                        <Select.Item value="cn">중국</Select.Item>
                    </Select.Popup>
                </Select.Root>
                <Field.Description>거주 중인 국가를 선택하세요.</Field.Description>
            </Field.Root>
        </VStack>
    );
}

Required Fields

필수 필드와 선택 필드를 구분하여 표시하는 예제입니다.

'use client';

import { Box, Field, Text, TextInput, VStack } from '@vapor-ui/core';

export default function FieldRequired() {
    return (
        <VStack gap="$200" width="300px">
            <Field.Root name="required-field">
                <Box render={<Field.Label />} flexDirection="column">
                    <span>
                        필수 입력 필드 <Text foreground="danger-100">*</Text>
                    </span>
                    <TextInput required placeholder="필수 입력 항목입니다" />
                </Box>
                <Field.Description>
                    이 필드는 반드시 입력해야 하는 필수 항목입니다.
                </Field.Description>
                <Field.Error match="valueMissing">이 필드는 필수 입력 항목입니다.</Field.Error>
                <Field.Success>입력이 완료되었습니다.</Field.Success>
            </Field.Root>

            {/* Optional Field */}
            <Field.Root name="optional-field">
                <Box render={<Field.Label />} flexDirection="column">
                    <span>
                        선택 입력 필드{' '}
                        <Text foreground="hint-100" typography="subtitle2">
                            (선택사항)
                        </Text>
                    </span>
                    <TextInput placeholder="선택적으로 입력하세요" />
                </Box>
                <Field.Description>이 필드는 선택적으로 입력할 수 있습니다.</Field.Description>
            </Field.Root>
        </VStack>
    );
}

Field States

다양한 필드 상태(일반, 오류, 성공, 비활성화)를 보여주는 예제입니다.

'use client';

import { Box, Field, TextInput, VStack } from '@vapor-ui/core';

export default function FieldStates() {
    return (
        <VStack gap="$200" width="300px">
            <Field.Root name="normal">
                <Box render={<Field.Label />} flexDirection="column">
                    일반 상태
                    <TextInput placeholder="일반 입력 필드" />
                </Box>

                <Field.Description>일반적인 필드 상태입니다.</Field.Description>
            </Field.Root>

            <Field.Root name="error">
                <Box render={<Field.Label />} flexDirection="column">
                    오류 상태
                    <TextInput placeholder="오류가 있는 필드" invalid />
                </Box>

                <Field.Error match={true}>필수 입력 항목입니다. 값을 입력해주세요.</Field.Error>
            </Field.Root>

            <Field.Root name="success">
                <Box render={<Field.Label />} flexDirection="column">
                    성공 상태
                    <TextInput placeholder="유효한 필드" defaultValue="valid@example.com" />
                </Box>

                <Field.Success>올바른 이메일 형식입니다.</Field.Success>
            </Field.Root>

            {/* Disabled State */}
            <Field.Root name="disabled" disabled>
                <Box render={<Field.Label />} flexDirection="column">
                    비활성화 상태
                    <TextInput placeholder="비활성화된 필드" />
                </Box>
                <Field.Description>이 필드는 현재 비활성화되어 있습니다.</Field.Description>
            </Field.Root>
        </VStack>
    );
}

Controlled Fields

제어 컴포넌트로 필드를 관리하고 실시간으로 상태를 표시하는 예제입니다.

'use client';

import { useState } from 'react';

import { Box, Button, Field, Form, Text, TextInput, VStack } from '@vapor-ui/core';

export default function FieldControlled() {
    const [firstName, setFirstName] = useState('');
    const [lastName, setLastName] = useState('');
    const [email, setEmail] = useState('');

    const handleSubmit = (event: React.FormEvent) => {
        event.preventDefault();
        alert(`[제출된 값]\n- 이름: ${firstName}\n- 성: ${lastName}\n- 이메일: ${email}`);
    };

    return (
        <Form onSubmit={handleSubmit}>
            <VStack gap="$200" width="300px">
                <Field.Root name="firstName">
                    <Box render={<Field.Label />} flexDirection="column">
                        이름
                        <TextInput
                            value={firstName}
                            onValueChange={(value) => setFirstName(value)}
                            placeholder="이름을 입력하세요"
                        />
                    </Box>

                    <Text typography="body3" foreground="hint-200">
                        현재 값: {firstName || '(비어있음)'}
                    </Text>
                </Field.Root>

                <Field.Root name="lastName">
                    <Box render={<Field.Label />} flexDirection="column">

                        <TextInput
                            value={lastName}
                            onValueChange={(value) => setLastName(value)}
                            placeholder="성을 입력하세요"
                        />
                    </Box>

                    <Text typography="body3" foreground="hint-200">
                        현재 값: {lastName || '(비어있음)'}
                    </Text>
                </Field.Root>

                <Field.Root name="email" validationMode="onChange">
                    <Box render={<Field.Label />} flexDirection="column">
                        이메일
                        <TextInput
                            type="email"
                            required
                            value={email}
                            onValueChange={(value) => setEmail(value)}
                            placeholder="이메일을 입력하세요"
                        />
                    </Box>

                    <Text typography="body3" foreground="hint-200">
                        현재 값: {email || '(비어있음)'}
                    </Text>

                    <Field.Error match="typeMismatch">올바른 이메일 형식이 아닙니다.</Field.Error>
                    <Field.Error match="valueMissing">이메일을 입력해주세요.</Field.Error>
                    <Field.Success>유효한 이메일 형식입니다.</Field.Success>
                </Field.Root>

                <Button>제출</Button>
            </VStack>
        </Form>
    );
}

Disabled

disabled 속성을 사용하여 비활성화된 필드를 만들 수 있습니다.

'use client';

import { Box, Field, TextInput } from '@vapor-ui/core';

export default function FieldDisabled() {
    return (
        <Field.Root name="disabled" disabled>
            <Box render={<Field.Label />} flexDirection="column">
                비활성 필드
                <TextInput value="이 필드는 수정할 수 없습니다" />
            </Box>

            <Field.Description>이 필드는 현재 비활성화되어 수정할 수 없습니다.</Field.Description>
        </Field.Root>
    );
}

Props Table


Field.Root

Field의 메인 컨테이너 컴포넌트입니다. 폼 필드의 기본 구조를 제공하며, 라벨, 입력 요소, 설명, 에러 메시지 등을 포함할 수 있습니다.

PropDefaultType
name
-
string
disabled?
false
boolean
invalid
-
boolean
validate
-
function
validationMode?
'onBlur'
'onBlur''onChange'
validationDebounceTime?
0
number
className
-
stringfunction
render
-
React.ReactElementfunction
children
-
React.ReactNode

Field.Label

필드에 라벨을 제공하는 컴포넌트입니다. 자동으로 폼 컨트롤과 연결됩니다.

PropDefaultType
children
-
React.ReactNode
className
-
stringfunction
render
-
React.ReactElementfunction

Field.Description

필드에 대한 추가 설명을 제공하는 컴포넌트입니다.

PropDefaultType
children
-
React.ReactNode
className
-
stringfunction
render
-
React.ReactElementfunction

Field.Error

필드 유효성 검사 실패 시 표시되는 오류 메시지 컴포넌트입니다.

PropDefaultType
children
-
React.ReactNode
match
-
booleanstringfunction
className
-
stringfunction
render
-
React.ReactElementfunction

Field.Success

필드 유효성 검사 성공 시 표시되는 성공 메시지 컴포넌트입니다.

PropDefaultType
children
-
React.ReactNode
className
-
stringfunction
render
-
React.ReactElementfunction

Field.Validity

필드의 유효성 상태에 따라 커스텀 내용을 표시할 수 있는 컴포넌트입니다.

PropDefaultType
children
-
function