CommandPalette
먼저 보고, 그다음 계약을 읽습니다
⌘K 스타일 검색·명령 팔레트 오버레이.
Component intent의도와 경계 읽기
document.body portal의 viewport 모달 combobox — 입력한 질의로 items 를 label·keywords·group 기준 부분일치 필터하고, group 별로 묶어 listbox 로 보여준다. open 상태는 소비자가 제어(controlled)하며 선택 시 onSelect + onClose 가 함께 불린다. 포커스 트랩·이전 포커스 복원·aria-activedescendant 로 WAI-ARIA dialog+combobox 패턴을 직접 구현한다. 항목은 CmdItem({ id, label, keywords?, meta?, group? }) 배열로 주입한다.
예제
SSOT에 등록된 실제 API 기준 snippet입니다. standalone 배지만 독립 실행 단위이며, fragment는 주변 state·handler 문맥을 생략합니다.
import { useState } from "react";import { CommandPalette, type CmdItem } from "@axe/ui";const items: CmdItem[] = [ { id: "new", label: "새 문서", group: "문서", meta: "⌘N" }, { id: "find", label: "검색", keywords: "find grep", group: "문서", meta: "⌘F" }, { id: "theme", label: "테마 전환", group: "설정" },];export function Launcher() { const [open, setOpen] = useState(false); return ( <> <button onClick={() => setOpen(true)}>명령 팔레트 (⌘K)</button> <CommandPalette open={open} items={items} ariaLabel="문서 명령 검색" placeholder="명령 검색…" onClose={() => setOpen(false)} onSelect={(item) => console.log(item.id)} /> </> );}<div class="axe-cmdk-backdrop"> <div class="axe-cmdk" role="dialog" aria-modal="true" aria-label="문서 명령 검색"> <div class="axe-cmdk__search-row"> <input class="axe-cmdk__input" role="combobox" aria-label="문서 명령 검색어" aria-expanded="true" aria-autocomplete="list" aria-controls="cmdk-list" placeholder="검색…" /> <button class="axe-cmdk__close" type="button" aria-label="검색 닫기">Esc</button> </div> <div id="cmdk-list" class="axe-cmdk__list" role="listbox"> <div class="axe-cmdk__group"> <div class="axe-cmdk__group-label">문서</div> <div class="axe-cmdk__item" role="option" aria-selected="true"> <span>새 문서</span><span class="axe-cmdk__item-meta">⌘N</span> </div> <div class="axe-cmdk__item" role="option" aria-selected="false"> <span>검색</span><span class="axe-cmdk__item-meta">⌘F</span> </div> </div> </div> </div></div>Props
TSX 소스가 진실입니다. 주요 export 컴포넌트의 public API만 노출합니다.
| 이름 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
open | boolean | 필수 | — | 열림 여부. 소비자가 제어(false 면 null 반환 — 언마운트). |
onClose | () => void | 필수 | — | 닫기 요청 — Esc·backdrop 클릭·항목 선택 후 호출. |
items | CmdItem[] | 필수 | — | 후보 목록. CmdItem = { id, label, keywords?, meta?, group? }. |
onSelect | (item: CmdItem) => void | 필수 | — | 항목 확정 시 호출(뒤이어 onClose 도 실행됨). |
placeholder | string | — | "검색…" | 검색 input placeholder. |
ariaLabel | string | — | "명령 검색" | dialog의 접근성 이름. 검색 input 이름도 이 값에서 파생. |
className | string | — | — | 루트 backdrop(.axe-cmdk-backdrop)에 덧대는 클래스. |
.axe-* 클래스 계약
React 밖에서도 같은 표면을 그리는 공개 계약입니다. stable은 minor 버전 안에서 이름을 바꾸지 않습니다.
| 클래스 | 안정성 | 용도 |
|---|---|---|
.axe-cmdk-backdrop | stable | 화면 전체 backdrop — 바깥 클릭 시 닫힘. |
.axe-cmdk | stable | 떠 있는 role=dialog 패널(포커스 트랩 컨테이너). |
.axe-cmdk__search-row | stable | 검색 input + 명시적 닫기 버튼 행. |
.axe-cmdk__input | stable | 상단 검색 input(role=combobox). |
.axe-cmdk__close | stable | 검색 닫기 버튼(Esc 힌트 포함). |
.axe-cmdk__list | stable | 결과 스크롤 영역(role=listbox). |
.axe-cmdk__group | stable | 동일 group 항목 묶음 래퍼. |
.axe-cmdk__group-label | stable | group 헤더 라벨(group 있을 때만). |
.axe-cmdk__item | stable | 항목 행(role=option). 강조는 [aria-selected="true"]·:hover 로. |
.axe-cmdk__item-meta | stable | 항목 우측 메타(단축키 등, mono). |
.axe-cmdk__empty | stable | 결과 없음 안내. |
접근성
키보드, ARIA, 구현 노트를 함께 검토합니다.
- ↑/↓ 활성 항목 이동(범위 clamp)
- Enter 활성 항목 선택(한글 IME 조합 중 Enter 는 글자 확정으로 무시)
- Esc 닫기
- Tab/Shift+Tab 팔레트 내부 순환(포커스 트랩 — 밖으로 탈출 금지)
패널 = role=dialog + aria-modal=true + aria-label. input = role=combobox + 접근 이름 + aria-expanded=true + aria-autocomplete=list + aria-controls(listbox) + aria-activedescendant(활성 option id). 리스트 = role=listbox, 그룹 = role=group + aria-labelledby, 항목 = role=option + aria-selected.
document.body portal이라 transformed/backdrop-filter 조상에 fixed modal이 갇히지 않는다. 열릴 때 같은 window의 다른 CommandPalette를 닫아 단일 modal만 유지하고, 최초 opener 포커스를 보존한 채 rAF로 input에 포커스한다. 마지막 palette가 닫힐 때만 body scroll lock을 풀고 최초 opener로 포커스를 복원한다. 방향키/Enter 선택은 input에서만 가로채므로 닫기 버튼의 네이티브 Enter를 침범하지 않는다. 활성 항목은 scrollIntoView(block:nearest)로 시야 유지. ⌘K 전역 단축키는 컴포넌트가 등록하지 않음 — open 토글은 소비자 몫.