ComposerBar
ChatGPT식 슬롯 기반 컴포저 셸 — 좌측 액션·중앙 textarea·우측 액션. 알약/박스 형태, 고정 지오메트리. Enter 전송·한글 IME 보호·자동 높이.
마지막 업데이트 2026-06-24
한눈에#
ChatGPT식 입력창을 위한 슬롯 기반 컴포저 셸입니다. 좌측 액션(+)·중앙 textarea·우측 액션(마이크·음성·전송)을 슬롯으로 조립합니다. 셸은 알약(pill)·박스(boxed) 형태와 고정 height/radius/spacing을 토큰으로 못 박아 흔들리지 않고, 버튼은 IconButton을 슬롯에 넣어 재사용합니다.
빠른 동작 칩·+ 메뉴로 모드 활성 · 모델 선택 · Enter 전송 · 버튼 hover 시 단축키 툴팁
- 좌측·우측 액션 슬롯
- 알약(pill) 셸·고정 지오메트리
- Enter 전송·Shift+Enter 줄바꿈
- 한글 IME 보호·자동 높이
입력하면 음성 버튼이 전송으로 바뀝니다 — 첨부·마이크·음성은 슬롯의 IconButton
사용 시점#
좌/우 액션이 있는 ChatGPT식 입력창이면 ComposerBar — 전송 버튼 하나면 충분하면 아래 AiChatInput 프리셋을 쓰세요(같은 셸 위에 단일 전송을 미리 엮은 것).
권장 — 이렇게 쓰세요
지양 — 이러지 마세요
빠른 동작 칩·+ 메뉴로 모드 활성 · 모델 선택 · Enter 전송 · 버튼 hover 시 단축키 툴팁
쓴다 — 첨부·음성 등 여러 액션을 가진 ChatGPT식 컴포저(슬롯으로 버튼 구성)
대신 Textarea — 액션 없는 일반 여러 줄 텍스트 입력
Enter 전송 · Shift+Enter 줄바꿈 · 한글 조합 중 Enter는 무시됩니다
프리셋 — 전송 버튼 하나면 — AiChatInput 프리셋(아래 「프리셋」 절)
해부#
셸과 동작만 컴포넌트가 소유하고, 액션 버튼은 소비자가 슬롯에 주입합니다. 이 분리로 버튼 상태(hover·focus·disabled)는 IconButton을 그대로 재사용하고, 셸은 지오메트리만 책임집니다.
| 슬롯 | prop | 권장 |
|---|---|---|
| 좌측 액션 | leadingAction | <IconButton variant="subtle" size="sm">(첨부 +) |
| 중앙 입력 | (내장) | 제어형 textarea — value/onValueChange |
| 우측 액션 | trailingActions | 마이크 ghost · 주 액션 solid(음성↔전송) |
예시 API의
variant="filled"는 WDSIconButton에서 채움 정도에 따라subtle(중립 채움) 또는solid(브랜드 채움)로 매핑합니다 — WDS는 ChatGPT의 검정 대신 브랜드 토큰을 씁니다.
제어형 계약 — 중앙 입력은 제어형 textarea입니다(value/onValueChange). 입력 상태는 소비자가 소유하고, 전송 후 입력 비우기도 소비자가 onSubmit에서 value를 ''로 되돌려 수행합니다. onSubmit은 키보드 Enter 전용 보조 경로이고, 실제 전송 버튼은 슬롯의 IconButton이 담당합니다.
변형·상태#
동작은 하나의 어휘로 통일하되, 셸 형태를 variant로 제공합니다.
| variant | 형태 | 정렬 | 쓰는 곳 |
|---|---|---|---|
pill (기본) | 완전 둥근 알약 (radius.full) | 세로 가운데 | 단행 중심 ChatGPT식 컴포저 |
boxed | 둥근 박스 (radius.xl) | 액션 하단 정렬 | 여러 줄로 자라는 컴포저 |
두 형태 모두 min-height를 control-height-lg로 고정해 단행 높이가 내용과 무관하게 일정하고(흔들림 제거), textarea는 내용에 맞춰 자동 높이로 자랍니다(약 7줄 상한, 초과분은 내부 스크롤 · 폭은 부모 100%).
상태 — 포커스는 셸 :focus-within(포커스 링), disabled는 입력과 슬롯 액션을 함께 차단합니다. 우측 주 액션은 입력 유무로 음성↔전송이 토글됩니다(아이콘/라벨/동작만 바뀌는 단일 안정 요소 — 포커스 보존, WCAG 2.4.3).
프리셋 — AiChatInput (단일 전송)#
전송 버튼 하나만 필요하면 ComposerBar를 직접 조립하지 않고 AiChatInput 프리셋을 씁니다 — onSend만 주면 됩니다. 내부에서 ComposerBar 셸을 사용하며, 단일 전송 버튼 + 스트리밍 중단 + 입력 상태 내부 관리(비제어)를 미리 엮었습니다. 여러 액션이 필요하면 위의 ComposerBar를 직접 쓰세요.
Enter 전송 · Shift+Enter 줄바꿈 · 한글 조합 중 Enter는 무시됩니다
스트리밍은 소비자가 소유하는 상태입니다 — 전송 후 응답 스트림이 시작되면 state="streaming"으로 바꾸고, 종료/중단 시 "idle"로 되돌립니다.
| AiChatInput 상태 | 트리거 | 표시 |
|---|---|---|
idle | 기본 | 전송 버튼(↑) — 빈 입력이면 비활성 |
streaming | 소비자가 prop으로 전환 | 중단 버튼(■) — onStop 호출 |
disabled | disabled prop | 입력·전송 모두 차단 |
AiChatInput의 variant는 ComposerBar 셸로 매핑됩니다 — pill→알약, expanded→박스+3줄 resting, (미지정)→박스.
AiChatInput Props#
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
onSend | (message: string) => void | — | 전송 콜백 — trim된 비어있지 않은 메시지만 전달한다. |
state | 'idle' | 'streaming' | idle | idle: 전송 가능 · streaming: 응답 생성 중(중단 버튼 노출) |
onStop | () => void | — | streaming 중단 콜백 |
sendLabel | string | 전송 | 전송 버튼 aria-label(기본 '전송'). |
stopLabel | string | 생성 중단 | 중단 버튼 aria-label(기본 '생성 중단'). |
variant | 'pill' | 'expanded' | — | 시각 변형 — pill: 둥근 소형 바 · expanded: 큰 작성 표면 · 미지정: 기본 박스 |
그 외 표준 textarea 속성(placeholder·disabled·maxLength 등)을 그대로 전달합니다 — value/onChange는 내부에서 관리합니다.
플레이그라운드#
AiChatInput를 직접 조작해 보세요
속성·테마·토큰을 바꾸고 React·Flutter 코드를 확인하는 풀스크린 빌더로 엽니다.
플레이그라운드는 넓은 작업 영역이 필요해 웹·태블릿에서 편집할 수 있어요.
API#
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
value | string | — | 제어 값 — 소비자가 상태를 소유한다(전송 후 비우기 등). |
onValueChange | (value: string) => void | — | 값 변경 콜백 — 제어형 계약. |
onSubmit | (value: string) => void | — | Enter 전송 콜백 — trim된 값을 전달(빈 값이면 호출하지 않음). Shift+Enter는 줄바꿈, 한글 IME 조합 확정 Enter는 무시한다. 값 비우기는 소비자가 onValueChange로 수행. |
leadingAction | ReactNode | — | 좌측 액션 슬롯 — 예: + 첨부(IconButton). |
trailingActions | ReactNode | — | 우측 액션 슬롯 — 예: 마이크·음성·전송(IconButton). 배열로 넘길 때는 각 노드에 key를 부여하거나 Fragment로 감싼다. |
variant | 'pill' | 'boxed' | pill | 셸 형태 — pill(기본) · boxed. |
minRows | number | — | resting 최소 줄 수 — textarea가 비어도 이 줄 수만큼 높이를 확보한다(긴 프롬프트 작성용). 미지정이면 단행. 자동 높이 상한(약 7줄)은 그대로 적용. (AiChatInput expanded 프리셋이 사용) |
aria-label | string | 메시지 입력 | textarea 접근성 이름 — placeholder는 신뢰할 수 있는 접근성 이름이 아니므로 별도 제공. 기본값 '메시지 입력'. |
textareaRef | Ref<HTMLTextAreaElement> | — | textarea로 전달되는 ref(React 19 ref-as-prop). |
그 외 표준 textarea 속성(id·name·placeholder·disabled·maxLength 등)을 그대로 전달합니다 — value/onChange/children 제외.
토큰#
component 토큰 없이 input.*과 semantic을 소비합니다(신설 기준 §4 미충족 — 셸은 input 어휘 재사용).
| 속성 | 토큰 |
|---|---|
| 배경/보더/포커스 | input.bg · input.border · input.border-focus |
| 높이 하한 | control-height-lg |
| 라운드 | radius.full(pill) · radius.xl(boxed) |
| 간격 | space-2(슬롯 gap) · space-1(패딩·액션 간격) |
접근성#
키보드 계약 — textarea 표준 동작 위에 구현합니다.
| 입력 | 결과 |
|---|---|
| Enter | onSubmit(value.trim()) — 빈 값이면 호출 안 함 |
| Shift+Enter | 줄바꿈 |
| 한글 IME 조합 중 Enter | 무시(isComposing) — 마지막 글자만 전송되는 버그 차단 |
- textarea에
aria-label기본 제공('메시지 입력') — placeholder는 접근성 이름으로 신뢰할 수 없음 - 슬롯 액션 버튼의
aria-label은 소비자 책임 —IconButton이aria-label을 필수로 강제합니다 - 포커스 링은 셸
:focus-within— Input·AiChatInput과 동일 어휘 - 상태 변화 안내는 소비자 책임: 전송 결과는
aria-live="polite"영역으로(위 데모가 기준 구현 — 입력 에코는 비-라이브 노드에)