Chat#
Chat / ChatGroup / ChatBubble 은 Flutter / Web 양쪽에서 동일한 API 로
채팅 인터페이스를 구성합니다.
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(
alignment: CoreChatAlignment.end,
variant: CoreChatBubbleVariant.primary,
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(
alignment: CoreChatAlignment.end,
variant: CoreChatBubbleVariant.primary,
child: Text('I have a question.'),
),
footer: Text('10:01 AM'),
),
],
);
}
}
class ChatNeutralExample extends StatelessComponent {
const ChatNeutralExample({super.key});
@override
Component build(BuildContext context) {
return Chat(
bubble: ChatBubble(
variant: CoreChatBubbleVariant.neutral,
child: Text('Neutral bubble'),
),
);
}
}
class ChatNeutralExample extends StatelessWidget {
const ChatNeutralExample({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const [
Chat(
bubble: ChatBubble(
variant: CoreChatBubbleVariant.neutral,
child: Text('Neutral bubble'),
),
),
],
);
}
}
class ChatPrimaryExample extends StatelessComponent {
const ChatPrimaryExample({super.key});
@override
Component build(BuildContext context) {
return Chat(
bubble: ChatBubble(
variant: CoreChatBubbleVariant.primary,
child: Text('Primary bubble'),
),
);
}
}
class ChatPrimaryExample extends StatelessWidget {
const ChatPrimaryExample({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const [
Chat(
bubble: ChatBubble(
variant: CoreChatBubbleVariant.primary,
child: Text('Primary bubble'),
),
),
],
);
}
}
class ChatSecondaryExample extends StatelessComponent {
const ChatSecondaryExample({super.key});
@override
Component build(BuildContext context) {
return Chat(
bubble: ChatBubble(
variant: CoreChatBubbleVariant.secondary,
child: Text('Secondary bubble'),
),
);
}
}
class ChatSecondaryExample extends StatelessWidget {
const ChatSecondaryExample({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const [
Chat(
bubble: ChatBubble(
variant: CoreChatBubbleVariant.secondary,
child: Text('Secondary bubble'),
),
),
],
);
}
}
class ChatAccentExample extends StatelessComponent {
const ChatAccentExample({super.key});
@override
Component build(BuildContext context) {
return Chat(
bubble: ChatBubble(
variant: CoreChatBubbleVariant.accent,
child: Text('Accent bubble'),
),
);
}
}
class ChatAccentExample extends StatelessWidget {
const ChatAccentExample({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const [
Chat(
bubble: ChatBubble(
variant: CoreChatBubbleVariant.accent,
child: Text('Accent bubble'),
),
),
],
);
}
}
class ChatInfoExample extends StatelessComponent {
const ChatInfoExample({super.key});
@override
Component build(BuildContext context) {
return Chat(
bubble: ChatBubble(
variant: CoreChatBubbleVariant.info,
child: Text('Info bubble'),
),
);
}
}
class ChatInfoExample extends StatelessWidget {
const ChatInfoExample({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const [
Chat(
bubble: ChatBubble(
variant: CoreChatBubbleVariant.info,
child: Text('Info bubble'),
),
),
],
);
}
}
class ChatSuccessExample extends StatelessComponent {
const ChatSuccessExample({super.key});
@override
Component build(BuildContext context) {
return Chat(
bubble: ChatBubble(
variant: CoreChatBubbleVariant.success,
child: Text('Success bubble'),
),
);
}
}
class ChatSuccessExample extends StatelessWidget {
const ChatSuccessExample({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const [
Chat(
bubble: ChatBubble(
variant: CoreChatBubbleVariant.success,
child: Text('Success bubble'),
),
),
],
);
}
}
class ChatWarningExample extends StatelessComponent {
const ChatWarningExample({super.key});
@override
Component build(BuildContext context) {
return Chat(
bubble: ChatBubble(
variant: CoreChatBubbleVariant.warning,
child: Text('Warning bubble'),
),
);
}
}
class ChatWarningExample extends StatelessWidget {
const ChatWarningExample({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const [
Chat(
bubble: ChatBubble(
variant: CoreChatBubbleVariant.warning,
child: Text('Warning bubble'),
),
),
],
);
}
}
class ChatErrorExample extends StatelessComponent {
const ChatErrorExample({super.key});
@override
Component build(BuildContext context) {
return Chat(
bubble: ChatBubble(
variant: CoreChatBubbleVariant.error,
child: Text('Error bubble'),
),
);
}
}
class ChatErrorExample extends StatelessWidget {
const ChatErrorExample({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const [
Chat(
bubble: ChatBubble(
variant: CoreChatBubbleVariant.error,
child: Text('Error bubble'),
),
),
],
);
}
}
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(
alignment: CoreChatAlignment.end,
variant: CoreChatBubbleVariant.primary,
child: Text('I have a question.'),
),
footer: Text('10:01 AM'),
),
],
classes: 'flex flex-col w-full',
);
}
}
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(
alignment: CoreChatAlignment.end,
variant: CoreChatBubbleVariant.primary,
child: Text('I have a question.'),
),
footer: Text('10:01 AM'),
),
],
);
}
}
컴포넌트 구성#
-
Chat — 한 번의 메시지 한 줄. 슬롯:
avatar/header/bubble/footer. -
ChatGroup — 같은 발신자의 메시지 묶음. 슬롯:
avatar/header/bubbles[]/footer. -
ChatBubble — 말풍선 컨테이너. 슬롯:
child. variant (8 종) 와tailboolean 으로 색상 / 꼬리 제어.
기본 사용법 (Basic Usage)#
Chat(
avatar: Avatar(initials: 'S'),
header: Text('Support'),
bubble: ChatBubble(child: Text('Hello! How can I help?')),
footer: Text('10:00 AM'),
)
Chat(
alignment: CoreChatAlignment.end,
bubble: ChatBubble(
alignment: CoreChatAlignment.end,
variant: CoreChatBubbleVariant.primary,
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'),
)
Chat(
alignment: CoreChatAlignment.end,
bubble: ChatBubble(
alignment: CoreChatAlignment.end,
variant: CoreChatBubbleVariant.primary,
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(
alignment: CoreChatAlignment.end,
variant: CoreChatBubbleVariant.primary,
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(
alignment: CoreChatAlignment.end,
variant: CoreChatBubbleVariant.primary,
child: Text('I have a question.'),
),
footer: Text('10:01 AM'),
),
],
classes: 'flex flex-col w-full',
);
}
}
Props#
Chat#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
bubble |
Widget / Component |
필수 | 메시지 본체 (보통 ChatBubble). |
avatar |
Widget? / Component? |
null |
행 옆에 표시되는 아바타. |
header |
Widget? / Component? |
null |
버블 위 헤더 (발신자명 등). |
footer |
Widget? / Component? |
null |
버블 아래 풋터 (타임스탬프). |
alignment |
CoreChatAlignment |
start |
start / end. |
chatStyle |
CoreChatStyle? |
null |
gap / header & footer 텍스트 스타일 override. |
ChatBubble#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
child |
Widget / Component |
필수 | 메시지 내용. |
variant |
CoreChatBubbleVariant |
neutral |
neutral / primary / secondary / accent / info / success / warning / error. |
alignment |
CoreChatAlignment |
start |
tail 방향. |
tail |
bool |
true |
꼬리(speech-bubble pointer) 표시. |
chatBubbleStyle |
CoreChatBubbleStyle? |
null |
padding / radius / 색상 / maxWidthFactor 등 override. |
ChatGroup#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
bubbles |
List<Widget> / List<Component> |
필수 | 같은 발신자의 연속 메시지들. |
avatar |
Widget? / Component? |
null |
그룹에 한 번만 표시되는 아바타. |
header |
Widget? / Component? |
null |
그룹 헤더. |
footer |
Widget? / Component? |
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 |
|---|---|---|---|---|---|---|---|---|
backgroundColor |
surfaceContainer | primary | secondary | tertiary | info | success | warning | error |
foregroundColor |
onSurface | onPrimary | onSecondary | onTertiary | onInfo | onSuccess | onWarning | onError |
사용 가이드라인 (Usage Guidelines)#
✅ Do#
Chat/ChatGroup과 내부 ChatBubble에 같은 alignment 지정
Chat(
alignment: CoreChatAlignment.end,
bubble: ChatBubble(
alignment: CoreChatAlignment.end,
variant: CoreChatBubbleVariant.primary,
child: Text('I have a question.'),
),
)
Chat.alignment은 행 레이아웃(아바타 위치·정렬)만 제어하고, bubble은 그대로 전달된 자식 위젯이라 꼬리 방향은 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)#
역할 (Semantics)#
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 하지 않습니다. - 발신자 구분이 정렬과 색에만 실려 있으므로, 누가 보낸 메시지인지는 텍스트로도 제공해야 합니다.
모션 감소 · 고대비 · 최소 터치 타깃처럼 모든 컴포넌트에 공통으로 적용되는 축은 전역 접근성 축 에 정리되어 있습니다.