Components

Combobox

사용 예시

입력해서 고르기

필터 조건을 직접 정할 때 써요. 이 예시에서는 이름에 입력한 글자가 들어 있는 항목만 보여요.

tsx
const zones = ['Asia/Seoul', 'Asia/Tokyo', 'Europe/London', 'America/New_York', 'UTC']const [picked, setPicked] = useState<string | null>(null)const [search, setSearch] = useState('')const { contains } = useComboboxFilter()const shown = zones.filter(zone => contains(zone, search))return (  <Combobox    items={zones}    filteredItems={shown}    value={picked}    onValueChange={setPicked}    inputValue={search}    onInputValueChange={setSearch}  >    <ComboboxAnchor>      <Search className="text-fg-subtle size-4 shrink-0" />      <ComboboxInput placeholder="시간대 이름" aria-label="시간대" />      <ComboboxTrigger />    </ComboboxAnchor>    <ComboboxContent aria-label="시간대">      <ComboboxEmpty>찾는 시간대가 없어요</ComboboxEmpty>      {(zone: string) => (        <ComboboxItem key={zone} value={zone}>          <ComboboxItemText>{zone}</ComboboxItemText>        </ComboboxItem>      )}    </ComboboxContent>  </Combobox>)

필터 맡기기

목록이 짧고 이름만 비교하면 될 때 써요. 필터 코드를 따로 쓰지 않아도 돼요.

tsx
<Combobox items={banks} value={bank} onValueChange={setBank}>  <ComboboxAnchor>    <ComboboxInput placeholder="은행 이름" aria-label="은행" />    <ComboboxCancel />    <ComboboxTrigger />  </ComboboxAnchor>  <ComboboxContent aria-label="은행">    <ComboboxEmpty>그런 은행이 없어요</ComboboxEmpty>    {(bank: string) => (      <ComboboxItem key={bank} value={bank}>        <ComboboxItemText>{bank}</ComboboxItemText>      </ComboboxItem>    )}  </ComboboxContent></Combobox>

호출하는 쪽에서 여닫기

고를 항목을 먼저 보여 줘야 하거나 다른 버튼으로 목록을 열어야 할 때 써요.

tsx
<div className="flex gap-2">  <Combobox    items={teams}    value={team}    onValueChange={setTeam}    open={teamOpen}    onOpenChange={setTeamOpen}    className="min-w-0 flex-1"  >    <ComboboxAnchor>      <ComboboxInput placeholder="부서 이름" aria-label="부서" />      <ComboboxTrigger />    </ComboboxAnchor>    <ComboboxContent aria-label="부서">      <ComboboxEmpty>그런 부서가 없어요</ComboboxEmpty>      {(team: string) => (        <ComboboxItem key={team} value={team}>          <ComboboxItemText>{team}</ComboboxItemText>        </ComboboxItem>      )}    </ComboboxContent>  </Combobox>  <Button color="neutral" emphasis="outline" onClick={() => setTeamOpen(true)}>부서 고르기</Button></div>

그룹

항목이 종류별로 나뉠 때 써요. 그룹 이름이 필요 없으면 ComboboxSeparator만 둬요.

tsx
const airports = [  { value: '국내', items: ['인천', '김포', '제주'] },  { value: '해외', items: ['나리타', '히스로'] },]<Combobox items={airports} value={airport} onValueChange={setAirport}>  <ComboboxAnchor>    <Search className="text-fg-subtle size-4 shrink-0" />    <ComboboxInput placeholder="공항 이름" aria-label="공항" />    <ComboboxTrigger />  </ComboboxAnchor>  <ComboboxContent aria-label="공항">    <ComboboxEmpty>그런 공항이 없어요</ComboboxEmpty>    {(group: typeof airports[number], index: number) => (      <Fragment key={group.value}>        {index > 0 && <ComboboxSeparator />}        <ComboboxGroup items={group.items} label={group.value}>          <ComboboxCollection>            {(one: string) => (              <ComboboxItem key={one} value={one}>                <ComboboxItemText>{one}</ComboboxItemText>              </ComboboxItem>            )}          </ComboboxCollection>        </ComboboxGroup>      </Fragment>    )}  </ComboboxContent></Combobox>

항목 설명

이름만으로 항목을 구분하기 어려울 때 써요. ComboboxInput에는 항목의 label만 보이고, label이 없는 객체는 itemToStringLabel로 보일 글자를 정해요.

