HapplyUI

Components

Chat

A composable chat interface with a scrollable, auto-scrolling message thread and a configurable message input. Composes the Avatar and Button components. Bring your own message data; override any part as needed.


Installation

bunx @happlyui/cli@latest add chat

Dependencies

npm packages

  • @remixicon/react

Registry dependencies

These are automatically installed when you add this component.

  • tv
  • happly-ui-utils
  • avatar
  • button

Usage

import * as Chat from "@/components/ui/chat"

<Chat.Root>
  <Chat.List>
    <Chat.Message side="received" avatar={<Chat.Avatar src="/avatar.png" />}>
      <Chat.Bubble>
        Hello there!
        <Chat.Timestamp>3:00 PM</Chat.Timestamp>
      </Chat.Bubble>
    </Chat.Message>
  </Chat.List>
  <Chat.Input onSend={(text) => send(text)} />
</Chat.Root>

Examples

Default

A full thread with received and sent bubbles, short pills, and date / new-message dividers. Set a height on a parent so the list can scroll.

Join us for an engaging seminar where we delve into user-centered design principles and their vital role in product development. Participants will collaborate on hands-on activities to identify user needs, craft detailed personas, and build effective prototypes.3:00 PM
Explore the fundamentals of user-centered design in our upcoming seminar. Attendees will participate in interactive sessions aimed at uncovering user needs, developing personas, and creating prototypes.3:00 PM
This seminar will cover the essentials of user-centered design and its impact on product development. Let's build products that truly resonate with users!3:00 PM
23 May, 2025
Hey!3:00 PM
How are you?3:00 PM
New message
Dive into the principles of user-centered design at our seminar. Together, we can create products that genuinely resonate with users!3:00 PM
<div className="h-[600px] w-full max-w-2xl">
  <Chat.Root>
    <Chat.List>
      <Chat.Message side="received" avatar={avatar}>
        <Chat.Bubble>
          Join us for an engaging seminar…
          <Chat.Timestamp>3:00 PM</Chat.Timestamp>
        </Chat.Bubble>
      </Chat.Message>
      <Chat.Message side="sent">
        <Chat.Bubble>
          Explore the fundamentals…
          <Chat.Timestamp>3:00 PM</Chat.Timestamp>
        </Chat.Bubble>
      </Chat.Message>
      <Chat.Divider>23 May, 2025</Chat.Divider>
      <Chat.Message side="received">
        <Chat.Bubble shape="pill">
          Hey!
          <Chat.Timestamp>3:00 PM</Chat.Timestamp>
        </Chat.Bubble>
      </Chat.Message>
      <Chat.Divider variant="feature">New message</Chat.Divider>
    </Chat.List>
    <Chat.Input onSend={(text) => send(text)} />
  </Chat.Root>
</div>

Bubbles

Sent, received, and highlighted bubble tones. The tone defaults from the parent Message's side and can be overridden.

A received message in the soft gray tone.3:00 PM
A sent message in the primary tone.3:00 PM
A highlighted "new" message.3:00 PM
<Chat.Message side="received" avatar={avatar}>
  <Chat.Bubble>
    A received message.
    <Chat.Timestamp>3:00 PM</Chat.Timestamp>
  </Chat.Bubble>
</Chat.Message>
<Chat.Message side="sent" avatar={avatar}>
  <Chat.Bubble>
    A sent message.
    <Chat.Timestamp>3:00 PM</Chat.Timestamp>
  </Chat.Bubble>
</Chat.Message>
<Chat.Message side="received">
  <Chat.Bubble tone="highlight">
    A highlighted message.
    <Chat.Timestamp>3:00 PM</Chat.Timestamp>
  </Chat.Bubble>
</Chat.Message>

Pills and Dividers

Short pill-shaped messages and both divider variants (date and highlighted new-message).

Hey!3:00 PM
23 May, 2025
How are you?3:00 PM
New message
<Chat.Message side="received">
  <Chat.Bubble shape="pill">
    Hey!
    <Chat.Timestamp>3:00 PM</Chat.Timestamp>
  </Chat.Bubble>
</Chat.Message>
<Chat.Divider>23 May, 2025</Chat.Divider>
<Chat.Divider variant="feature">New message</Chat.Divider>

Input

The default input: attachment + emoji buttons, an auto-growing field, and a send button. Uncontrolled by default; Enter sends, Shift+Enter inserts a newline.

