MENU
Wwoon

Floating

Select

키보드 내비게이션과 접근성을 갖춘 커스텀 선택 컴포넌트. 방향키·typeahead·ARIA listbox 패턴을 지원합니다.

설치

npm install @woon-ui/select
import { Select } from '@woon-ui/select'
import '@woon-ui/select/css'

기본 사용법

Select.RootvalueonValueChange를 연결하고, Select.Trigger 안에 Select.Value를 배치합니다.

Preview

import { Select } from '@woon-ui/select'
import '@woon-ui/select/css'

function Example() {
  const [value, setValue] = useState('')

  return (
    <Select.Root value={value} onValueChange={setValue}>
      <Select.Trigger>
        <Select.Value placeholder="프레임워크 선택" />
      </Select.Trigger>
      <Select.Content>
        <Select.Item value="react">React</Select.Item>
        <Select.Item value="vue">Vue</Select.Item>
        <Select.Item value="svelte">Svelte</Select.Item>
      </Select.Content>
    </Select.Root>
  )
}

키보드 동작:

  • — 항목 이동 (닫힌 상태에서 누르면 열림)
  • Enter / Space — 항목 선택
  • Escape — 닫기
  • 문자 입력 — typeahead (일치하는 항목으로 이동)

그룹과 Label

Select.GroupSelect.Label로 관련 항목을 묶고 레이블을 붙일 수 있습니다.

Preview

<Select.Content>
  <Select.Group>
    <Select.Label>프론트엔드</Select.Label>
    <Select.Item value="ts">TypeScript</Select.Item>
    <Select.Item value="js">JavaScript</Select.Item>
  </Select.Group>
  <Select.Separator />
  <Select.Group>
    <Select.Label>백엔드</Select.Label>
    <Select.Item value="go">Go</Select.Item>
    <Select.Item value="rust" disabled>Rust (준비 중)</Select.Item>
  </Select.Group>
</Select.Content>

Disabled

Select.Itemdisabled를 주면 해당 항목이 비활성화됩니다. Select.Triggerdisabled를 주거나 Select.Rootdisabled를 주면 전체가 비활성화됩니다.

Preview

disabled trigger

API

Select.Root

PropTypeDefault
valuestring
제어 모드: 선택된 값
defaultValuestring''
비제어 모드: 초기 선택 값
onValueChange(value: string) => void
값 변경 콜백
disabledbooleanfalse
전체 Select 비활성화
side'top'|'bottom''bottom'
리스트가 표시되는 방향
align'start'|'center'|'end''start'
트리거 기준 정렬
sideOffsetnumber4
트리거와의 간격 (px)
alignOffsetnumber0
정렬 방향 오프셋 (px)

Select.Trigger

PropTypeDefault
asChildbooleanfalse
자식 요소를 트리거로 사용 (Slot 패턴)
disabledbooleanfalse
트리거 비활성화

Select.Value

PropTypeDefault
placeholderReact.ReactNode
값이 없을 때 표시할 내용

Select.Item

PropTypeDefault
valuestring
이 항목의 값. onValueChange에 전달됩니다.
disabledbooleanfalse
비활성화. 클릭·키보드 선택이 막히고 시각적으로 흐리게 표시됩니다.
asChildbooleanfalse
자식 요소를 항목으로 사용 (Slot 패턴)
textValuestring
typeahead용 텍스트. children이 문자열이면 자동 추출됩니다.

Select.Content

Select.Content는 floating-ui로 포지셔닝되는 listbox입니다. 포지셔닝 옵션(side, align, sideOffset)은 Select.Root에서 설정합니다.

Select.Group / Select.Label / Select.Separator

구조적 요소로 별도 props 없이 children만 받습니다.

스타일

@woon-ui/select/css로 기본 스타일이 적용됩니다. 특정 값만 바꾸려면 선택자로 override하세요.

/* 트리거 너비 조정 */
[data-woon-select-trigger] {
  min-width: 14rem;
}