tsx
const rooms = [  { value: '대회의실', label: '대회의실', description: '20명까지 들어가요' },  { value: '소회의실', label: '소회의실', description: '6명까지 들어가요' },  { value: '화상 부스', label: '화상 부스', description: '1명만 들어가요' },]<Combobox items={rooms} value={room} onValueChange={setRoom}>  <ComboboxAnchor>    <Search className="text-fg-subtle size-4 shrink-0" />    <ComboboxInput placeholder="회의실 이름" aria-label="회의실" />    <ComboboxTrigger />  </ComboboxAnchor>  <ComboboxContent aria-label="회의실">    <ComboboxEmpty>그런 회의실이 없어요</ComboboxEmpty>    {(one: typeof rooms[number]) => (      <ComboboxItem key={one.value} value={one}>        <ComboboxItemText>{one.label}</ComboboxItemText>        <ComboboxItemDescription>{one.description}</ComboboxItemDescription>      </ComboboxItem>    )}  </ComboboxContent></Combobox>

결과 없음

필터 결과가 없을 때 다음 동작을 할 수 있는 버튼을 함께 둬요.

tsx
<Combobox  items={zones}  filteredItems={shown}  value={picked}  onValueChange={setPicked}  inputValue={search}  onInputValueChange={setSearch}>  <ComboboxAnchor>    <Search className="text-fg-subtle size-4 shrink-0" />    <ComboboxInput placeholder="시간대 이름" aria-label="시간대" />    <ComboboxTrigger />  </ComboboxAnchor>  <ComboboxContent aria-label="시간대">    <ComboboxEmpty className="flex flex-col items-center gap-tight">      찾는 시간대가 없어요      <Button color="neutral" emphasis="ghost" size="sm" onClick={() => setSearch('')}>전체 목록 보기</Button>    </ComboboxEmpty>    {(zone: string) => (      <ComboboxItem key={zone} value={zone}>        <ComboboxItemText>{zone}</ComboboxItemText>      </ComboboxItem>    )}  </ComboboxContent></Combobox>

라벨과 도움말

Field로 감싸면 라벨과 도움말이 ComboboxInput에 연결돼요. 값이 올바르지 않으면 Field에 invalid를 주고 FieldError에 필요한 값을 적어요.

영문 이름으로 찾아요.

저장하기 전에 시간대를 골라야 해요.

tsx
<Field>  <FieldLabel>표시할 시간대</FieldLabel>  <Combobox    items={zones}    filteredItems={shown}    value={picked}    onValueChange={setPicked}    inputValue={search}    onInputValueChange={setSearch}  >    <ComboboxAnchor>      <Search className="text-fg-subtle size-4 shrink-0" />      <ComboboxInput placeholder="시간대 이름" />      <ComboboxTrigger />    </ComboboxAnchor>    <ComboboxContent aria-label="시간대">      <ComboboxEmpty>찾는 시간대가 없어요</ComboboxEmpty>      {(zone: string) => (        <ComboboxItem key={zone} value={zone}>          <ComboboxItemText>{zone}</ComboboxItemText>        </ComboboxItem>      )}    </ComboboxContent>  </Combobox>  <FieldHelp>영문 이름으로 찾아요.</FieldHelp></Field>

배경

Combobox 여러 개가 나란히 있으면 bare를 써요. 하나만 있거나 입력하는 곳이 먼저 눈에 띄어야 하면 well을 써요.

tsx
<Combobox items={banks} value={bank} onValueChange={setBank}>  <ComboboxAnchor>    <ComboboxInput placeholder="은행 이름" aria-label="은행" />    <ComboboxTrigger />  </ComboboxAnchor>  …</Combobox><Combobox items={banks} value={bank} onValueChange={setBank}>  <ComboboxAnchor surface="well">    <ComboboxInput placeholder="은행 이름" aria-label="은행" />    <ComboboxTrigger />  </ComboboxAnchor>  …</Combobox>

목록 줄에서 바로 바꾸기

표 셀이나 목록 줄처럼 값이 많이 모인 곳에서 써요. 펼침 아이콘과 지우기 버튼은 평소에 옅게 보여요.

  • 첫 화면 문구 다듬기
  • 로그인 오류 고치기
tsx
<ul className="frame rounded-r12 divide-line flex flex-col divide-y">  {tasks.map(task => (    <li key={task.title} className="flex items-center justify-between gap-2 py-1 pr-1 pl-4">      <span className="text-value">{task.title}</span>      <Combobox items={teams} value={task.team} onValueChange={team => assign(task, team)} className="w-36 shrink-0">        <ComboboxAnchor surface="ghost">          <ComboboxInput placeholder="담당 팀" aria-label={`${task.title} 담당 팀`} />          <ComboboxTrigger />        </ComboboxAnchor>        <ComboboxContent aria-label="팀">          <ComboboxEmpty>그런 팀이 없어요</ComboboxEmpty>          {teams.map(team => (            <ComboboxItem key={team} value={team}>              <ComboboxItemText>{team}</ComboboxItemText>            </ComboboxItem>          ))}        </ComboboxContent>      </Combobox>    </li>  ))}</ul>

