Message

A comprehensive suite of components for displaying chat messages, including message rendering, branching, actions, and markdown responses.

The Message component suite provides a complete set of tools for building chat interfaces. It includes components for displaying messages from users and AI assistants, managing multiple response branches, adding action buttons, and rendering markdown content.

Install using CLI

AI Elements Vue
shadcn-vue CLI
npx ai-elements-vue@latest add message

Install Manually

Copy and paste the following code in the same folder.

Message.vue
MessageContent.vue
MessageActions.vue
MessageAction.vue
MessageBranch.vue
MessageBranchContent.vue
MessageBranchSelector.vue
MessageBranchPrevious.vue
MessageBranchNext.vue
MessageBranchPage.vue
MessageResponse.vue
MessageAttachments.vue
MessageAttachment.vue
MessageToolbar.vue
context.ts
index.ts
<script setup lang="ts">
import type { UIMessage } from 'ai'
import type { HTMLAttributes } from 'vue'
import { cn } from '@repo/shadcn-vue/lib/utils'

interface Props {
  from: UIMessage['role']
  class?: HTMLAttributes['class']
}

const props = defineProps<Props>()
</script>

<template>
  <div
    :class="
      cn(
        'group flex w-full max-w-[80%] gap-2',
        props.from === 'user' ? 'is-user ml-auto justify-end' : 'is-assistant',
        props.class,
      )
    "
    v-bind="$attrs"
  >
    <slot />
  </div>
</template>

Usage with AI SDK

Build a simple chat UI where the user can copy or regenerate the most recent message.

Add the following component to your frontend:

pages/index.vue
<script setup lang="ts">
import { useChat } from '@ai-sdk/vue'
import { CopyIcon, RefreshCcwIcon } from 'lucide-vue-next'
import { ref } from 'vue'
import {
  Conversation,
  ConversationContent,
  ConversationScrollButton,
} from '@/components/ai-elements/conversation'
import {
  Message,
  MessageAction,
  MessageActions,
  MessageContent,
  MessageResponse,
} from '@/components/ai-elements/message'
import {
  PromptInput,
  PromptInputSubmit,
  PromptInputTextarea,
} from '@/components/ai-elements/prompt-input'

const input = ref('')

const { messages, sendMessage, status, regenerate } = useChat()

function handleSubmit() {
  if (input.value.trim()) {
    sendMessage({ text: input.value })
    input.value = ''
  }
}
</script>

<template>
  <div
    class="max-w-4xl mx-auto p-6 relative size-full rounded-lg border h-[600px]"
  >
    <div class="flex flex-col h-full">
      <Conversation>
        <ConversationContent>
          <template
            v-for="(message, messageIndex) in messages"
            :key="message.id"
          >
            <template v-for="(part, i) in message.parts" :key="`${message.id}-${i}`">
              <template v-if="part.type === 'text'">
                <Message :from="message.role">
                  <MessageContent>
                    <MessageResponse :content="part.text" />
                  </MessageContent>
                </Message>

                <MessageActions
                  v-if="
                    message.role === 'assistant'
                      && messageIndex === messages.length - 1
                  "
                >
                  <MessageAction label="Retry" @click="regenerate()">
                    <RefreshCcwIcon class="size-3" />
                  </MessageAction>
                  <MessageAction
                    label="Copy"
                    @click="navigator.clipboard.writeText(part.text)"
                  >
                    <CopyIcon class="size-3" />
                  </MessageAction>
                </MessageActions>
              </template>
            </template>
          </template>
        </ConversationContent>
        <ConversationScrollButton />
      </Conversation>

      <PromptInput
        class="mt-4 w-full max-w-2xl mx-auto relative"
        @submit.prevent="handleSubmit"
      >
        <PromptInputTextarea
          v-model="input"
          placeholder="Say something..."
          class="pr-12"
        />
        <PromptInputSubmit
          :status="status === 'streaming' ? 'streaming' : 'ready'"
          :disabled="!input.trim()"
          class="absolute bottom-1 right-1"
        />
      </PromptInput>
    </div>
  </div>
</template>

Features

  • Displays messages from both user and AI assistant with distinct styling and automatic alignment
  • Minimalist flat design with user messages in secondary background and assistant messages full-width
  • Response branching with navigation controls to switch between multiple AI response versions
  • Markdown rendering with GFM support (tables, task lists, strikethrough), math equations, and smart streaming
  • Action buttons for common operations (retry, like, dislike, copy, share) with tooltips and state management
  • File attachments display with support for images and generic files with preview and remove functionality
  • Code blocks with syntax highlighting and copy-to-clipboard functionality
  • Keyboard accessible with proper ARIA labels
  • Responsive design that adapts to different screen sizes
  • Seamless light/dark theme integration

Usage with AI SDK

Build a simple chat UI where the user can copy or regenerate the most recent message.

Add the following component to your frontend:

pages/index.vue
<script setup lang="ts">
import { useChat } from '@ai-sdk/vue'
import { CopyIcon, RefreshCcwIcon } from 'lucide-vue-next'
import { ref } from 'vue'

