Chat
통일된 채팅 메시지 행 / 말풍선 / 그룹 컴포넌트.
목차
Chat / ChatGroup / ChatBubble 은 Flutter / Web 양쪽에서 동일한 API 로
채팅 인터페이스를 구성합니다.
기본 사용법 (Basic Usage)#
Chat(
avatar: Avatar(initials: 'S'),
header: Text('Support'),
bubble: ChatBubble(child: Text('Hello! How can I help?')),
footer: Text('10:00 AM'),
)
// 오른쪽(end)이면 primary 말풍선 · 꼬리 end 가 기본입니다.
Chat(
alignment: CoreChatAlignment.end,
bubble: ChatBubble(child: Text('I have a question.')),
footer: Text('10:01 AM'),
)
Chat(
avatar: Avatar(initials: 'S'),
header: Text('Support'),
bubble: ChatBubble(
child: Text('Hello! How can I help?'),
),
footer: Text('10:00 AM'),
)
// 오른쪽(end)이면 primary 말풍선 · 꼬리 end 가 기본입니다.
Chat(
alignment: CoreChatAlignment.end,
bubble: ChatBubble(child: Text('I have a question.')),
footer: Text('10:01 AM'),
)
빠른 오버라이드 (Chain)#
이미 만든 Chat 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
class ChatChainExample extends StatelessWidget {
const ChatChainExample({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Chat(
avatar: Avatar(initials: 'S'),
header: Text('Support'),
bubble: ChatBubble(
child: Text('Hello! How can I help?'),
),
footer: Text('10:00 AM'),
).withStyle(
const CoreChatStyle(
avatarSpacing: CoreSpace.space16,
groupSpacing: CoreSpace.space4,
headerTextStyle: CoreTextStyle.token(
CoreTextStyles.labelLarge,
color: CoreColor.token(CoreColors.primary),
),
footerTextStyle: CoreTextStyle.token(
CoreTextStyles.labelSmall,
color: CoreColor.token(CoreColors.onSurfaceVariant),
),
),
),
const Gap.space12(),
const Chat(
alignment: CoreChatAlignment.end,
bubble: ChatBubble(
child: Text('I have a question.'),
),
footer: Text('10:01 AM'),
),
],
);
}
}
class ChatChainExample extends StatelessComponent {
const ChatChainExample({super.key});
@override
Component build(BuildContext context) {
return div(
[
Chat(
avatar: Avatar(initials: 'S'),
header: const Text('Support'),
bubble: const ChatBubble(
child: Text('Hello! How can I help?'),
),
footer: const Text('10:00 AM'),
).withStyle(
const CoreChatStyle(
avatarSpacing: CoreSpace.space16,
groupSpacing: CoreSpace.space4,
headerTextStyle: CoreTextStyle.token(
CoreTextStyles.labelLarge,
color: CoreColor.token(CoreColors.primary),
),
footerTextStyle: CoreTextStyle.token(
CoreTextStyles.labelSmall,
color: CoreColor.token(CoreColors.onSurfaceVariant),
),
),
),
const Gap.space12(),
const Chat(
alignment: CoreChatAlignment.end,
bubble: ChatBubble(
child: Text('I have a question.'),
),
footer: Text('10:01 AM'),
),
],
classes: 'flex flex-col w-full',
);
}
}
Live Preview#
코드 보기
class ChatDefaultExample extends StatelessComponent {
const ChatDefaultExample({super.key});
@override
Component build(BuildContext context) {
return div(
[
Chat(
avatar: Avatar(initials: 'S'),
header: const Text('Support'),
bubble: const ChatBubble(
child: Text('Hello! How can I help?'),
),
footer: const Text('10:00 AM'),
),
const Gap.space12(),
const Chat(
alignment: CoreChatAlignment.end,
bubble: ChatBubble(
child: Text('I have a question.'),
),
footer: Text('10:01 AM'),
),
],
classes: 'flex flex-col w-full',
);
}
}
class ChatDefaultExample extends StatelessWidget {
const ChatDefaultExample({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const [
Chat(
avatar: Avatar(initials: 'S'),
header: Text('Support'),
bubble: ChatBubble(
child: Text('Hello! How can I help?'),
),
footer: Text('10:00 AM'),
),
Gap.space12(),
Chat(
alignment: CoreChatAlignment.end,
bubble: ChatBubble(
child: Text('I have a question.'),
),
footer: Text('10:01 AM'),
),
],
);
}
}
컴포넌트 구성#
-
Chat — 한 번의 메시지 한 줄. 슬롯:
avatar/header/bubble/footer. -
ChatGroup — 같은 발신자의 메시지 묶음. 슬롯:
avatar/header/bubbles[]/footer. -
ChatBubble — 말풍선 컨테이너. 슬롯:
child. variant (9 종) 와tailboolean 으로 색상 / 꼬리 제어. 꼬리가 놓이는 변(tailEdge, 기본 아래)과 꼬리 없는 말풍선의 꼬리 쪽 모서리(tailCornerRadius)는CoreChatBubbleStyle이 정합니다.
Chat 은 자기 alignment 를 안에 든 ChatBubble 에 넘기고, ChatBubble
은 alignment · variant · tail 을 적지 않으면 그 정렬이 정하는 기본값을 씁니다. 보낸 메시지(end)는
primary 말풍선에 꼬리가 끝 쪽으로, 받은 메시지(start)는 neutral 말풍선에 꼬리 없음입니다. 직접 적은 값은 그대로 지킵니다.
사용 시기 (When to Use)#
발신자별 정렬과 말풍선 색상(필요하면 꼬리)을 갖는 메시지 행이면 Chat / ChatBubble, 같은 발신자의 연속 메시지를 한 아바타로 묶으면
ChatGroup 을 씁니다. 말풍선 없이 일반 행·열 목록만 필요하면 Listing
입니다. 메시지 전송·구독 기능의 직접 대체 컴포넌트는 없습니다.
Props / Parameters#
W 는 Flutter의 Widget, Web의 Jaspr Component 입니다. W?
는 선택 슬롯, List<W> 는 콘텐츠 목록입니다.
Chat#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
bubble |
W |
필수 | 메시지 본체 (보통 ChatBubble). |
avatar |
W? |
null |
행 옆에 표시되는 아바타. |
header |
W? |
null |
버블 위 헤더 (발신자명 등). |
footer |
W? |
null |
버블 아래 풋터 (타임스탬프). |
alignment |
CoreChatAlignment |
start |
start
/
end
. 안에 든
bubble
에도 넘어가 말풍선의 기본
alignment
·
variant
·
tail
을 정합니다.
|
chatStyle |
CoreChatStyle? |
null |
gap / header & footer 텍스트 스타일 override. |
ChatBubble#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
child | W | 필수 | 메시지 내용. |
variant |
CoreChatBubbleVariant? |
null |
neutral / plain / primary / secondary / accent / info / success / warning / error.
null
이면 말풍선의 정렬이 정합니다 —
end
는
primary
,
start
는
neutral
.
plain
은 면이 없는 말(투명 면 ·
onSurfaceVariant
글 · 여백 0 · 폭 전체)입니다.
|
alignment |
CoreChatAlignment? |
null |
꼬리 쪽. 꼬리와 꼬리 쪽 모서리가 이쪽(시작 = 왼쪽, 끝 = 오른쪽)에 놓입니다.
null
이면 안에 든
Chat
행의 정렬을, 행 밖이면
start
를 씁니다.
|
tail |
bool? |
null |
꼬리(speech-bubble pointer) 표시.
null
이면 말풍선의 정렬이 정합니다 —
end
는 꼬리 있음,
start
는 꼬리 없음. 채움이 없는 말풍선(
plain
, 투명 override)은
true
여도 꼬리를 그리지 않습니다.
|
chatBubbleStyle |
CoreChatBubbleStyle? |
null |
padding / radius / 색상 / maxWidthFactor 등 override. 우선순위는
chatBubbleStyle
> 변형 기본값 > 공통 기본값입니다.
|
ChatGroup#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
bubbles | List<W> | 필수 | 같은 발신자의 연속 메시지들. |
avatar |
W? |
null |
그룹에 한 번만 표시되는 아바타. |
header | W? | null | 그룹 헤더. |
footer | W? | null | 그룹 풋터. |
alignment |
CoreChatAlignment |
start |
그룹 정렬. |
chatStyle |
CoreChatStyle? |
null |
gap / 텍스트 스타일 override. |
CoreChatStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
avatarSpacing |
double? |
Padding spacing between the avatar and the message column (logical px). Fed into the avatar wrapper's
EdgeInsetsDirectional
(Flutter) /
margin-inline-*
(Web) — not a
Gap
, so the value stays a raw double.
|
groupSpacing |
double? |
Inter-bubble spacing between consecutive bubbles inside a
ChatGroup
(logical px, pre-scaling). Forwarded directly to
Column.spacing
(Flutter) / flex
gap
CSS (Web) — both are native paint-API arguments that consume raw doubles, so this slot stays scalar (
double
).
|
headerTextStyle |
CoreTextStyle? |
Optional text style for the header slot (sender name). |
footerTextStyle |
CoreTextStyle? |
Optional text style for the footer slot (timestamp / status). |
CoreChatStyle 변형별 기본값 (CoreChatBubbleVariantStyle)#
| 필드 | neutral |
primary |
secondary |
accent |
info |
success |
warning |
error |
plain |
|---|---|---|---|---|---|---|---|---|---|
backgroundColor |
surfaceContainerHigh | primary | secondary | tertiary | info | success | warning | error | transparent |
foregroundColor |
onSurface | onPrimary | onSecondary | onTertiary | onInfo | onSuccess | onWarning | onError | onSurfaceVariant |
padding |
— | — | — | — | — | — | — | — | zero |
maxWidthFactor |
— | — | — | — | — | — | — | — | 1 |
동작 스펙 (Behavior)#
-
Chat은 header → bubble → footer를 세로로 쌓고 아바타는 alignment의 시작/끝 쪽에 한 번 배치합니다.ChatGroup은 같은 구조 안에bubbles를 주어진 순서로 쌓습니다. -
ChatBubble.tail은 말풍선 꼬리 표시만 제어합니다. 적지 않으면 정렬이 정합니다 —start는 꼬리 없음,end는 꼬리 있음.alignment는 꼬리 방향,variant는 색상,maxWidthFactor는 부모 너비 대비 최대 비율(기본 75%,plain은 100%)이며 꼬리가 없을 때도 양 플랫폼에서 똑같이 적용됩니다. -
말풍선이 쓰는 정렬은 직접 적은
alignment, 없으면 안에 든Chat행의alignment, 행 밖이면start입니다.variant·tail의 기본값은 이렇게 정해진 그 정렬에서 나옵니다 — 행은end인데 말풍선이alignment: start를 직접 적었다면 그 말풍선은start쪽 기본값(neutral· 꼬리 없음)을 씁니다. 행의header·footer·avatar칸에 든 말풍선은 행 안의 말풍선이 아니라서 정렬을 받지 않습니다. -
팝오버 · 대화상자처럼 행 밖으로 띄운 화면 안의 말풍선도 행의 정렬을 받지 않습니다.
ChatGroup도 정렬을 안의bubbles에 넘기지 않으므로, 그룹 안의 말풍선은alignment·variant·tail을 각자 적습니다. -
꼬리는 말풍선의 채움을 이어 그리는 모양이라 채움이 없으면 그릴 것이 없습니다.
plain이나 투명으로 덮어쓴 말풍선은tail: true여도 꼬리 없이 그립니다. -
사용자 말은
Chat(alignment: end)만으로primary말풍선이 되고, AI 답은plain+start로 짜면 보내는 쪽만 말풍선 면이 있고 받는 쪽은 면 없이 읽힙니다. -
꼬리는
CoreChatBubbleStyle.tailEdge가 정하는 변(bottom기본 ·top)의 정렬 쪽 모서리에서 나갑니다.top은 메시지가 시작하는 쪽, 행이 위에 맞추는 아바타 쪽을 가리킵니다. 꼬리를 그리면 그 모서리는 각지게 됩니다 — 꼬리의 바깥 변이 말풍선의 옆선을 이어야 하기 때문입니다. -
꼬리 없는 말풍선은
CoreChatBubbleStyle.tailCornerRadius로 꼬리 쪽 모서리 하나만 다르게 둘 수 있습니다(보낸 쪽 모서리만 덜 둥근 모양). 값이 없으면 그 모서리는borderRadius의 모서리 그대로라, 반경을 다시 정하면 네 모서리가 함께 움직입니다. 꼬리를 그리면 이 값과 상관없이 각집니다. 밀도 · 표면 스타일은 이 모서리를 나머지 셋과 같이 움직입니다. -
꼬리와 꼬리 쪽 모서리는 물리 방향(시작 = 왼쪽, 끝 = 오른쪽)입니다 —
CoreBorderRadius가 물리 네 모서리인 것과 같은 도달 범위라, 오른쪽에서 왼쪽으로 쓰는 페이지에서 뒤집히지 않습니다. - 모두 표시용 구성 요소이며 메시지 전송 · 수신 · 자동 스크롤 · 읽음 처리 · 콜백 · 비활성 상태를 제공하지 않습니다. 내용 갱신과 목록 컨테이너는 호출부가 관리합니다.
// 앱 전체: 보낸 쪽 위 모서리만 6 — 내 말풍선 · 상대 말풍선 모두 테마 하나로
CoreComponentTheme(
chatBubble: CoreChatBubbleTheme(
style: CoreChatBubbleStyle(
tailEdge: CoreChatBubbleTailEdge.top,
tailCornerRadius: CoreRadius.radius6,
),
),
)
사용 가이드라인 (Usage Guidelines)#
✅ Do#
사용자 말은 end(기본 primary), AI 답은 plain + start
Chat(
alignment: CoreChatAlignment.end,
bubble: ChatBubble(child: Text('Summarize this document.')),
)
Chat(
bubble: ChatBubble(
variant: CoreChatBubbleVariant.plain,
child: Text('Here is a short summary of the document.'),
),
)
보내는 쪽에만 말풍선 면을 주고 받는 쪽은 면 없는 글로 두면 긴 답이 화면 폭을 따라 편하게 읽힙니다. 보낸 쪽(end) 말풍선은 꼬리가 기본으로 켜지고, 받는 쪽(start)은 꺼져 있습니다 — 보낸 쪽 꼬리를 끄려면 ChatBubble(tail: false, ...), 받는 쪽에 꼬리를 켜려면 ChatBubble(tail: true, ...) 로 직접 적습니다.
정렬은 Chat 에만 적기
Chat(
alignment: CoreChatAlignment.end,
bubble: ChatBubble(child: Text('I have a question.')),
)
Chat.alignment 는 행 레이아웃(아바타 위치·정렬)을 정하고, 안에 든 ChatBubble 에도 넘어가 말풍선의 정렬(꼬리 방향까지)과 기본 variant · tail 을 정합니다. 그래서 같은 값을 말풍선에 또 적을 필요가 없습니다. 말풍선에 alignment · variant · tail 을 직접 적으면 그 값이 이깁니다 — 다만 행 밖에서 쓰는 말풍선은 행의 정렬을 받지 못하므로 ChatBubble(alignment: ...) 로 직접 적어야 합니다.
❌ Don't#
발신자 구분을 정렬/색상에만 맡기지 않기
// ❌ alignment(start/end)와 variant 색만으로 발신자를 구분
Chat(bubble: ChatBubble(child: Text(message)))
// ✅ header 슬롯에 발신자명을 텍스트로 함께 제공
Chat(
header: Text(senderName),
bubble: ChatBubble(child: Text(message)),
)
Chat / ChatBubble / ChatGroup은 role 이나 Semantics를 emit 하지 않는 순수 레이아웃 래퍼입니다. 발신자 구분이 정렬과 배경색이라는 시각적 신호로만 이루어지면 스크린 리더 사용자는 누가 보낸 메시지인지 알 수 없으므로, header 슬롯으로 텍스트 정보를 함께 줘야 합니다.
접근성 (Accessibility)#
역할#
Chat · ChatBubble · ChatGroup 세 클래스 모두 양 플랫폼에서 role 이나 Semantics
를 하나도 emit 하지 않습니다. Flutter 는 Column / Row / Padding
/ DefaultTextStyle(버블은 DecoratedBox / Stacks / CustomPaint) 만 조립하고, Web 은 세 클래스 모두 루트가
<div> 이며 caller attributes 만 통과시킵니다 — 네이티브 시맨틱 요소도 쓰지 않습니다.
즉 이들은 레이아웃 래퍼이며, 보조 기술이 받는 것은 avatar / header / bubble / footer
슬롯에 넣은 자식이 스스로 알리는 내용이 전부입니다. 대화라는 구조를 알리는 일은 소비자 몫입니다.
키보드#
양 플랫폼 모두 키를 처리하지 않습니다.
포커스 관리#
포커스 개념이 없습니다 — FocusNode · Focus · tabindex · 포커스 스타일이 어디에도 없습니다.
Chat / ChatGroup / ChatBubble 자체는 포커스를 받지 않으며, 포커스 가능한 것은 슬롯에 넣은 자식뿐입니다.
스크린 리더#
슬롯 내용을 위젯 / DOM 순서대로 이어진 평범한 텍스트로 읽습니다. 메시지라는 사실도, 발신자가 누구인지도, 어느 쪽에서 온 메시지인지도 알리지 않습니다 — 발신자 구분은 alignment(start/end 레이아웃) 와
CoreChatBubbleVariant 배경색, 즉 순수하게 시각적인 신호만으로 이루어집니다.
버블 꼬리는 장식이며 조용합니다(Flutter 는 CustomPaint, Web 은 빈 <div>) — 이건 의도한 동작입니다.
알려진 제약#
아래는 전부 양 플랫폼에 동일하게 존재합니다.
-
role="log"/aria-live가 없어 진행 중인 대화에 도착한 새 메시지는 안내되지 않습니다. 실시간 대화라면 메시지 목록을 감싸는 live region 을 소비자가 직접 만들어야 합니다. - list / listitem 구조가 없어
ChatGroup의 연속 버블은 서로 구분되지 않는 형제로 읽힙니다. -
header(발신자) 와footer(타임스탬프) 는 해당 버블과aria-labelledby/Semantics로 연결되어 있지 않은 별개 텍스트입니다 — 읽히기는 하지만 어느 메시지에 속하는지는 전달되지 않습니다. - Web
ChatBubble은<article>도, role 도, 접근 가능한 이름도 emit 하지 않습니다. - 발신자 구분이 정렬과 색에만 실려 있으므로, 누가 보낸 메시지인지는 텍스트로도 제공해야 합니다.
모션 감소 · 고대비 · 최소 터치 타깃처럼 모든 컴포넌트에 공통으로 적용되는 축은 전역 접근성 축 에 정리되어 있습니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 행·그룹 | Row / Column.spacing | flex 행·열과 CSS gap |
| 꼬리 | CustomPaint 경로(위쪽 꼬리는 세로로 뒤집은 경로) |
CSS 꼬리 엘리먼트 — 같은 경로의 clip-path, 위쪽 꼬리는 top: 0 |
| 최대 너비 | LayoutBuilder 의 부모 제약 × factor | CSS 최대 너비 백분율 |
| 의미·이벤트 | 자식 의미가 통과 | 루트 attributes / 이벤트를 호출부에서 제공 가능 |