버튼 옆

옆에 있는 버튼과 size를 맞춰요. ComboboxAnchor가 lg면 버튼도 lg예요.

tsx
<div className="flex gap-2">  <Combobox items={banks} value={bank} onValueChange={setBank} className="min-w-0 flex-1">    <ComboboxAnchor>      <ComboboxInput placeholder="은행 이름" aria-label="은행" />      <ComboboxTrigger />    </ComboboxAnchor>    …  </Combobox>  <Button color="brand" emphasis="solid">조회</Button></div><div className="flex gap-2">  <Combobox items={banks} value={bank} onValueChange={setBank} className="min-w-0 flex-1">    <ComboboxAnchor size="lg">      <ComboboxInput placeholder="은행 이름" aria-label="은행" />      <ComboboxTrigger />    </ComboboxAnchor>    …  </Combobox>  <Button color="brand" emphasis="solid" size="lg">조회</Button></div>

컨트롤

값{"picked":null,"search":""}
tsx
<Combobox  items={zones}  filteredItems={shown}  value={picked}  onValueChange={setPicked}  inputValue={search}  onInputValueChange={setSearch}>  <ComboboxAnchor>    <Search className="text-fg-subtle size-4 shrink-0" />    <ComboboxInput placeholder="시간대 이름" aria-label="시간대" />    <ComboboxTrigger />  </ComboboxAnchor>  <ComboboxContent aria-label="시간대">    <ComboboxEmpty>찾는 시간대가 없어요</ComboboxEmpty>    {(zone: string) => (      <ComboboxItem key={zone} value={zone}>        <ComboboxItemText>{zone}</ComboboxItemText>      </ComboboxItem>    )}  </ComboboxContent></Combobox>

명세

이름
이름값기본
Combobox
valuestring | null-
호출하는 쪽에서 고른 항목을 관리해요.
defaultValuestring | null-
처음 값이에요.
onValueChange(value: string | null, eventDetails) => void-
항목을 고르면 호출돼요.
openboolean-
호출하는 쪽에서 열림 상태를 관리해요.
defaultOpenbooleanfalse
처음에 열어 둘지 정해요.
onOpenChange(open: boolean, eventDetails) => void-
열리거나 닫히면 호출돼요.
inputValuestring-
호출하는 쪽에서 입력한 글자를 관리해요.
onInputValueChange(value: string, eventDetails) => void-
입력한 글자가 바뀌면 호출돼요.
itemsstring[]-
필터하기 전의 항목 전부를 넘겨요. 그룹은 { value, items } 배열로 넘겨요.
filteredItemsstring[]-
호출하는 쪽에서 필터한 항목을 넘겨요. 넘기면 Combobox는 필터하지 않아요.
filter(item, query) => boolean, null-
기본은 입력한 글자가 들어 있는 항목만 보여요. null이면 필터하지 않아요.
classNamestring-
ComboboxCollection을 뺀 모든 하위 컴포넌트에 쓸 수 있어요. Combobox에서는 전체를 감싼 wrapper에 적용돼요.
ComboboxAnchor
surfacebare, well, ghostbare
bare는 투명 배경에 테두리, well은 회색 배경이에요. ghost는 평소에 글자만 보이고, 마우스를 올리면 옅은 배경, 입력하거나 열면 조금 진한 배경이 생겨요. 여러 개가 나란히 있으면 bare, 하나만 있으면 well, 표 셀이나 목록 줄에서 값을 바로 바꾸면 ghost를 써요.
sizesm, md, lgmd
높이가 32, 36, 40px이에요.
ComboboxInput
placeholderstring-
ComboboxCancel
labelstring지우기
스크린 리더가 읽는 이름이에요.
ComboboxTrigger
labelstring목록 열기
스크린 리더가 읽는 이름이에요.
ComboboxContent
sideOffsetnumber, 함수4
기준과 떨어진 거리예요. 단위는 px이에요.
alignOffsetnumber, 함수0
align 방향으로 옮기는 거리예요. 단위는 px이에요.
collisionAvoidanceobject-
화면 가장자리에 닿을 때 뒤집을지, 밀어 넣을지 정해요. Base UI Positioner의 값을 그대로 받아요.
ComboboxGroup
labelstring-
그룹 위에 보여요.
itemsstring[]-
그룹에 들어가는 항목이에요. ComboboxCollection이 이 항목을 그려요.
ComboboxItem
valuestring필수
이 항목을 고르면 Combobox의 value가 이 값이 돼요.
disabledbooleanfalse