Floating
Select
키보드 내비게이션과 접근성을 갖춘 커스텀 선택 컴포넌트. 방향키·typeahead·ARIA listbox 패턴을 지원합니다.
설치
npm install @woon-ui/selectimport { Select } from '@woon-ui/select'
import '@woon-ui/select/css'기본 사용법
Select.Root에 value와 onValueChange를 연결하고, 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.Group과 Select.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.Item에 disabled를 주면 해당 항목이 비활성화됩니다. Select.Trigger에 disabled를 주거나 Select.Root에 disabled를 주면 전체가 비활성화됩니다.
Preview
disabled trigger
API
Select.Root
| Prop | Type | Default |
|---|---|---|
| value | string | — |
| 제어 모드: 선택된 값 | ||
| defaultValue | string | '' |
| 비제어 모드: 초기 선택 값 | ||
| onValueChange | (value: string) => void | — |
| 값 변경 콜백 | ||
| disabled | boolean | false |
| 전체 Select 비활성화 | ||
| side | 'top'|'bottom' | 'bottom' |
| 리스트가 표시되는 방향 | ||
| align | 'start'|'center'|'end' | 'start' |
| 트리거 기준 정렬 | ||
| sideOffset | number | 4 |
| 트리거와의 간격 (px) | ||
| alignOffset | number | 0 |
| 정렬 방향 오프셋 (px) | ||
Select.Trigger
| Prop | Type | Default |
|---|---|---|
| asChild | boolean | false |
| 자식 요소를 트리거로 사용 (Slot 패턴) | ||
| disabled | boolean | false |
| 트리거 비활성화 | ||
Select.Value
| Prop | Type | Default |
|---|---|---|
| placeholder | React.ReactNode | — |
| 값이 없을 때 표시할 내용 | ||
Select.Item
| Prop | Type | Default |
|---|---|---|
| value | string | — |
| 이 항목의 값. onValueChange에 전달됩니다. | ||
| disabled | boolean | false |
| 비활성화. 클릭·키보드 선택이 막히고 시각적으로 흐리게 표시됩니다. | ||
| asChild | boolean | false |
| 자식 요소를 항목으로 사용 (Slot 패턴) | ||
| textValue | string | — |
| 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;
}
}