Chat interface components for PhiaUI.
Provides a complete AI/human chat UI: a scrollable message log, individual message rows aligned by role, styled content bubbles with optional avatars and feedback buttons, clickable suggestion chips, and a compose form.
CSS-only layout — no JS hook required. Fully compatible with LiveView streams for real-time message delivery, including streaming AI token output.
When to use
- AI assistant chat (GPT-style, Anthropic Claude, etc.)
- Customer support live chat embedded in an app
- Team messaging feed within a product
Anatomy
| Component | Element | Purpose |
|---|---|---|
chat_container/1 | div | Scrollable log (role="log", live region) |
chat_message/1 | div | A message row, aligned by :role |
chat_bubble/1 | div | Styled content balloon with optional avatar |
chat_suggestions/1 | div | Row of suggestion chips below an AI message |
chat_input/1 | form | Bottom compose form with phx-submit |
Role alignment
| Role | Alignment | Background |
|---|---|---|
user | Right | bg-primary |
assistant | Left | bg-muted |
system | Center | transparent, italic |
Minimal chat example
<.chat_container id="ai-chat" class="h-96 p-4">
<.chat_message role="assistant">
<.chat_bubble role="assistant" timestamp="2:30 PM">
Welcome! How can I help you today?
</.chat_bubble>
<.chat_suggestions
suggestions={["Show me an example", "What can you do?"]}
on_select="select_suggestion"
/>
</.chat_message>
<.chat_message role="user">
<.chat_bubble role="user" timestamp="2:31 PM">
Show me an example
</.chat_bubble>
</.chat_message>
</.chat_container>
<.chat_input id="chat-compose" on_submit="send_message" placeholder="Ask anything..." />Full AI assistant with LiveView streams
Using streams means only new messages are DOM-patched; the full message list is never re-rendered, which is critical for long conversations.
# LiveView mount/3 — initialise the stream
def mount(_params, _session, socket) do
{:ok, stream(socket, :messages, initial_messages())}
end
# Push a new AI message (e.g. from a Task or GenServer)
def handle_info({:ai_message, msg}, socket) do
{:noreply, stream_insert(socket, :messages, msg)}
end
# Template
<.chat_container id="ai-chat" class="flex-1 overflow-y-auto p-4">
<.chat_message
:for={{dom_id, msg} <- @streams.messages}
id={dom_id}
role={msg.role}
>
<.chat_bubble
role={msg.role}
timestamp={Calendar.strftime(msg.inserted_at, "%I:%M %p")}
on_feedback={if msg.role == "assistant", do: "rate_message"}
message_id={msg.id}
>
<:avatar :if={msg.role == "assistant"}>
<.avatar size="sm">
<.avatar_fallback name="AI" />
</.avatar>
</:avatar>
{msg.content}
</.chat_bubble>
</.chat_message>
</.chat_container>
<.chat_input
id="chat-compose"
on_submit="send_message"
placeholder="Type a message..."
max_chars={2000}
/>Streaming AI token output
For token-by-token streaming responses, stream_insert/4 with at: -1
updates the last message in-place via LiveView's morphdom patching:
def handle_info({:token, token}, socket) do
# Append the token to the last AI message in your stream
{:noreply, stream_insert(socket, :messages, updated_message, at: -1)}
end
Summary
Functions
Renders the styled chat content balloon.
Renders the chat log scroll container.
Renders the chat compose form.
Renders a message row with role-based alignment.
Renders a row of clickable conversation suggestion chips.
Functions
Renders the styled chat content balloon.
role="user"—bg-primary text-primary-foreground(right-aligned)role="assistant"—bg-muted text-foreground(left-aligned)role="system"— transparent, italic, centered small text
Supply an :avatar slot to show a user or AI avatar beside the bubble.
Set on_feedback + message_id to enable thumbs up/down rating buttons
for assistant responses (common in AI assistant UIs for RLHF collection).
Example — assistant bubble with avatar and feedback
<.chat_bubble
role="assistant"
timestamp="2:34 PM"
on_feedback="rate_message"
message_id={msg.id}
>
<:avatar>
<.avatar size="sm"><.avatar_fallback name="AI" /></.avatar>
</:avatar>
The capital of France is Paris.
</.chat_bubble>Attributes
role(:string) (required) - Bubble role — controls background colour and text colour. Must be one of"user","assistant", or"system".timestamp(:string) - Optional time label rendered beneath the bubble, e.g. "2:34 PM". Defaults tonil.on_feedback(:string) -phx-clickevent name for thumbs up/down feedback buttons. Only rendered forrole="assistant"bubbles. Pair with:message_idto identify which message received feedback.Defaults to
nil.message_id(:string) - ID passed asphx-value-message-idto feedback buttons. The LiveView receives%{"message_id" => id, "feedback" => "up" | "down"}.Defaults to
nil.class(:string) - Additional CSS classes for the bubble wrapper. Defaults tonil.Global attributes are accepted. HTML attributes forwarded to the bubble wrapper div.
Slots
avatar- Optionalavatar/1component displayed beside the bubble. Forrole="assistant"the avatar appears on the left; forrole="user"it appears on the right (matching message alignment).inner_block(required) - Bubble text content. May include inline HTML or Markdown-rendered content. For streaming AI output, update this viastream_insert/4.
Renders the chat log scroll container.
Sets role="log" and aria-live="polite" so screen readers announce new
messages as they arrive via LiveView streams without interrupting the user's
current focus or reading flow.
Apply a fixed height and overflow-y-auto via :class to make the log
scrollable within a fixed layout region:
<.chat_container id="chat" class="h-[600px] overflow-y-auto p-4">
...
</.chat_container>Attributes
id(:string) - DOM id for the container. Recommended when using LiveView streams so the engine can locate and patch individual messages efficiently.Defaults to
nil.class(:string) - Additional CSS classes for the scroll container (e.g.h-96 overflow-y-auto). Defaults tonil.Global attributes are accepted. HTML attributes forwarded to the root div.
Slots
inner_block(required) -chat_message/1children.
Renders the chat compose form.
The form fires on_submit via phx-submit. A max_chars counter is
displayed when provided. Attachment chips (e.g. file uploads) can be rendered
above the textarea via the :attachments slot.
Example
<.chat_input
id="chat-compose"
on_submit="send_message"
placeholder="Ask me anything..."
max_chars={4000}
phx-change="typing"
>
<:attachments>
<.badge :for={file <- @pending_attachments} variant="outline">
<.icon name="paperclip" size={:xs} />
{file.name}
</.badge>
</:attachments>
</.chat_input>Attributes
id(:string) (required) - DOM id for the form element.on_submit(:string) (required) -phx-submitevent name. The LiveView receives%{"message" => text}. After handling, reset the textarea withPhoenix.LiveView.push_event/3or by clearing the assign that controls the input value.placeholder(:string) - Textarea placeholder text. Defaults to"Type a message...".max_chars(:integer) - Optional character limit displayed as a static counter below the textarea. Note: actual enforcement must be done server-side in the LiveView handler.Defaults to
nil.class(:string) - Additional CSS classes for the form wrapper. Defaults tonil.Global attributes are accepted. HTML attributes forwarded to the form element. Supports all globals plus:
["phx-change"].
Slots
attachments- Optional attachment chips rendered above the textarea. Usebadge/1or custom chips to show files the user has attached.
Renders a message row with role-based alignment.
Alignment is determined by :role:
user→items-end(right side, mirroring human side of a conversation)assistant→items-start(left side, mirroring AI/agent side)system→items-center(centered, for notices like "Session started")
Attributes
role(:string) (required) - Message role — controls horizontal alignment:user→ right-aligned (items-end)assistant→ left-aligned (items-start)system→ centered (items-center) Must be one of"user","assistant", or"system".
id(:string) - DOM id — required when using LiveView streams (passdom_idfrom stream tuple). Defaults tonil.class(:string) - Additional CSS classes for the message wrapper. Defaults tonil.- Global attributes are accepted. HTML attributes forwarded to the message wrapper div.
Slots
inner_block(required) -chat_bubble/1and/orchat_suggestions/1children.
Renders a row of clickable conversation suggestion chips.
Each chip fires on_select with phx-value-suggestion set to the chip text.
Render these below an assistant bubble to help users discover common queries.
Example
<.chat_suggestions
suggestions={["Tell me more", "Show an example", "How does billing work?"]}
on_select="select_suggestion"
/>
# LiveView handler
def handle_event("select_suggestion", %{"suggestion" => text}, socket) do
# Treat the suggestion as if the user typed and sent it
{:noreply, send_user_message(socket, text)}
endAttributes
suggestions(:list) (required) - List of suggestion strings shown as clickable pill buttons below an assistant bubble. Typically 2–4 concise prompts that guide the user.on_select(:string) (required) -phx-clickevent emitted when a suggestion is selected. The LiveView receives%{"suggestion" => text}.class(:string) - Additional CSS classes for the suggestions row. Defaults tonil.Global attributes are accepted. HTML attributes forwarded to the wrapper div.