/* 드롭다운 최소 너비 */
[data-woon-select-content] {
  --woon-select-content-min-width: 14rem;
}

/* 선택된 아이템 색상 */
[data-woon-select-item][data-selected] {
  color: #2563eb;
}

완전히 교체하려면 @woon-ui/react/css import를 제거하고 아래 CSS를 복사해서 수정하세요.

/* ─── Select Trigger ─────────────────────────────────────────────────────────── */

[data-woon-select-trigger],
[data-woon-select-trigger] *,
[data-woon-select-trigger] *::before,
[data-woon-select-trigger] *::after {
  box-sizing: border-box;
}

[data-woon-select-trigger] {
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  height: 2.25rem;
  min-width: 10rem;
  padding: 0 0.75rem;
  background: #fff;
  border: 1px solid #e4e4e7;
  border-radius: 6px;
  font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  font-size: 0.875rem;
  color: #18181b;
  cursor: pointer;
  outline: none;
  transition: border-color 120ms, box-shadow 120ms;
}

[data-woon-select-trigger]:hover {
  border-color: #a1a1aa;
}

[data-woon-select-trigger]:focus-visible {
  border-color: #18181b;
  box-shadow: 0 0 0 3px rgba(24, 24, 27, 0.12);
}

[data-woon-select-trigger][data-state='open'] {
  border-color: #18181b;
}

[data-woon-select-trigger][data-disabled] {
  opacity: 0.4;
  pointer-events: none;
}

/* ─── Select Value ────────────────────────────────────────────────────────────── */

[data-woon-select-value] {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;
}

[data-woon-select-value][data-placeholder] {
  color: #a1a1aa;
}

/* ─── Select 외부 래퍼 (floating-ui 포지셔닝 전용) ──────────────────────────── */

[data-woon-select-floating] {
}

/* ─── Select Content ─────────────────────────────────────────────────────────── */

[data-woon-select-content],
[data-woon-select-content] *,
[data-woon-select-content] *::before,
[data-woon-select-content] *::after {
  box-sizing: border-box;
}

[data-woon-select-content] {
  min-width: var(--woon-select-content-min-width, 10rem);
  max-height: 18rem;
  overflow-y: auto;
  padding: 0.25rem;
  background: #fff;
  border-radius: 8px;
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.06),
    0 4px 16px rgba(0, 0, 0, 0.12);
  font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  font-size: 0.875rem;
  line-height: 1.5;
  outline: none;
}

[data-woon-select-content][data-state='open'] {
  animation: woon-select-in 160ms cubic-bezier(0.16, 1, 0.3, 1);
}

@keyframes woon-select-in {
  from { opacity: 0; scale: 0.97; }
  to   { opacity: 1; scale: 1; }
}

/* ─── Item ───────────────────────────────────────────────────────────────────── */

[data-woon-select-item] {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.375rem 0.625rem;
  border-radius: 5px;
  color: #18181b;
  cursor: default;
  user-select: none;
  outline: none;
  transition: background-color 100ms;
}

[data-woon-select-item][data-highlighted] {
  background-color: #f4f4f5;
}

[data-woon-select-item][data-selected] {
  font-weight: 500;
}

[data-woon-select-item][data-disabled] {
  opacity: 0.4;
  pointer-events: none;
}

/* ─── Separator ──────────────────────────────────────────────────────────────── */

[data-woon-select-separator] {
  height: 1px;
  margin: 0.25rem 0.625rem;
  background-color: #e4e4e7;
}

/* ─── Label ──────────────────────────────────────────────────────────────────── */

[data-woon-select-label] {
  padding: 0.375rem 0.625rem 0.25rem;
  font-size: 0.75rem;
  font-weight: 600;
  color: #71717a;
  user-select: none;
}

/* ─── Group ──────────────────────────────────────────────────────────────────── */

[data-woon-select-group] + [data-woon-select-group] {
  margin-top: 0.25rem;
}

@media (prefers-reduced-motion) {
  [data-woon-select-content] {
    animation: none !important;
  }
}