Select

목록에서 고르는 컨트롤이다. 내부는 react-select 이지만 값은 옵션 객체가 아니라 원시값으로 주고받는다. 여러 개를 고르려면 MultiSelect 를 쓴다.

import { Select, MultiSelect } from "@nui-kit/react";
// 서브패스로 좁힐 때
import { Select } from "@nui-kit/react/select";
import "@nui-kit/react/styles/select.css";   // 온디맨드일 때
import { Select, MultiSelect } from "@nui-kit/react";

const OPTIONS = [
  { label: "서울", value: "seoul" },
  { label: "부산", value: "busan" },
];

<Select options={OPTIONS} value={city} onChange={setCity} />
<MultiSelect options={OPTIONS} value={cities} onChange={setCities} />
controlled 전용이다. value onChange 를 소비자가 소유한다. onChange 의 첫 인자가 원시값이고, 두 번째·세 번째로 react-select 의 옵션 객체와 actionMeta 가 함께 온다. react-hook-form 을 쓴다면 @nui-kit/react/rhfRHFSelect · RHFMultiSelect 를 쓴다.

기본

값은 옵션 객체가 아니라 원시값(value) 으로 주고받는다. 폼 상태에 그대로 넣을 수 있다.

지역을 고르세요

현재 값: null

<Select options={OPTIONS} value={city} onChange={setCity} />

선택한 값이 그대로 상태에 들어간다

검색과 지우기

isSearchable 로 타이핑 필터를, isClearable 로 선택 해제 버튼을 켠다. 둘 다 기본값은 false 다.

부산
<Select options={OPTIONS} isSearchable isClearable />

isSearchable + isClearable

옵션 그룹

지역을 고르세요
options={[{ label: "수도권", options: [{ label: "서울", value: "seoul" }] }]}

options 에 { label, options } 를 넣는다

다중 선택

MultiSelect 는 값이 배열이다. 선택 항목은 칩으로 표시되고 칩의 × 로 개별 해제한다.

서울
부산

현재 값: ["seoul","busan"]

<MultiSelect options={OPTIONS} value={cities} onChange={setCities} />

값이 원시값 배열로 들어온다

상태

우선순위는 disabled > error > readonly 다. readOnly 는 값을 보여주되 메뉴를 열지 않는다 — disabled 와 달리 포커스는 받는다.

서울
<Select options={OPTIONS} errorMessage="지역을 선택해주세요" />

disabled

부산

readOnly — 열리지 않는다

서울
부산

MultiSelect disabled — 태그도 비활성 색을 따른다

지역을 고르세요
지역을 선택해주세요.

errorMessage — 아이콘과 텍스트를 함께 표시하고 aria-describedby 로 연결한다

지역을 고르세요
배송 가능 지역만 표시됩니다.

infoMessage

Field 와 함께

Field 안에 넣으면 라벨의 htmlFor 와 컨트롤의 id, 설명·에러의 aria-describedby 가 자동으로 연결된다.

배송지 기준으로 선택해주세요.

거주 지역

react-hook-form 연동

@nui-kit/react/rhfRHFSelect · RHFMultiSelectcontrol 만 넘기면 값과 에러를 스스로 소유한다. value · onChange · name 은 타입에서 제외되어 있어 중복 소유가 생기지 않는다.

필수 항목입니다.

거주 지역

하나 이상 선택해주세요.

관심 지역
{
  "values": {
    "city": null,
    "interests": []
  },
  "isValid": false,
  "errors": {}
}

mode: "onChange" — 값이 바뀔 때마다 검증한다

에러 메시지는 fieldState.error.message 가 그대로 errorMessage 로 전달되어 컨트롤 아래에 표시되고, aria-describedby 로 연결된다. 직접 errorMessage 를 넘기면 RHF 에러가 없을 때의 대체값으로 쓰인다.

스타일 커스터마이징

컨트롤 외형은 공개 CSS 변수로 조정한다. 이 변수들은 라이브러리 레이어(@layer nui.components) 밖에서 선언하면 언제나 우선한다.

색은 컴포넌트별로 열지 않는다. 한 곳만 바꾸려면 className 을, 화면 전체를 바꾸려면 브랜드 프리셋을 쓴다.

컴포넌트변수기본값비고
Select · MultiSelect--nui-select--border-widthvar(--nui-border-width-1) · 3자리를 함께 움직인다
--nui-select--heightvar(--nui-size-field)
--nui-select--radiusvar(--nui-radius-1_5)
react-select 의 styles prop 은 우리 CSS 를 이긴다. react-select 은 emotion 으로 스타일을 주입하는데, 그 클래스는 CSS 레이어 밖에 있어 @layer nui.components 안의 우리 규칙보다 항상 우선한다. 그래서 이 컴포넌트는 unstyled 로 구동하면서 충돌하는 속성만 emotion 쪽에서 걷어내 CSS 가 책임지게 한다. styles prop 을 직접 넘기면 그 정리된 값 위에 얹히므로 의도한 대로 덧칠할 수 있다. 반대로 메뉴 최대 높이처럼 react-select 이 배치 계산에 쓰는 값은 CSS 가 아니라 maxMenuHeight prop 으로 조정해야 한다.
components 는 렌더 밖에서 선언한다. 매 렌더 새 컴포넌트 함수를 넘기면 react-select 이 내부 input 을 remount 해 포커스와 입력 중이던 검색어가 사라진다. react-select 공식 문서도 같은 것을 권고한다.
// ❌ 렌더 안에서 컴포넌트를 새로 만든다
<Select components={{ Option: (props) => <CustomOption {...props} /> }} />