import {
  Conversation,
  ConversationContent,
  ConversationScrollButton,
} from '@/components/ai-elements/conversation'
import { Message, MessageAction, MessageActions, MessageContent, MessageResponse } from '@/components/ai-elements/message'
import {
  PromptInput,
  PromptInputSubmit,
  PromptInputTextarea,
} from '@/components/ai-elements/prompt-input'

const { messages, append, status, reload } = useChat()
const input = ref('')

function handleSubmit() {
  if (input.value.trim()) {
    append({ role: 'user', content: input.value })
    input.value = ''
  }
}

function copyToClipboard(text: string) {
  navigator.clipboard.writeText(text)
}
</script>

<template>
  <div class="max-w-4xl mx-auto p-6 relative size-full rounded-lg border h-[600px]">
    <div class="flex flex-col h-full">
      <Conversation>
        <ConversationContent>
          <template v-for="(message, messageIndex) in messages" :key="message.id">
            <template v-if="message.parts">
              <template v-for="(part, i) in message.parts" :key="`${message.id}-${i}`">
                <template v-if="part.type === 'text'">
                  <Message :from="message.role">
                    <MessageContent>
                      <MessageResponse>{{ part.text }}</MessageResponse>
                    </MessageContent>
                  </Message>

                  <MessageActions
                    v-if="message.role === 'assistant' && messageIndex === messages.length - 1"
                  >
                    <MessageAction label="Retry" @click="reload()">
                      <RefreshCcwIcon class="size-3" />
                    </MessageAction>
                    <MessageAction label="Copy" @click="copyToClipboard(part.text)">
                      <CopyIcon class="size-3" />
                    </MessageAction>
                  </MessageActions>
                </template>
              </template>
            </template>

            <template v-else>
              <Message :from="message.role">
                <MessageContent>
                  <MessageResponse>{{ message.content }}</MessageResponse>
                </MessageContent>
              </Message>

              <MessageActions
                v-if="message.role === 'assistant' && messageIndex === messages.length - 1"
              >
                <MessageAction label="Retry" @click="reload()">
                  <RefreshCcwIcon class="size-3" />
                </MessageAction>
                <MessageAction label="Copy" @click="copyToClipboard(message.content)">
                  <CopyIcon class="size-3" />
                </MessageAction>
              </MessageActions>
            </template>
          </template>
        </ConversationContent>
        <ConversationScrollButton />
      </Conversation>

      <PromptInput
        class="mt-4 w-full max-w-2xl mx-auto relative bg-transparent border-0"
        @submit="handleSubmit"
      >
        <PromptInputTextarea
          v-model="input"
          placeholder="Say something..."
          class="pr-12"
        />
        <PromptInputSubmit
          :status="status === 'streaming' ? 'streaming' : 'ready'"
          :disabled="!input.trim()"
          class="absolute bottom-1 right-1"
        />
      </PromptInput>
    </div>
  </div>
</template>

Props

<Message />

fromrequiredUIMessage['role']
The role of the message sender ("user", "assistant", or "system").
classstring
''
Additional classes applied to the component.

<MessageContent />

classstring
''
Additional classes applied to the component.

<MessageActions />

classstring
''
Additional classes applied to the component.

<MessageAction />

tooltipstring
''
Optional tooltip text shown on hover.
labelstring
''
Accessible label for screen readers. Also used as fallback if tooltip is not provided.
variantButtonVariants['variant']
'ghost'
Optional button variant.
sizeButtonVariants['size']
'icon-sm'
Optional button size.

<MessageBranch />

defaultBranchnumber
0
The index of the branch to show by default.
classstring
''
Additional classes applied to the component.

<MessageBranchContent />

classstring
''
Additional classes applied to the component.

<MessageBranchSelector />

fromrequiredUIMessage['role']
The role of the message sender ("user", "assistant", or "system").

<MessageBranchPage />

classstring
''
Additional classes applied to the component.

<MessageResponse />

contentstring
''
The content of the message.
classstring
''
Additional classes applied to the component.
streamdown-vue props
Additional props from Streamdown-vue

<MessageAttachments />

classstring
''
Additional classes applied to the component.

Example:

<MessageAttachments class="mb-2">
  <MessageAttachment
    v-for="attachment in files"
    :key="attachment.url"
    :data="attachment"
  />
</MessageAttachments>

<MessageAttachment />

datarequiredFileUIPart
The file data to display. Must include url and mediaType.
classstring
''
Additional classes applied to the component.

Example:

<MessageAttachment
  data="{
    type: 'file',
    url: 'https://example.com/image.jpg',
    mediaType: 'image/jpeg',
    filename: 'image.jpg'
  }"
  @remove="() => console.log('Remove clicked')"
/>

<MessageToolbar />

classstring
''
Additional classes applied to the component.

Emits

<MessageBranch />

branchChangefunction
Emitted when the branch changes.

<MessageAttachment />

removefunction
Emitted when the attachment is removed.