Chat | CoUI
LogoCoUI

Chat

통일된 채팅 메시지 행 / 말풍선 / 그룹 컴포넌트.

Chat#

Chat / ChatGroup / ChatBubble 은 Flutter / Web 양쪽에서 동일한 API 로 채팅 인터페이스를 구성합니다.

Live Preview#

컴포넌트 구성#

  • Chat — 한 번의 메시지 한 줄. 슬롯: avatar / header / bubble / footer.
  • ChatGroup — 같은 발신자의 메시지 묶음. 슬롯: avatar / header / bubbles[] / footer.
  • ChatBubble — 말풍선 컨테이너. 슬롯: child. variant (8 종) 와 tail boolean 으로 색상 / 꼬리 제어.

기본 사용법 (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 하지 않습니다.
  • 발신자 구분이 정렬과 색에만 실려 있으므로, 누가 보낸 메시지인지는 텍스트로도 제공해야 합니다.

모션 감소 · 고대비 · 최소 터치 타깃처럼 모든 컴포넌트에 공통으로 적용되는 축은 전역 접근성 축 에 정리되어 있습니다.

관련 컴포넌트#

  • Avatar — 메시지 발신자 표시.
  • Loading — AI 응답 대기 인디케이터.
  • TextField — 입력 영역.