<Chat.Input onSend={(text) => console.log(text)} />

Interactive

A live thread that appends messages on send. The list sticks to the bottom as new messages arrive.

Hey! How can I help you today?Now
const [messages, setMessages] = React.useState([])

<Chat.Root>
  <Chat.List>
    {messages.map((m) => (
      <Chat.Message key={m.id} side={m.side} avatar={m.avatar}>
        <Chat.Bubble>
          {m.text}
          <Chat.Timestamp>Now</Chat.Timestamp>
        </Chat.Bubble>
      </Chat.Message>
    ))}
  </Chat.List>
  <Chat.Input onSend={(text) => setMessages((p) => [...p, { id: p.length + 1, side: 'sent', text }])} />
</Chat.Root>

Custom Input

Override the default input clusters with the leading / actions slots. Chat.SendButton forwards Button props, so restyling is just variant/mode.

<Chat.Input
  placeholder="Ask anything…"
  showAttachment={false}
  actions={<Chat.SendButton variant="primary" mode="filled" />}
  onSend={(text) => send(text)}
/>

Disabled

Disabled input with muted styling.

<Chat.Input disabled placeholder="You cannot type here…" />

API Reference

Chat.Root

Vertical layout container. Fills its parent and pins the input 20px below the list. Set a height on it (or its parent) so the list can scroll.

PropTypeDefaultDescription

Chat.List

Scrollable message container. Auto-scrolls to the newest message when the user is near the bottom.

PropTypeDefaultDescription
autoScrollbooleantrueStick to the bottom as new messages arrive (unless the user has scrolled up)

Chat.Message

A single message row. Reserves a 44px gutter on both sides so avatar-less rows stay aligned with rows that have an avatar.

PropTypeDefaultDescription
side'sent' | 'received'-Which side the message is on. Drives alignment and the default bubble tone.
avatarReact.ReactNode-Optional avatar rendered in the gutter on the message's side (e.g. <Chat.Avatar src="…" />)

Chat.Avatar

Thin wrapper around the Avatar component, sized for chat rows (32px). Pass `src` for the common case, or compose Avatar parts as children. Forwards all Avatar.Root props (size, color, etc.).

PropTypeDefaultDescription
srcstring-Image source. Renders an Avatar.Image when provided.
sizestring'32'Avatar size token (forwarded to Avatar.Root)

Chat.Bubble

The message bubble. Reads side from the parent Message to pick a default tone.

PropTypeDefaultDescription
shape'bubble' | 'pill''bubble'Rounded card ('bubble') or content-hugging pill ('pill') for short messages
tone'received' | 'highlight' | 'sent'-Color tone. Defaults from the parent Message's side (sent → sent, otherwise received).

Chat.Timestamp

Small caption inside a bubble. Adapts its color and position to the surrounding bubble.

PropTypeDefaultDescription

Chat.Divider

A centered label between two rules, e.g. a date or a new-message marker.

PropTypeDefaultDescription
variant'default' | 'feature''default'Neutral date label ('default') or highlighted primary label ('feature')

Chat.Input

Batteries-included composer. Uncontrolled by default; pass value/onChange to control it. Enter sends, Shift+Enter inserts a newline.

PropTypeDefaultDescription
onSend(value: string) => void-Fired when the user sends (Enter or the send button). Receives the trimmed text.
valuestring-Controlled value. Omit for uncontrolled behavior.
onChange(value: string) => void-Fired on every keystroke
placeholderstring'Write a message...'Placeholder text
onAttach() => void-Click handler for the default attachment button
onEmoji() => void-Click handler for the default emoji button
showAttachmentbooleantrueShow the attachment button
showEmojibooleantrueShow the emoji button
leadingReact.ReactNode-Replace the left (attachment) cluster entirely
actionsReact.ReactNode-Replace the right (emoji + send) cluster entirely
sendDisabledboolean-Disable sending even when there is text
maxHeightnumber128Max auto-grow height in px before the field scrolls
containerClassNamestring-Additional classes for the input container

Chat.SendButton

The send button, built on the Button component (neutral / lighter by default). Renders an arrow-up icon unless given children, and forwards all Button.Root props (variant, mode, loading, etc.).

PropTypeDefaultDescription

Previous
Timeline Status Badge