AI

AI Message

One chat turn — role, part-driven content, a copy action and your own actions.

A single turn in a conversation. It wraps Nuxt UI's UChatMessage with a motion-choreographed action row, sensible defaults per role, and typed slots — so you render one message here and never hand-assemble an avatar, a bubble, a header and a copy button.

Defaults are the reason it exists. A user turn is solid and right-aligned, the way a sent message reads everywhere. An assistant turn is soft and left-aligned, so it does not fight the reasoning traces, tool cards and diffs it usually contains. Pass bubble: false and you get neither.

Use it for one turn. For the transcript, see AI Messages.

Usage

Give it a role and some content and the styling follows: solid and right for user, soft and left for everything else. Override the defaults only where the conversation genuinely needs it.

Ship the token audit behind a flag, or land it cold?
Reviewer
Behind a flag. The audit walks every theme token, so landing it cold means a long tail of visual diffs nobody has reviewed yet.
<template>
  <div class="flex w-full max-w-lg flex-col gap-4">
    <div class="rounded-2xl border border-default p-4">
      <MAiMessage
        id="turn-1"
        :parts="[{ type: 'text', text: 'Ship the token audit behind a flag, or land it cold?' }]"
        role="user"
      />
    </div>

    <div class="rounded-2xl border border-default p-4">
      <MAiMessage
        id="turn-2"
        :actions="voteActions"
        :bubble="bubble"
        :parts="assistantParts"
        role="assistant"
      >
        <template #header>
          <span class="flex items-center gap-2 text-xs text-muted">
            <MAgentAvatar
              :seed="'reviewer'"
              :size="20"
            />
            Reviewer
          </span>
        </template>
      </MAiMessage>
    </div>

    <USwitch
      v-model="bubble"
      class="self-start"
      label="bubble"
    />
  </div>
</template>

<script setup lang="ts">
type MessageAction = {
  key: string
  icon: string
  label: string
  active?: boolean
  onClick: () => void
}

const bubble = ref(true)
const vote = ref<'up' | 'down' | null>(null)

const assistantParts = [
  { type: 'reasoning', text: 'Check blast radius, then pick a default.' },
  { type: 'text', text: 'Behind a flag. The audit walks every theme token, so landing it cold means a long tail of visual diffs nobody has reviewed yet.' },
]

const voteActions = computed<MessageAction[]>(() => [
  {
    key: 'up',
    icon: 'i-lucide-thumbs-up',
    label: 'good response',
    active: vote.value === 'up',
    onClick: () => vote.value = vote.value === 'up' ? null : 'up',
  },
  {
    key: 'down',
    icon: 'i-lucide-thumbs-down',
    label: 'bad response',
    active: vote.value === 'down',
    onClick: () => vote.value = vote.value === 'down' ? null : 'down',
  },
])
</script>

Customize

Behind a flag. The audit walks every theme token, so landing it cold means a long tail of visual diffs nobody has reviewed yet.

Customize

Idturn-1
Roleassistant
Metadata
Side
Variant
Color
Bubble
Copyable

Props

Prop Type Default Description
idstring-Required. Stable identity for the turn. The parts array is keyed off it, so changing it remounts the message rather than reconciling two different turns.
role'system' | 'user' | 'assistant'-Required. Drives the default side and variant, and whether a copy action is offered first.
partsAiMessagePart[]-Required. The turn's content as typed pieces — { type, state?, text? }. type is a plain string, not a union, so a part type the AI SDK adds later cannot break your build. The #content slot receives the joined plain text of every type: 'text' part.
bubblebooleantrueWrap the turn in a surface. Set false when the turn already brings its own surfaces — a reasoning trace, a tool call, a diff — because a bubble around a stack of cards reads as a box inside a box.
side'left' | 'right'-Overrides alignment. Defaults to right for user and left otherwise.
variant'solid' | 'outline' | 'soft' | 'subtle' | 'naked'-Overrides the surface style. Defaults to solid for a bubbled user turn, soft for a bubbled assistant turn, and naked whenever bubble is false.
color'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | 'neutral'-Passed straight through to the underlying UChatMessage.
copyablebooleantrueOffer a copy action — but only when the turn has plain text. A turn made purely of tool calls has nothing to copy, so no button appears.
actionsAiMessageAction[][]Extra { key, icon, label, active?, onClick } objects, rendered after the copy button. active: true marks the button as a toggle that is currently on.
metadataunknown-Anything else you want to hand to the slots — a model name, token counts, a timestamp.
classstring''Merged onto the underlying UChatMessage root.

Slots

Every message-level slot receives { id, role, parts, metadata }. The #content slot additionally receives content, the joined plain text.

Slot Props Description
header-Replaces the header row entirely.
leading-The avatar slot, to the left of the bubble.
files-Where attachments render.
body-The full body area. Use it when you are composing cards rather than text.
content-Just the text. Receives the derived content string alongside the message props.
actions{ actions: AiMessageAction[] }Rendered before the default icon buttons. To replace them, override #body as well.

UI

This component's theme exposes the slots below. Override any of them per instance with the ui prop, or app-wide under ui.aiMessage in app.config.ts.

Prop Type Default Description
uiobject-Per-slot classes merged over the theme defaults.
Slot Props Description
root-ai-message-root
action-ai-message-action cursor-pointer rounded-lg p-1.5 text-muted hover:bg-muted hover:text-default aria-pressed:bg-muted aria-pressed:text-default
actionIcon-ai-message-pop

Accessibility

  • The action row is hover-revealed, which would hide it from anyone tabbing through the transcript — so :focus-within reveals it too. Focus counts as intent.
  • Each action is a native <button> carrying its label as aria-label, since the visible content is an icon only. The icon itself is aria-hidden.
  • A toggle action sets aria-pressed, so a cast vote is announced as pressed rather than only looking pressed.
  • Buttons slide in from the side their message belongs to — left for the agent, right for the user — via a --ai-message-slide custom property, so the reveal points at the turn it belongs to.
  • Under prefers-reduced-motion: reduce the slide is dropped and the copy icon's pop keyframe is disabled; the buttons simply appear.
  • The copy button's success state lasts 1600ms and then reverts. A refused clipboard permission is swallowed: the icon does not change and nothing is announced, because a failure here is not worth interrupting a reader over.
  • The component sets no landmark or ARIA role of its own; semantics come from UChatMessage.

Performance

  • Nuxt UI owns the row's opacity reveal and these buttons own transform — one property per owner, so the two fades never fight.
  • The copy timer is cleared on every copy and again in onBeforeUnmount, so a fast double-click or a mid-transition unmount cannot leave a stray timer writing to a detached component.
  • Only text parts are joined for the copy string. Reasoning traces and file parts are skipped, so a turn with a long trace does not copy the trace.
  • The action array is a computed, so the copy button's icon and label swap on a 1600ms timer without re-rendering the message body.
Copyright © 2026