# Autocomplete 초점이 들어오면 목록을 펼치고, 입력한 글자를 포함하는 항목만 남겨 보여주는 검색형 선택 엘리먼트입니다. > 원본: `03_ASSET_GUIDE/catalog/autocomplete.js` · CSS: `03_ASSET_GUIDE/styles/elements/autocomplete.css` > kind `component` · category `selection` · status `stable` · specSource `measured` > [!NOTE] > **이번 31개 목록의 `combobox` 자리를 실제로 구현한 항목입니다** > `docs/components/combobox.md` 는 아직 `proposed` 상태의 제안 문서이고, 이 문서는 그중 > "타이핑으로 목록을 좁힌다" 하나에 집중해 실제 CSS 로 구현한 것입니다. 수치는 > `styles/elements/autocomplete.css` 에서 실측했습니다. 참고한 것은 Spectrum 의 > Combo Box 와 Ant Design 의 AutoComplete, 그리고 이 저장소의 > [Text Input](./text-input.md) · [Select](./select.md) 규약입니다. --- ## 1. 구성 값을 입력받는 **Field** 와, 초점이 들어오면 그 아래에 펼쳐지는 **Listbox** 로 나뉩니다. [Select](./select.md) 와 달리 Trigger 가 버튼이 아니라 실제 입력 필드이고, Option 은 입력과 일치한 구간을 강조합니다. ``` ┌──────────────────────────────────────┐ │ [🔍] 설| (×) │ ← Field ① Icon ② Input ③ Clear ④ └──────────────────────────────────────┘ ┌──────────────────────────────────────┐ │ 기본 항목 │ ← Listbox ⑤ Group label ⑥ │ ▌[설]정 관리 │ ← Option + Match ⑦ (data-active) │ 상세 [설]정 ✓ │ ← Check ⑧ (aria-selected) │ 전체 항목 │ └──────────────────────────────────────┘ (2개 결과) ← Status ⑨ (aria-live, 화면에 안 보임) ``` | # | 요소 | 필수 | 클래스 | 설명 | |---|---|---|---|---| | — | Root | 필수 | `atom-autocomplete` | 상태(`data-state` · `data-invalid` · `data-disabled` · `data-loading`)를 가집니다. | | ① | Field | 필수 | `atom-autocomplete__field` | 실제 `` 을 감싸는 상자입니다. 상자가 테두리를 그리고 input 은 투명합니다. | | ② | Icon | 선택 | `atom-autocomplete__icon` | 검색이라는 성격을 알리는 앞쪽 아이콘입니다. 선택입니다. | | ③ | Input | 필수 | `atom-autocomplete__input` | `role="combobox"` 를 가진 입력입니다. 초점은 열려 있는 동안에도 여기 머뭅니다. | | ④ | Clear | 선택 | `atom-autocomplete__clear` | 입력을 한 번에 지웁니다. 값이 있을 때만 나타납니다. | | — | Spinner | `loading` 일 때 | `atom-autocomplete__spinner` | 원격 조회 중 Clear 자리에 들어갑니다. | | ⑤ | Listbox | 필수 | `atom-autocomplete__listbox` | 필터된 선택지 목록. Field 바로 아래에 붙습니다. | | ⑥ | Group label | 선택 | `atom-autocomplete__group-label` | 선택지가 많을 때 묶음의 이름입니다. | | ⑦ | Option | 필수 | `atom-autocomplete__option` · `__option-icon` · `__option-text` · `__option-label` · `__option-description` | 라벨은 필수, 아이콘과 설명은 선택입니다. | | ⑦ | Match | — | `atom-autocomplete__match` | Option 라벨 안에서 입력과 일치한 구간입니다. | | ⑧ | Check | — | `atom-autocomplete__option-check` | 이미 선택된 항목에만 나타납니다. | | — | Empty | 결과 0개일 때 | `atom-autocomplete__empty` · `__empty-title` · `__empty-hint` | 검색어와 다음 행동 안내. | | — | Loading | `loading` 일 때 | `atom-autocomplete__loading` | Listbox 안의 `불러오는 중` 한 줄. | | ⑨ | Status | 필수 | `atom-autocomplete__status` | 결과 개수를 읽어주는 `aria-live` 영역입니다. 화면에는 보이지 않습니다. | `__text` · `__placeholder` · `__caret` 은 카드 프리뷰처럼 초점이 없는 정적 데모가 입력된 글자와 커서를 그리기 위한 대역입니다. 실제 컴포넌트는 `__input` 을 씁니다. --- ## 2. 종류 / 크기 ### 초점과 열림 초점이 들어오면 입력 없이도 전체 목록이 펼쳐집니다. 고를 수 있는 값을 입력 전에 보여주기 위해서입니다. `Esc` 나 바깥 클릭, 초점 이탈로 닫히고 항목을 고르면 값이 채워지면서 함께 닫힙니다. | 동작 | 결과 | | --- | --- | | Field 에 초점 | `data-state` 가 `open` 이 되고 전체 목록이 펼쳐집니다. | | 입력 | 입력을 포함하는 항목만 남습니다. 목록은 열린 채입니다. | | 항목 선택 | 값이 Input 에 채워지고 목록이 닫힙니다. | | Esc | 목록만 닫힙니다. 입력한 글자는 그대로 남습니다. | | 초점 이탈 (blur) | 목록이 닫힙니다. | ### 필터링 매칭은 **앞자리 일치(prefix)가 아니라 부분 문자열 일치(contains)** 입니다. `정` 을 입력하면 `정보 수정` 뿐 아니라 `설정 관리` 와 `사용자 정보` 도 남습니다. 이름의 일부만 기억하는 경우에도 찾을 수 있게 하기 위해서입니다. - 대소문자는 구분하지 않습니다. - 일치한 구간은 `__match` 로 감싸 강조합니다. 한 라벨 안에 여러 번 걸리면 모두 강조합니다. - 필터는 입력할 때마다 즉시 반영합니다. 원격 조회라면 요청을 debounce 하고 그동안 `data-loading` 을 켭니다. > [!NOTE] > **강조는 굵기와 색으로 합니다** > 일치 구간은 줄바꿈을 만나면 두 줄에 걸쳐 나뉩니다. 배경을 칠하면 잘린 상자가 남으므로 > `--atom-fg-brand` 와 굵기만 씁니다. ### 활성 항목과 선택된 항목 `↑` `↓` 로 옮겨 다니는 **활성 항목**과 이미 고른 **선택된 항목**은 구분합니다. 활성 항목은 `data-active` 로 브랜드 톤 배경을 얻고, 선택된 항목은 체크와 굵은 라벨로 표시합니다. 둘을 같은 색으로 칠하면 화살표 키로 이동한 위치가 보이지 않습니다. | 표시 | 속성 | 의미 | | --- | --- | --- | | 브랜드 톤 배경 | `data-active` | 키보드 커서가 지금 여기 있습니다. 초점은 Input 에 있습니다. | | 회색 배경 | `:hover` | 포인터가 지금 여기 있습니다. | | 체크 + 굵은 라벨 | `aria-selected="true"` | 이미 고른 값입니다. | | 흐림 | `aria-disabled="true"` | 고를 수 없습니다. 목록에서 빼지 않고 남겨 이유를 설명합니다. | ### 결과 없음 필터 결과가 0개이면 검색어와 다음에 해볼 방법을 한 줄로 알려줍니다. 빈 Listbox 만 두면 오류로 보입니다. ### 로딩 선택지를 서버에서 가져오는 경우에만 씁니다. `data-loading` 은 Field 의 Clear 를 Spinner 로 바꾸고 Listbox 에 `불러오는 중` 한 줄을 남깁니다. 결과 없음과 로딩 중을 구분하기 위해 목록을 비우지 않습니다. ### 크기 (Size) 높이는 컨트롤 공통 토큰 `--atom-control-h-small` · `--atom-control-h` · `--atom-control-h-large`(40 · 48 · 56px)를 씁니다. 같은 폼 안의 [Text Input](./text-input.md) · [Select](./select.md) 와 높이를 맞춥니다. | Size | Field 높이 | 모서리 | 폰트 | Icon | | --- | --- | --- | --- | --- | | `small` | 40px | 8px | 14px | 16px | | `medium` | 48px | 8px | 15px | 18px | | `large` | 56px | 12px | 16px | 18px | ### 종류 (Variant) `outline` 이 기본이며 Text Input 과 같습니다. 툴바처럼 이미 회색 면 위에 놓일 때만 `weak` 로 바꿉니다. ### Field 와 함께 라벨과 필수 표시, 하단 설명은 [Field](./field.md) 가 담당합니다. ### 클래스 계약 `styles/elements/autocomplete.css` 헤더와 원본 `registry.json` `css.blocks` 기준입니다. 클래스가 없는 값(—)은 기본 스타일 그대로입니다. | Prop | 값 | 클래스 | |---|---|---| | `size` | `small` · `medium` · `large` | `--size_small` · — · `--size_large` | | `variant` | `outline` · `weak` | — · `--variant_weak` | 상태 선택자: `[data-state="closed|open"]` · `[data-invalid]` · `[data-disabled]` · `[data-loading]` · `__field:hover` · `__field:focus-within` · `__option[data-active]` · `__option[aria-selected="true"]` · `__option[aria-disabled="true"]` --- ## 3. 상태 `closed` 와 `open` 이 기본 상태이고 여기에 `data-invalid` 와 `data-disabled` 가 겹칩니다. 열려 있으면 초점이 들어와 있는 상태이므로 `open` 일 때 Field 는 초점 링을 유지합니다. | State | 표현 | 선택자 | |---|---|---| | `closed` | Listbox 숨김 | `[data-state="closed"]` | | `open` | Listbox 표시, Field 초점 링 유지 | `[data-state="open"]` | | hover | Field 테두리 `--atom-control-border-hover` | `__field:hover` | | focus | Field 테두리 `--atom-control-border-focus` | `__field:focus-within` | | `loading` | Clear 대신 Spinner | `[data-loading]` | | `invalid` | Field 테두리 `--atom-control-border-invalid` | `[data-invalid]` | | `disabled` | `opacity: 0.45`, 입력 불가, Field 배경 `--atom-bg-neutral-weak` | `[data-disabled]` | --- ## 4. Props | Prop | Type | Default | 필수 | 설명 | |---|---|---|---|---| | `value` | `string` | | | 선택이 확정된 항목의 값입니다. | | `inputValue` | `string` | | | Input 에 지금 적혀 있는 글자입니다. 필터의 기준이며 `value` 와 다를 수 있습니다. | | `options` | `Array<{ value, label, description?, icon?, disabled? }>` | | ● | 필터 전 전체 선택지입니다. | | `filter` | `(option, inputValue) => boolean` | 대소문자 무시 contains | | 필터 규칙입니다. 기본값은 라벨에 입력이 포함되는지 봅니다. | | `openOnFocus` | `boolean` | `true` | | 초점이 들어올 때 입력 없이도 전체 목록을 펼칩니다. | | `highlightMatch` | `boolean` | `true` | | Option 라벨에서 일치한 구간을 `__match` 로 감쌉니다. | | `placeholder` | `string` | | | 값이 없을 때 Input 에 보이는 문구입니다. | | `clearable` | `boolean` | `true` | | 값이 있을 때 Clear 버튼을 보여줍니다. | | `loading` | `boolean` | `false` | | 원격 조회 중임을 알립니다. Clear 자리에 Spinner 가 들어갑니다. | | `size` | `"small" \| "medium" \| "large"` | `"medium"` | | Field 높이와 폰트 크기를 결정합니다. | | `variant` | `"outline" \| "weak"` | `"outline"` | | Field 의 배경과 테두리 처리입니다. | | `invalid` | `boolean` | `false` | | Field 테두리를 오류 색으로 바꿉니다. | | `disabled` | `boolean` | `false` | | 입력도 열기도 할 수 없게 만듭니다. | | `emptyText` | `string` | `"검색 결과가 없어요"` | | 필터 결과가 0개일 때 Listbox 에 보일 문구입니다. | | `onInputValueChange` | `(inputValue: string) => void` | | | 입력이 바뀔 때마다 호출됩니다. 원격 조회는 여기에 debounce 를 겁니다. | | `onValueChange` | `(value: string) => void` | | | 항목을 골라 선택이 확정될 때 호출됩니다. | ### Parts (compound) Autocomplete 는 파트 없이 하나의 컴포넌트입니다. 내부 DOM 은 원본 Markup 을 그대로 따릅니다. | Part | 요소 | 설명 | |---|---|---| | `Autocomplete` | `
` | Root. 위 Props 를 받습니다. `ref` 는 내부 `` 를 가리킵니다. [Field](./field.md) 안에서는 `useFieldControl()` 로 id · aria-describedby · invalid · disabled · required · size 를 받습니다. | --- ## 5. Component tokens | 대상 | 속성 | Token | Value | |---|---|---|---| | Field | background | `{color.bg.layer}` | | | Field | min-height | `{control.h}` | 48px | | Field (`small` · `large`) | min-height | `{control.h-small}` · `{control.h-large}` | 40 · 56px | | Field | border | `{control.border}` | `{color.border.default}` 1px inset | | Field | radius | `{radius.r2}` | 8px | | Field (`large`) | radius | `{radius.r3}` | 12px | | Field (focus · open) | border | `{control.border-focus}` | `{color.primary.600}` inset 2px | | Field (invalid) | border | `{color.fg.critical}` | inset 2px | | Listbox | background | `{color.bg.layer}` | | | Listbox | radius | `{radius.r3}` | 12px | | Listbox | shadow | `{shadow.s2}` | + inset 1px `{color.border.subtle}` | | Option (active) | background | `{color.bg.side-active}` | | | Option (hover) | background | `{color.bg.neutral-weak}` | | | Match | color | `{color.fg.brand}` | weight 700 | | Empty hint | color | `{color.fg.neutral-subtle}` | 12px | | Spinner | border | `{color.border.default}` | + `{color.fg.neutral-muted}` | ```json { "autocomplete": { "field": { "bg": { "$type": "color", "$value": "{color.bg.layer}" }, "min-height": { "$type": "dimension", "default": { "$value": "{control.h}" }, "small": { "$value": "{control.h-small}" }, "large": { "$value": "{control.h-large}" } }, "border": { "$type": "shadow", "$value": "{control.border}" }, "border-focus": { "$type": "shadow", "$value": "{control.border-focus}" }, "border-invalid": { "$type": "shadow", "$value": "{control.border-invalid}" }, "radius": { "$type": "dimension", "default": { "$value": "{radius.r2}" }, "large": { "$value": "{radius.r3}" } } }, "listbox": { "bg": { "$type": "color", "$value": "{color.bg.layer}" }, "radius": { "$type": "dimension", "$value": "{radius.r3}" }, "shadow": { "$type": "shadow", "$value": "{shadow.s2}" }, "border": { "$type": "color", "$value": "{color.border.subtle}" } }, "option": { "bg-active": { "$type": "color", "$value": "{color.bg.side-active}" }, "bg-hover": { "$type": "color", "$value": "{color.bg.neutral-weak}" } }, "match": { "$type": "color", "$value": "{color.fg.brand}" }, "empty-hint": { "$type": "color", "$value": "{color.fg.neutral-subtle}" }, "spinner": { "track": { "$type": "color", "$value": "{color.border.default}" }, "head": { "$type": "color", "$value": "{color.fg.neutral-muted}" } } } } ``` --- ## 6. 접근성 WAI-ARIA 의 combobox 패턴을 따릅니다. 입력을 이어갈 수 있도록 **초점은 Input 에 유지합니다.** - **역할** — Input 은 `role="combobox"` · `aria-expanded` · `aria-controls` · `aria-autocomplete="list"` 를 가집니다. Listbox 는 `role="listbox"`, 각 항목은 `role="option"` 입니다. - **활성 항목** — 초점은 Input 에 두고 `aria-activedescendant` 로 활성 Option 의 `id` 를 가리킵니다. 화면에는 `data-active` 로 같은 항목을 표시합니다. - **선택 상태** — 이미 고른 항목에만 `aria-selected="true"` 를 둡니다. 활성 항목 표시에 `aria-selected` 를 돌려 쓰지 않습니다. - **키보드** — `↓` `↑` 로 이동(닫혀 있으면 `↓` 가 목록을 엽니다) · `Enter` 로 활성 항목 선택 · `Esc` 로 닫기 · `Tab` 은 목록을 닫고 다음 필드로 넘어갑니다. - **알림** — 결과 개수가 바뀔 때마다 `role="status"` `aria-live="polite"` 영역에 `3개 결과` 처럼 남깁니다. 목록이 줄어드는 것은 시각적으로만 드러나기 때문입니다. - **결과 없음** — 안내 문구도 같은 live 영역으로 알립니다. Listbox 가 비는 것만으로는 스크린리더에 전달되지 않습니다. - **대비** — 강조 구간은 `--atom-fg-brand` 로 본문 텍스트 4.5:1 을 넘고, 활성 항목 배경은 색만으로 구분하지 않도록 `aria-activedescendant` 와 짝을 이룹니다. > [!CAUTION] > **강조만으로 상태를 전달하지 않습니다** > `__match` 는 색과 굵기로만 구분되므로 이것을 필터 결과의 유일한 신호로 쓰면 안 됩니다. > 결과 개수는 반드시 live 영역으로 함께 알립니다. Specification 의 요약: - **역할** — Input 은 `combobox`, 목록은 `listbox`, 항목은 `option`. - **키보드** — `↓` `↑` 이동 · `Enter` 선택 · `Esc` 닫기 · `Tab` 닫고 이동. - **스크린리더** — 초점은 Input 에 머물고, 활성 항목은 `aria-activedescendant` 로만 가리킵니다. 결과 개수는 `aria-live="polite"` 로 알립니다. - **대비** — Match 는 `--atom-fg-brand`, 활성 항목 배경은 `--atom-bg-side-active` 로 텍스트 4.5:1 · UI 요소 3:1 을 충족합니다. --- ## 7. 사용 가이드 ### 초점이 들어오면 바로 보여줍니다 초점이 들어오면 전체 목록을 먼저 보여주고, 타이핑은 목록을 좁히는 데 씁니다. 입력해야만 목록이 뜨면 자유 입력 필드로 오인해 목록에 없는 값을 입력하게 됩니다. - ✅ **Do** 초점만으로 전체 목록을 펼쳐 무엇을 고를 수 있는지 먼저 보여줍니다. - ❌ **Don't** 글자를 쳐야만 목록이 나타나게 두지 않습니다. ### 부분 일치로 찾고, 찾은 자리를 보여줍니다 앞자리만 맞추는 필터는 `설정 관리` 를 `정` 으로 찾지 못합니다. 일치한 구간은 반드시 강조해 항목이 남은 이유를 보여줍니다. - ✅ **Do** 가운데에 걸린 항목까지 남기고, 일치한 구간을 강조합니다. - ❌ **Don't** 앞자리만 비교해 후보를 잘라내지 않습니다. ### 결과가 없을 때도 다음 행동을 줍니다 빈 상자만으로는 결과 없음과 로딩 중을 구분할 수 없습니다. 검색어와 바꿔 볼 방법을 한 줄로 남기세요. - ✅ **Do** 검색어를 되짚어주고 다시 시도할 방법을 알려줍니다. - ❌ **Don't** 빈 Listbox 를 그대로 띄워두지 않습니다. ### Select · Multi Select 와 구분하기 **선택지의 수와 사용자가 답을 이미 아는지** 로 나눕니다. | 상황 | 쓸 컴포넌트 | | --- | --- | | 선택지 2~5개, 전부 보여주고 싶다 | [Radio](./radio.md) | | 선택지 6개 이상이고 목록이 정해져 있다, 하나만 고른다 | [Select](./select.md) | | 선택지 6개 이상이고 여러 개를 고른다 | [Multi Select](./multi-select.md) | | 선택지가 수십 개 이상이고, 사용자가 찾을 값을 대체로 알고 있다 | **Autocomplete** | | 목록에 없는 값도 자유롭게 적을 수 있어야 한다 | [Text Input](./text-input.md) | > [!WARNING] > **Autocomplete 는 자유 입력을 받지 않습니다** > 값은 목록 안에서만 확정됩니다. 목록에 없는 글자를 남긴 채 폼을 넘기면 `data-invalid` 로 > 막고, 자유 입력이 필요하다면 Text Input 을 쓰세요. ### 관련 컴포넌트 - [Select](./select.md) — 목록이 짧고 정해져 있어 타이핑이 필요 없을 때. - [Multi Select](./multi-select.md) — 좁힌 목록에서 여러 개를 골라야 할 때. - [Text Input](./text-input.md) — Field 의 상자·상태 처리를 그대로 물려받았습니다. - [Field](./field.md) — 라벨과 하단 설명을 담당합니다. --- ## 8. 예시 ```tsx ``` ```html
설정 관리

검색 결과가 없어요 다른 이름으로 검색해보세요

3개 결과
```