본문으로 건너뛰기

Select

정해진 목록에서 값 하나를 고릅니다. trigger는 TextField와 같은 shell에 chevron을 얹은 형태입니다.

tsx
import { Select } from 'neba';

<Select
  label="리전"
  placeholder="리전을 고르세요"
  items={[
    { value: 'icn', label: '서울' },
    { value: 'nrt', label: '도쿄' }
  ]}
/>;

Props

Prop타입기본값설명
variant공통'solid' | 'outline' | 'text''outline'표면의 무게. TextField와 같은 셸이므로 폼 안에서 두 컨트롤이 구분되지 않습니다
size공통'xs' | 'sm' | 'md' | 'lg' | 'xl''md'높이와 타입 스케일. Button·TextField와 같은 높이입니다
color공통'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info''primary'의미론적 색 역할. 표면은 흰색이므로 가장자리와 포커스 링에만 나타납니다
density공통'default' | 'compact''default'여백만 바꿉니다. 높이와 글자 크기는 그대로
elevation공통0 | 1 | 2 | 30트리거의 그림자 깊이. 팝업은 자기 그림자를 따로 가집니다
items * readonly SelectOption[]옵션 목록. { value, label?, disabled?, group? } 배열입니다. group이 같은 연속된 옵션 위에 제목이 붙습니다
valuestring | number | null선택된 값. onValueChange와 함께 제어 컴포넌트로 씁니다
defaultValuestring | number | null초기 선택 값
onValueChange(value: string | number | null) => void선택이 바뀔 때
placeholderReactNode아무것도 고르지 않았을 때 트리거에 보이는 내용
labelReactNode컨트롤과 연결되는 라벨. Base UI Field가 묶어 줍니다
descriptionReactNode보조 설명
errorReactNode오류 메시지. 값이 있으면 invalid 상태도 함께 켜지고 색 계열이 danger로 넘어갑니다
invalidboolean!!error메시지 없이 invalid만 켭니다. 외부 폼 라이브러리가 유효성을 가질 때
startIconReactNode값 앞에 놓이는 내용
fullWidthbooleanfalse컨테이너 너비만큼 확장
namestring폼 제출 시의 필드 이름
requiredboolean폼을 제출하기 전에 값을 골라야 하는지
readOnlybooleanfalse값은 보이지만 바꿀 수 없음. 색과 가장자리는 유지한 채 채도만 빠집니다
disabledbooleanfalse사용 불가. 색 계열을 버리고 중립 회색이 됩니다
classNamesNebaSlots<'label' | 'control' | 'description' | 'error' | 'popup' | 'item'>루트 뒤에 있는 각 파트의 class. 루트 자체는 className이 맡으므로 root 키는 없습니다

<div>의 native 속성은 root로 전달됩니다. colordefaultValue만 위 표와 이름이 겹쳐 제외됩니다.

선택지를 검색해서 찾아야 한다면 Combobox를, 선택지가 두세 개뿐이라면 RadioGroup이나 SegmentedButton을 쓰세요.

items

선택지는 컴포넌트를 조합하는 대신 배열로 넘깁니다.

ts
interface SelectOption {
  value: string | number;
  label?: React.ReactNode; // 생략하면 value 자체
  disabled?: boolean;
  group?: string;
}

value는 문자열이나 숫자입니다. 폼과 함께 제출되는 값이므로 객체는 받지 않습니다. 식별자만 넘기고 객체는 호출부에서 찾으세요.

예시

variant

TextField와 같은 세 가지 무게를 같은 shell 위에 그립니다. 한 폼 안에서 필드와 select의 높이와 테두리가 어긋나지 않습니다.

size

group

group은 같은 이름을 가진 이웃한 옵션들 위에 제목을 답니다. 지역별 시간대, 계열별 글꼴처럼 묶이는 목록에 씁니다. 제목은 하이라이트되지도, 선택되지도, typeahead로 닿지도 않습니다.

group은 이웃한 옵션들의 묶음이라 배열 순서가 곧 목록 순서입니다. 라이브러리가 항목을 옮기는 일은 없습니다. 같은 이름이 떨어져서 두 번 나오면 제목도 두 번 붙습니다. group이 없는 옵션은 배열에 있는 자리에 그대로 놓입니다.

disabled · readOnly · error

팝업

팝업은 portal을 통해 <body> 끝에 렌더링되므로, CSS reset을 특정 subtree에만 적용한 앱에서는 그 범위를 벗어납니다. positioner에 neba-portal 클래스가 붙어 있으니 그 경우 reset을 이 클래스에 걸어 주세요. Tailwind Preflight를 전역으로 적용했다면 아무것도 하지 않아도 됩니다.

classNames

className은 루트(라벨과 trigger, 그 아래 두 줄을 담는 열)에 붙고, trigger 자체는 classNames.control로 갑니다.

tsx
<Select
  items={plans}
  label="Plan"
  classNames={{ control: 'font-mono', popup: 'max-h-40', item: 'rounded-none' }}
/>

slot은 label, control, description, error, popup, item입니다. 뒤의 둘이 특히 중요합니다. popup은 <body> 끝에 그려지므로 루트를 기준으로 쓴 하위 선택자로는 닿지 않고, 이 slot이 유일한 경로입니다. 넘긴 class가 컴포넌트 자신의 class와 어떻게 겨루는지는 prop 규약을 보세요.

접근성

  • trigger는 combobox role을 갖고, label이 accessible name이 됩니다.
  • 팝업 위치 계산과 화면 경계에서의 뒤집힘, focus 처리, typeahead, 폼 제출용 hidden input이 모두 처리됩니다.
  • disabled 선택지는 목록에 남은 채 aria-disabled로 보고됩니다.

Released under the MIT License