- Accordion
- Alert
- Alert Dialog
- Aspect Ratio
- Attachment
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Chart
- Checkbox
- Code Block
- Collapsible
- Combobox
- Command
- Context Menu
- Data Table
- Date Picker
- Dialog
- Drawer
- Dropdown Menu
- Empty
- Field
- Form
- Hover Card
- Input
- Input Group
- Input OTP
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Native Select
- Navigation Menu
- Number Field
- Pagination
- Pin Input
- Popover
- Progress
- Questionnaire
- Radio Group
- Range Calendar
- Resizable
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Sonner
- Spinner
- Stepper
- Switch
- Table
- Tabs
- Tags Input
- Textarea
- Toast
- Toggle
- Toggle Group
- Tooltip
- Trace
- Typography
Install Dialog, then open it from the gear button in SiteHeader.vue.
npx shadcn-vue@latest add dialogWrap the button in a DialogTrigger and put the settings form in DialogContent. Focus moves into the dialog when it opens, and Esc closes it.
<script setup lang="ts">
import {
CheckIcon,
CopyIcon,
PencilIcon,
RefreshCcwIcon,
ThumbsDownIcon,
ThumbsUpIcon,
} from '@lucide/vue'
import { createReusableTemplate, useClipboard } from '@vueuse/core'
import { ref } from 'vue'
import Markdown from '@/components/Markdown.vue'
import { Bubble, BubbleContent } from '@/components/ui/bubble'
import { Button } from '@/components/ui/button'
import {
Message,
MessageContent,
MessageFooter,
} from '@/components/ui/message'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
const reply = `Install Dialog, then open it from the gear button in \`SiteHeader.vue\`.
\`\`\`bash
npx shadcn-vue@latest add dialog
\`\`\`
Wrap the button in a \`DialogTrigger\` and put the settings form in \`DialogContent\`. Focus moves into the dialog when it opens, and **Esc** closes it.`
const { copy, copied } = useClipboard({ copiedDuring: 2000, legacy: true })
const feedback = ref<'like' | 'dislike' | null>(null)
const [DefineMessageAction, MessageAction] = createReusableTemplate<{
label: string
pressed?: boolean
onClick?: () => void
}>()
</script>
<template>
<DefineMessageAction v-slot="{ label, pressed, onClick, $slots }">
<Tooltip>
<TooltipTrigger as-child>
<Button
variant="ghost"
size="icon-sm"
:aria-label="label"
:aria-pressed="pressed"
@click="onClick?.()"
>
<component :is="$slots.default" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ label }}</p>
</TooltipContent>
</Tooltip>
</DefineMessageAction>
<div class="mx-auto flex w-full max-w-md flex-col gap-8 py-6 text-sm">
<Message align="end">
<MessageContent class="gap-2">
<Bubble align="end" variant="muted">
<BubbleContent>
How do I add a settings dialog? It should open from the gear icon
in the header.
</BubbleContent>
</Bubble>
<MessageFooter>
<MessageAction label="Copy">
<CopyIcon />
</MessageAction>
<MessageAction label="Edit">
<PencilIcon />
</MessageAction>
</MessageFooter>
</MessageContent>
</Message>
<Message>
<MessageContent class="gap-2">
<Markdown :value="reply" />
<MessageFooter class="px-0">
<MessageAction
:label="copied ? 'Copied' : 'Copy'"
@click="copy(reply)"
>
<CheckIcon v-if="copied" />
<CopyIcon v-else />
</MessageAction>
<MessageAction
label="Good response"
:pressed="feedback === 'like'"
@click="feedback = feedback === 'like' ? null : 'like'"
>
<ThumbsUpIcon :class="feedback === 'like' ? 'fill-current' : undefined" />
</MessageAction>
<MessageAction
label="Bad response"
:pressed="feedback === 'dislike'"
@click="feedback = feedback === 'dislike' ? null : 'dislike'"
>
<ThumbsDownIcon :class="feedback === 'dislike' ? 'fill-current' : undefined" />
</MessageAction>
<MessageAction label="Retry">
<RefreshCcwIcon />
</MessageAction>
</MessageFooter>
</MessageContent>
</Message>
</div>
</template>Use Message for one turn in a thread. The person's text is a Bubble on a Message with align="end". The agent's reply is markdown on a Message, with actions under it. A thread of these turns is Chat.
Installation
Message is built with the Message, Bubble, and Tooltip components.
pnpm dlx shadcn-vue@latest add message bubble tooltip
Usage
<script setup lang="ts">
import { CopyIcon } from '@lucide/vue'
import { Bubble, BubbleContent } from '@/components/ui/bubble'
import { Button } from '@/components/ui/button'
import { Message, MessageContent, MessageFooter } from '@/components/ui/message'
</script>
<template>
<Message align="end">
<MessageContent>
<Bubble align="end" variant="muted">
<BubbleContent>How do I add a settings dialog?</BubbleContent>
</Bubble>
</MessageContent>
</Message>
<Message>
<MessageContent class="gap-2">
<p>Install Dialog, then open it from the gear button.</p>
<MessageFooter>
<Button variant="ghost" size="icon-sm" aria-label="Copy">
<CopyIcon />
</Button>
</MessageFooter>
</MessageContent>
</Message>
</template>See Message.
Actions
Put actions in MessageFooter. It follows the message alignment, so on the person's turn the actions sit on the right. Use ghost icon buttons with a tooltip for each.
- Copy writes the reply to the clipboard and swaps to a check for two seconds.
- Good response and Bad response are toggles. Set
aria-pressedso screen readers announce the state. - Retry runs the turn again.
- Edit on the person's turn puts the text back in the field.
Markdown
The reply streams as markdown. Render it with Comark (@comark/vue) and route fenced code to a Code Block with a custom pre component, so code in the reply gets line numbers and a copy button. Pass streaming while the reply streams, so unfinished syntax is closed as it renders.
<script setup lang="ts">
import { Markdown } from '@comark/vue'
import MarkdownCodeBlock from './MarkdownCodeBlock.vue'
</script>
<template>
<Markdown
class="typeset"
:value="text"
:components="{ pre: MarkdownCodeBlock }"
:streaming="isStreaming"
/>
</template>AI SDK
Branch on the message role. The person's turn is the bubble. The agent's turn is the reply, and chat.regenerate with the message id runs it again.
<script setup lang="ts">
import type { UIMessage } from 'ai'
import { Chat } from '@ai-sdk/vue'
import { Markdown } from '@comark/vue'
import { RefreshCcwIcon } from '@lucide/vue'
import MarkdownCodeBlock from './MarkdownCodeBlock.vue'
const chat = new Chat({})
function getText(message: UIMessage) {
return message.parts.map(part => (part.type === 'text' ? part.text : '')).join('')
}
</script>
<template>
<template v-for="message in chat.messages" :key="message.id">
<Message v-if="message.role === 'user'" align="end">
<MessageContent>
<Bubble align="end" variant="muted">
<BubbleContent>{{ getText(message) }}</BubbleContent>
</Bubble>
</MessageContent>
</Message>
<Message v-else>
<MessageContent class="gap-2">
<Markdown
class="typeset"
:value="getText(message)"
:components="{ pre: MarkdownCodeBlock }"
:streaming="chat.status === 'streaming' && message === chat.messages.at(-1)"
/>
<MessageFooter>
<Button
variant="ghost"
size="icon-sm"
aria-label="Retry"
@click="chat.regenerate({ messageId: message.id })"
>
<RefreshCcwIcon />
</Button>
</MessageFooter>
</MessageContent>
</Message>
</template>
</template>Migrating from AI Elements Vue
If you added Message from AI Elements Vue, your project has a components/ai-elements/message/ folder. Paste this prompt into your coding agent to rebuild it on Message, Bubble and Tooltip. Every export keeps its name and props, so the code that uses it keeps working.
Migrate components/ai-elements/message/ from AI Elements Vue to the shadcn-vue Message, Bubble and Tooltip components.
Keep every export, with the same name and props, so the code that imports it keeps working. Only change how each part renders.
1. Install the components: npx shadcn-vue@latest add message bubble tooltip
2. Read https://www.shadcn-vue.com/docs/ai-elements/message and build Message the same way.
3. Rebuild each part:
- This folder exports Message and MessageContent too, so import the shadcn-vue parts under other names, like MessageRoot and MessageBody.
- Message renders the shadcn-vue Message, with align="end" when from is "user". Pass from down with provide/inject.
- MessageContent renders the shadcn-vue MessageContent. For the person's messages, wrap the slot in a Bubble with align="end" and variant="muted".
- MessageActions renders a MessageFooter.
- MessageAction renders a Button with variant="ghost" and size="icon-sm" in a Tooltip, with its label as the aria-label.
- Keep MessageResponse on the markdown renderer it already uses, and keep the branch parts and MessageToolbar as they are.
4. Remove the imports the files no longer use.
5. Run vue-tsc and fix any errors.Sponsors
Your logo hereshadcn-vue is free and open source. Sponsorship keeps it maintained.
Become a sponsor