Conversation
A chat / inbox message thread. Like
Kanban, it is fully
controlled and data-driven — you own the messages array and
append in your own send handler. Consecutive messages from the same author are
grouped (avatar & name shown once), day separators are inserted on date
change, and your own messages (authorId === currentUserId) align
to the right.
Basic
Pass a controlled value array and the currentUserId that
decides which messages are outgoing (rendered on the right). Enable the built-in
composer and append to your own state in onSend
(@send in Vue). Each message may carry a delivery
status (sending / sent /
delivered / read).
import { useState } from "react";
import { Conversation } from "@usevyre/react";
import type { ConversationMessage } from "@usevyre/react";
const [messages, setMessages] = useState<ConversationMessage[]>([
{ id: "1", authorId: "sam", authorName: "Sam Rivera", text: "Hey! Did you see the API draft?" },
{ id: "2", authorId: "me", text: "Yep — looks solid.", status: "read" },
{ id: "3", authorId: "sam", authorName: "Sam Rivera", text: "It's fully controlled, right?" },
{ id: "4", authorId: "me", text: "Exactly — same as Kanban. You own the array.", status: "delivered" },
]);
let nextId = 100;
// onSend receives (text, files). Basic chat ignores files —
// see the Attachments example for the full signature.
<Conversation
value={messages}
currentUserId="me"
composer
typing="Sam is typing"
onSend={(text) =>
setMessages((m) => [
...m,
{ id: String(++nextId), authorId: "me", text, status: "sent" },
])
}
/> <script setup>
import { ref } from "vue";
import { Conversation } from "@usevyre/vue";
const messages = ref([
{ id: "1", authorId: "sam", authorName: "Sam Rivera", text: "Hey! Did you see the API draft?" },
{ id: "2", authorId: "me", text: "Yep — looks solid.", status: "read" },
{ id: "3", authorId: "sam", authorName: "Sam Rivera", text: "It's fully controlled, right?" },
{ id: "4", authorId: "me", text: "Exactly — same as Kanban. You own the array.", status: "delivered" },
]);
// @send receives (text, files). Basic chat ignores files —
// see the Attachments example for the full signature.
let nextId = 100;
function onSend(text) {
messages.value.push({ id: String(++nextId), authorId: "me", text, status: "sent" });
}
</script>
<template>
<Conversation
:value="messages"
current-user-id="me"
composer
typing="Sam is typing"
@send="onSend"
/>
</template> Custom bubbles
Use renderMessage (React render prop) or the #message
scoped slot (Vue) to render any content inside a bubble — badges, rich text,
custom layouts. The meta argument tells you whether the message is
outgoing and where it sits in its author group.
import { useState } from "react";
import { Conversation, Badge } from "@usevyre/react";
import type { ConversationMessage } from "@usevyre/react";
const [messages, setMessages] = useState<ConversationMessage[]>([
{ id: "a", authorId: "bot", authorName: "Vyre Bot", text: "Pick a component to scaffold:" },
{ id: "b", authorId: "me", text: "Kanban please", status: "read" },
{ id: "c", authorId: "bot", authorName: "Vyre Bot", text: "Done — added Kanban.tsx + Kanban.vue." },
]);
<Conversation
value={messages}
currentUserId="me"
composer
placeholder="Ask the bot…"
onSend={(text) =>
setMessages((m) => [
...m,
{ id: String(Date.now()), authorId: "me", text, status: "sent" },
])
}
renderMessage={(msg, meta) => (
<span>
{!meta.outgoing && <Badge variant="teal">bot</Badge>} {msg.text}
</span>
)}
/> <script setup>
import { ref } from "vue";
import { Conversation, Badge } from "@usevyre/vue";
const messages = ref([
{ id: "a", authorId: "bot", authorName: "Vyre Bot", text: "Pick a component to scaffold:" },
{ id: "b", authorId: "me", text: "Kanban please", status: "read" },
{ id: "c", authorId: "bot", authorName: "Vyre Bot", text: "Done — added Kanban.tsx + Kanban.vue." },
]);
function onSend(text) {
messages.value.push({ id: String(Date.now()), authorId: "me", text, status: "sent" });
}
</script>
<template>
<Conversation
:value="messages"
current-user-id="me"
composer
placeholder="Ask the bot…"
@send="onSend"
>
<template #message="{ message, meta }">
<span>
<Badge v-if="!meta.outgoing" variant="teal">bot</Badge>
{{ message.text }}
</span>
</template>
</Conversation>
</template> Attachments
Add an attachments array to any message. Each attachment has a
kind of image, audio, video,
or file — rendered inside the bubble as an image preview, a native
audio/video player, or a download chip (with optional size).
To let users attach files from the composer, set
allowAttachments — a 📎 button and staged-file chips appear, and
onSend (@send in Vue) is called with
(text, files) where files is a File[].
You own the upload and turning each File into a message
attachment (use accept to restrict file types).
const [messages, setMessages] = useState<ConversationMessage[]>([
{
id: "1", authorId: "sam", authorName: "Sam Rivera",
text: "Here's the moodboard 👇",
attachments: [
{ kind: "image", url: "https://picsum.photos/seed/vyre/320/200", name: "moodboard.png" },
],
},
{
id: "2", authorId: "me", text: "Love it. Specs attached.", status: "read",
attachments: [
{ kind: "file", url: "#", name: "design-spec.pdf", size: "2.4 MB" },
],
},
{
id: "3", authorId: "sam", authorName: "Sam Rivera",
attachments: [
{ kind: "audio", url: "https://www.w3schools.com/html/horse.mp3" },
],
},
]);
<Conversation
value={messages}
currentUserId="me"
composer
allowAttachments
onSend={(text, files) =>
setMessages((m) => [
...m,
{
id: String(Date.now()),
authorId: "me",
text,
status: "sent",
attachments: files.map((f) =>
f.type.startsWith("image/")
? { kind: "image", url: URL.createObjectURL(f), name: f.name }
: { kind: "file", url: URL.createObjectURL(f), name: f.name }
),
},
])
}
/> <script setup>
import { ref } from "vue";
import { Conversation } from "@usevyre/vue";
const messages = ref([
{
id: "1", authorId: "sam", authorName: "Sam Rivera",
text: "Here's the moodboard 👇",
attachments: [
{ kind: "image", url: "https://picsum.photos/seed/vyre/320/200", name: "moodboard.png" },
],
},
{
id: "2", authorId: "me", text: "Love it. Specs attached.", status: "read",
attachments: [
{ kind: "file", url: "#", name: "design-spec.pdf", size: "2.4 MB" },
],
},
{
id: "3", authorId: "sam", authorName: "Sam Rivera",
attachments: [
{ kind: "audio", url: "https://www.w3schools.com/html/horse.mp3" },
],
},
]);
function onSend(text, files) {
messages.value.push({
id: String(Date.now()),
authorId: "me",
text,
status: "sent",
attachments: files.map((f) =>
f.type.startsWith("image/")
? { kind: "image", url: URL.createObjectURL(f), name: f.name }
: { kind: "file", url: URL.createObjectURL(f), name: f.name }
),
});
}
</script>
<template>
<Conversation
:value="messages"
current-user-id="me"
composer
allow-attachments
@send="onSend"
/>
</template> Props
Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | ConversationMessage[] | — | Ordered messages. ConversationMessage = id, authorId, text?, authorName?, authorAvatar?, timestamp?, status?, attachments?. Required & controlled. |
currentUserId | string | — | Whose messages are outgoing (aligned right). Matched against message.authorId. Required. |
composer | boolean | false | Show the built-in input + Send button. Pair with onSend / @send. |
onSend / @send | (text: string, files: File[]) => void | — | Called when the composer submits. files holds anything staged via the attach button (empty when allowAttachments is off). You own the upload. |
placeholder | string | "Write a message…" | Composer input placeholder. |
typing | boolean | string | false | Show an incoming typing indicator. Pass a string to label it. |
allowAttachments | boolean | false | Show a 📎 attach button + staged-file chips in the built-in composer. |
accept | string | — | Forwarded to the file input's accept attribute (e.g. "image/*"). |
renderMessage / #message | (message, meta) => ReactNode | — | Custom bubble body. meta = { outgoing, isGroupStart, isGroupEnd }. Vue: #message scoped slot. |
renderComposer / #composer | (api) => ReactNode | — | Replace the composer entirely. api = { value, setValue, send }. Vue: #composer scoped slot. |
className / class | string | — | Additional CSS class on the root element. |
ConversationAttachment
Props
| Prop | Type | Default | Description |
|---|---|---|---|
kind | "image" | "audio" | "video" | "file" | — | Renders an image preview, an audio/video player, or a download chip for files. |
url | string | — | Media source (image/audio/video) or download href (file). |
name | string | — | Image alt text / file display name. |
size | string | — | Optional human-readable file size, e.g. "2.4 MB" (file kind). |
Valid props
| Prop | Values | Default |
|---|---|---|
composer | true|false | false |
allowAttachments | true|false | false |
Common AI mistakes
- Conversation without currentUserId→ Always pass currentUserId matching one of the message authorId values
- Expecting Conversation to store/append messages→ Append to your own state in onSend (or @send) and pass it back via value
- composer without onSend (React) / @send (Vue)→ Provide onSend / @send to append the message to value
- Treating onSend as (text) only when using allowAttachments→ Handle onSend(text, files) — map files to message attachments and append
Quick examples
const [messages, setMessages] = useState([
{ id: "1", authorId: "sam", authorName: "Sam", text: "Hey!" },
{ id: "2", authorId: "me", text: "Hi \ud83d\udc4b", status: "read" },
]);
<Conversation
value={messages}
currentUserId="me"
composer
onSend={(t) => setMessages((m) => [...m, { id: crypto.randomUUID(), authorId: "me", text: t }])}
/><Conversation
value={messages}
currentUserId="me"
typing="Sam is typing"
renderMessage={(m) => <strong>{m.text}</strong>}
/>