// ✅ 모듈 스코프에 한 번만 선언한다
const SELECT_COMPONENTS = { Option: CustomOption };
<Select components={SELECT_COMPONENTS} />

styles 는 컴포넌트가 아니라 함수 객체라 인라인으로 넘겨도 remount 되지 않는다.

valueoptions 안에 존재하는 값이어야 한다. 옵션을 비동기로 불러오는 동안처럼 options 에 없는 값을 넣으면 선택이 표시되지 않고 placeholder 가 보인다 — 원시값 API 의 구조적 특성이다.

접근성

API

아래 표는 이 라이브러리가 정의한 prop 이다. 여기에 없는 react-select 의 prop(menuPlacement, maxMenuHeight, closeMenuOnSelect 등)도 그대로 전달된다. 단 defaultValue(controlled 전용), getOptionValue(원시값 매칭이 value 고정), theme(unstyled 라 효과 없음) 은 받지 않는다.

Select

이름타입기본값설명
options *OptionsOrGroups<SelectOption, GroupBase<SelectOption>>
aria-describedbystring
classNamestring
componentsSelectComponentsConfig< SelectOption, IsMulti, GroupBase<SelectOption> >
disabledbooleanfalse
errorMessagestring""
hasPortalbooleanfalse메뉴를 `body` 로 내보내 **잘리는 조상을 탈출한다.** 기본 배치는 제자리(`absolute`)라 조상에 `overflow: hidden` 이 있으면 메뉴가 잘려 값을 고를 수 없다. 카드·팝업 안에 넣을 때 켠다. `Tooltip` 의 같은 이름 prop 과 한 규칙이다. ⚠️ 소비자가 `menuPortalTarget` 을 직접 주면 그쪽이 이긴다.
idstring
infoMessagestring""
isClearableboolean | undefinedfalseIs the select value clearable
isErrorbooleanfalse
isSearchableboolean | undefinedfalseWhether to enable search functionality
maxMenuHeightnumber | undefined240Maximum height of the menu before scrolling
namestring
noOptionsMessage((obj: { inputValue: string; }) => ReactNode) | undefinedDEFAULT_NO_OPTIONS_MESSAGEText to display when there are no options
onChange( nextValue: SingleSelectValue, selectedOption: SelectOption | null, meta: SelectChangeMeta, ) => void
placeholderstring
readOnlybooleanfalse
stylesStylesConfig<SelectOption, IsMulti, GroupBase<SelectOption>>
valueSingleSelectValuenull

SelectProps 에서 자동 생성됨 (components/Select/Select.tsx). 표준 DOM 속성 58개는 그대로 전달되며 표에서 생략했다.

MultiSelect

이름타입기본값설명
options *OptionsOrGroups<SelectOption, GroupBase<SelectOption>>
aria-describedbystring
classNamestring
componentsSelectComponentsConfig< SelectOption, IsMulti, GroupBase<SelectOption> >
disabledbooleanfalse
errorMessagestring""
hasPortalbooleanfalse메뉴를 `body` 로 내보내 **잘리는 조상을 탈출한다.** 기본 배치는 제자리(`absolute`)라 조상에 `overflow: hidden` 이 있으면 메뉴가 잘려 값을 고를 수 없다. 카드·팝업 안에 넣을 때 켠다. `Tooltip` 의 같은 이름 prop 과 한 규칙이다. ⚠️ 소비자가 `menuPortalTarget` 을 직접 주면 그쪽이 이긴다.
idstring
infoMessagestring""
isClearableboolean | undefinedfalseIs the select value clearable
isErrorbooleanfalse
isSearchableboolean | undefinedfalseWhether to enable search functionality
maxMenuHeightnumber | undefined240Maximum height of the menu before scrolling
namestring
noOptionsMessage((obj: { inputValue: string; }) => ReactNode) | undefinedDEFAULT_NO_OPTIONS_MESSAGEText to display when there are no options
onChange( nextValue: MultiSelectValue, selectedOptions: readonly SelectOption[], meta: SelectChangeMeta, ) => void
placeholderstring
readOnlybooleanfalse
removeButtonLabel(optionLabel: string) => stringDEFAULT_REMOVE_BUTTON_LABEL칩의 삭제 버튼 접근 이름 (KRDS 가이드 566쪽 02). 기본값 `"{라벨} 옵션 삭제"`. 문자열이 아니라 함수인 이유는 라벨을 끼워 넣는 자리가 언어마다 다르기 때문이다.
stylesStylesConfig<SelectOption, IsMulti, GroupBase<SelectOption>>
valueMultiSelectValue[]

MultiSelectProps 에서 자동 생성됨 (components/Select/MultiSelect.tsx). 표준 DOM 속성 58개는 그대로 전달되며 표에서 생략했다.