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} />value 와 onChange 를 소비자가 소유한다. onChange 의 첫 인자가 원시값이고, 두 번째·세 번째로 react-select 의 옵션 객체와 actionMeta 가 함께 온다. react-hook-form 을 쓴다면 @nui-kit/react/rhf 의 RHFSelect · 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/rhf 의 RHFSelect · RHFMultiSelect 는 control 만 넘기면 값과 에러를 스스로 소유한다. value · onChange · name 은 타입에서 제외되어 있어 중복 소유가 생기지 않는다.
mode: "onChange" — 값이 바뀔 때마다 검증한다
fieldState.error.message 가 그대로 errorMessage 로 전달되어 컨트롤 아래에 표시되고, aria-describedby 로 연결된다. 직접 errorMessage 를 넘기면 RHF 에러가 없을 때의 대체값으로 쓰인다.스타일 커스터마이징
컨트롤 외형은 공개 CSS 변수로 조정한다. 이 변수들은 라이브러리 레이어(@layer nui.components) 밖에서 선언하면 언제나 우선한다.
색은 컴포넌트별로 열지 않는다. 한 곳만 바꾸려면 className 을, 화면 전체를 바꾸려면 브랜드 프리셋을 쓴다.
| 컴포넌트 | 변수 | 기본값 | 비고 |
|---|---|---|---|
| Select · MultiSelect | --nui-select--border-width | var(--nui-border-width-1) | — · 3자리를 함께 움직인다 |
| --nui-select--height | var(--nui-size-field) | — | |
| --nui-select--radius | var(--nui-radius-1_5) | — |
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 되지 않는다.
value 는 options 안에 존재하는 값이어야 한다. 옵션을 비동기로 불러오는 동안처럼 options 에 없는 값을 넣으면 선택이 표시되지 않고 placeholder 가 보인다 — 원시값 API 의 구조적 특성이다.접근성
Field안에서는 라벨의htmlFor와 컨트롤의id가 자동으로 연결된다- 설명·에러 메시지 id 는
aria-describedby로 중복 없이 합쳐진다 - 에러일 때
aria-invalid가 붙고, 메시지는 색이 아니라 아이콘 + 텍스트로 표시된다 - 키보드로 조작한다 — ↑ ↓ 로 이동, Enter 로 선택, Esc 로 닫기
readOnly는 포커스는 받되 메뉴를 열지 않는다.disabled는 포커스 자체를 받지 않는다- MultiSelect 칩의 × 는 Tab 으로 닿는 버튼이다. 칩이 여럿이면 앞에서부터 하나씩 잡히고, 그다음이 입력창이다. Enter 와 Space 로 지운다. 지우고 나면 포커스가 이전 칩으로, 없으면 입력창으로 간다
- 칩 × 의 접근 이름은 기본 "서울 옵션 삭제" 이고
removeButtonLabel로 바꾼다. 라벨을 끼워 넣는 자리가 언어마다 달라서 문자열이 아니라 함수를 받는다<MultiSelect removeButtonLabel={(label) => `Remove ${label}`} />
API
아래 표는 이 라이브러리가 정의한 prop 이다. 여기에 없는 react-select 의 prop(menuPlacement, maxMenuHeight, closeMenuOnSelect 등)도 그대로 전달된다. 단 defaultValue(controlled 전용), getOptionValue(원시값 매칭이 value 고정), theme(unstyled 라 효과 없음) 은 받지 않는다.
Select
| 이름 | 타입 | 기본값 | 설명 |
|---|---|---|---|
options * | OptionsOrGroups<SelectOption, GroupBase<SelectOption>> | — | — |
aria-describedby | string | — | — |
className | string | — | — |
components | SelectComponentsConfig< SelectOption, IsMulti, GroupBase<SelectOption> > | — | — |
disabled | boolean | false | — |
errorMessage | string | "" | — |
hasPortal | boolean | false | 메뉴를 `body` 로 내보내 **잘리는 조상을 탈출한다.** 기본 배치는 제자리(`absolute`)라 조상에 `overflow: hidden` 이 있으면 메뉴가 잘려 값을 고를 수 없다. 카드·팝업 안에 넣을 때 켠다. `Tooltip` 의 같은 이름 prop 과 한 규칙이다. ⚠️ 소비자가 `menuPortalTarget` 을 직접 주면 그쪽이 이긴다. |
id | string | — | — |
infoMessage | string | "" | — |
isClearable | boolean | undefined | false | Is the select value clearable |
isError | boolean | false | — |
isSearchable | boolean | undefined | false | Whether to enable search functionality |
maxMenuHeight | number | undefined | 240 | Maximum height of the menu before scrolling |
name | string | — | — |
noOptionsMessage | ((obj: { inputValue: string; }) => ReactNode) | undefined | DEFAULT_NO_OPTIONS_MESSAGE | Text to display when there are no options |
onChange | ( nextValue: SingleSelectValue, selectedOption: SelectOption | null, meta: SelectChangeMeta, ) => void | — | — |
placeholder | string | — | — |
readOnly | boolean | false | — |
styles | StylesConfig<SelectOption, IsMulti, GroupBase<SelectOption>> | — | — |
value | SingleSelectValue | null | — |
SelectProps 에서 자동 생성됨 (components/Select/Select.tsx). 표준 DOM 속성 58개는 그대로 전달되며 표에서 생략했다.
MultiSelect
| 이름 | 타입 | 기본값 | 설명 |
|---|---|---|---|
options * | OptionsOrGroups<SelectOption, GroupBase<SelectOption>> | — | — |
aria-describedby | string | — | — |
className | string | — | — |
components | SelectComponentsConfig< SelectOption, IsMulti, GroupBase<SelectOption> > | — | — |
disabled | boolean | false | — |
errorMessage | string | "" | — |
hasPortal | boolean | false | 메뉴를 `body` 로 내보내 **잘리는 조상을 탈출한다.** 기본 배치는 제자리(`absolute`)라 조상에 `overflow: hidden` 이 있으면 메뉴가 잘려 값을 고를 수 없다. 카드·팝업 안에 넣을 때 켠다. `Tooltip` 의 같은 이름 prop 과 한 규칙이다. ⚠️ 소비자가 `menuPortalTarget` 을 직접 주면 그쪽이 이긴다. |
id | string | — | — |
infoMessage | string | "" | — |
isClearable | boolean | undefined | false | Is the select value clearable |
isError | boolean | false | — |
isSearchable | boolean | undefined | false | Whether to enable search functionality |
maxMenuHeight | number | undefined | 240 | Maximum height of the menu before scrolling |
name | string | — | — |
noOptionsMessage | ((obj: { inputValue: string; }) => ReactNode) | undefined | DEFAULT_NO_OPTIONS_MESSAGE | Text to display when there are no options |
onChange | ( nextValue: MultiSelectValue, selectedOptions: readonly SelectOption[], meta: SelectChangeMeta, ) => void | — | — |
placeholder | string | — | — |
readOnly | boolean | false | — |
removeButtonLabel | (optionLabel: string) => string | DEFAULT_REMOVE_BUTTON_LABEL | 칩의 삭제 버튼 접근 이름 (KRDS 가이드 566쪽 02). 기본값 `"{라벨} 옵션 삭제"`. 문자열이 아니라 함수인 이유는 라벨을 끼워 넣는 자리가 언어마다 다르기 때문이다. |
styles | StylesConfig<SelectOption, IsMulti, GroupBase<SelectOption>> | — | — |
value | MultiSelectValue | [] | — |
MultiSelectProps 에서 자동 생성됨 (components/Select/MultiSelect.tsx). 표준 DOM 속성 58개는 그대로 전달되며 표에서 생략했다.