---
title: "AI Messages"
description: "A transcript of chat turns, with this module's six AI states mapped onto Nuxt UI's four."
canonical_url: "https://nuxt-motion-block.pages.dev/components/ai/ai-messages"
---
# AI Messages

> A transcript of chat turns, with this module's six AI states mapped onto Nuxt UI's four.

The transcript: a scrolling list of turns with per-role defaults already set.
It wraps Nuxt UI's `UChatMessages` and does one thing the wrapper cannot — it
accepts this module's six-state `AIState` vocabulary and translates it down to
the four states chat components actually speak.

That translation is the whole reason it exists. `idle`, `listening`, `thinking`,
`streaming`, `done`, `error` describe what an AI surface is doing far better
than `ready` / `submitted` / `streaming` / `error` describe what a chat list
should show. Rather than asking every consumer to bridge the two, this component
collapses the six onto the four in one place:

<table>
<thead>
  <tr>
    <th>
      <code>
        state
      </code>
    </th>
    
    <th>
      chat status
    </th>
    
    <th>
      why
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        idle
      </code>
    </td>
    
    <td>
      <code>
        ready
      </code>
    </td>
    
    <td>
      resting, input accepted
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        listening
      </code>
    </td>
    
    <td>
      <code>
        ready
      </code>
    </td>
    
    <td>
      still taking input in, so it is still ready
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        thinking
      </code>
    </td>
    
    <td>
      <code>
        submitted
      </code>
    </td>
    
    <td>
      a turn is in flight, nothing has arrived
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        streaming
      </code>
    </td>
    
    <td>
      <code>
        streaming
      </code>
    </td>
    
    <td>
      tokens are landing
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        done
      </code>
    </td>
    
    <td>
      <code>
        ready
      </code>
    </td>
    
    <td>
      finished, back to a resting surface
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        error
      </code>
    </td>
    
    <td>
      <code>
        error
      </code>
    </td>
    
    <td>
      the turn failed
    </td>
  </tr>
</tbody>
</table>

## Usage

Pass your turns as messages with `parts` and set `state` to whichever of the six
AI states the surface is in — you never pick a chat status yourself. To render a
single turn with the motion-choreographed action row, see
[AI Message](https://nuxt-motion-block.pages.dev/components/ai/ai-message).

<code-preview label="Preview">
<div className="flex,justify-center,w-full,h-full">
<component-example name="ai-messages-basic">



</component-example>
</div>

<template v-slot:code="">

```vue
<template>
  <div class="flex w-full max-w-lg flex-col gap-4">
    <div class="flex items-center gap-2">
      <UButton
        v-for="value in states"
        :key="value"
        :color="state === value ? 'primary' : 'neutral'"
        :label="value"
        :variant="state === value ? 'solid' : 'outline'"
        size="sm"
        @click="state = value"
      />
    </div>

    <div class="rounded-2xl border border-default">
      <MAiMessages
        :messages="messages"
        :state="state"
        class="max-h-72"
      >
        <template #leading="{ role }">
          <MAgentAvatar
            v-if="role !== 'user'"
            :seed="role"
            :size="28"
          />
        </template>
      </MAiMessages>
    </div>
  </div>
</template>

<script setup lang="ts">
type ChatMessage = {
  id: string
  role: 'system' | 'user' | 'assistant'
  parts: { type: string, text?: string }[]
  metadata?: unknown
}

type DemoState = 'idle' | 'listening' | 'thinking' | 'streaming' | 'done' | 'error'

const states: DemoState[] = ['idle', 'thinking', 'streaming', 'done', 'error']
const state = ref<DemoState>('idle')

const messages = ref<ChatMessage[]>([
  {
    id: 'm-1',
    role: 'user',
    parts: [{ type: 'text', text: 'Why does the token audit need a flag?' }],
  },
  {
    id: 'm-2',
    role: 'assistant',
    parts: [{
      type: 'text',
      text: 'It walks every theme token, so landing it cold produces a long tail of visual diffs that nobody has reviewed yet.',
    }],
  },
])
</script>
```

</template>
</code-preview>

## Customize

<code-preview label="Preview">
<div className="w-full">
<component-example name="ai-messages-custom">



</component-example>
</div>

<template v-slot:code="">
<playground-code name="AiMessages">



</playground-code>
</template>
</code-preview>

## Props

<component-props>
<component-prop name="messages" type="AiUIMessage[]">

Required. Each entry is `{ id, role, parts, metadata? }`, where `role` is
`'system' | 'user' | 'assistant'` and `parts` is an array of
`{ type, state?, text? }`. `type` is a plain string, so a part type the AI SDK
adds later cannot break your build.

</component-prop>

<component-prop name="state" type="'idle' | 'listening' | 'thinking' | 'streaming' | 'done' | 'error'">

The conversation's state in this module's vocabulary, mapped internally onto a
chat status per the table above. Omit it for a settled conversation.

</component-prop>

<component-prop name="user" type="Record<string, unknown>">

Extra props for the `user` role, merged over the defaults
`{ side: 'right', variant: 'solid', color: 'neutral' }`.

</component-prop>

<component-prop name="assistant" type="Record<string, unknown>">

Extra props for the `assistant` role, merged over the defaults
`{ side: 'left', variant: 'soft' }`. Assistant turns stay soft rather than
solid so a turn carrying its own surfaces — a reasoning trace, a tool call — is
not boxed twice.

</component-prop>

<component-prop name="compact" type="boolean" default="false">

Tighter vertical rhythm, passed through to the wrapper.

</component-prop>

<component-prop name="shouldAutoScroll" type="boolean" default="false">

Track new content automatically.

</component-prop>

<component-prop name="shouldScrollToBottom" type="boolean" default="true">

Jump to the newest turn when the list mounts.

</component-prop>

<component-prop name="spacingOffset" type="number" default="0">

Extra scroll room at the end of the list, in pixels. Raise it when the prompt
input sits above the transcript rather than below it.

</component-prop>

<component-prop name="class" type="string" default="''">

Merged onto the wrapper.

</component-prop>
</component-props>

## Slots

The wrapper forwards every slot it does not consume — `header`, `leading`,
`files`, `body`, `content`, `actions` — into each turn. Slot props come from
`UChatMessages`, so a `#leading` receives the turn's `role`, and a `#body`
receives the turn's `parts`. A `#default` slot is forwarded too, which is the
supported way to draw the list yourself.

To get the motion-choreographed action row and the per-role bubble defaults on
each turn, use [AI Message](https://nuxt-motion-block.pages.dev/components/ai/ai-message) inside a `#default`
slot instead of relying on the wrapper's own rendering.

## UI

This component's theme exposes the slots below. Override any of them per instance with the [`ui` prop](https://nuxt-motion-block.pages.dev/getting-started/theming), or app-wide under `ui.aiMessages` in `app.config.ts`.

<component-props>
<component-prop name="ui" type="object">

Per-slot classes merged over the theme defaults.

</component-prop>
</component-props>

<component-slots>
<component-slot-row name="root">



</component-slot-row>
</component-slots>

## Accessibility

- This component adds no ARIA of its own; the scroll container, landmarks and
live-region behaviour come from `UChatMessages`.
- `state` is the hook a screen reader can be told about. Nuxt UI announces status
changes off the mapped chat status, which is why `thinking` and `streaming`
map to `submitted` and `streaming` and not to `ready`.
- Because `listening` and `done` both collapse to `ready`, neither announces a
change to assistive technology. If your UI needs those distinctions spoken,
render them yourself rather than assuming the transcript will.
- The list has no fixed height by default. Set one on the wrapper — `max-h-72` in
the example above — so scrolling actually happens, and keep the scroll container
focusable if you want keyboard paging.

## Performance

- `state` is resolved through a `computed`, so a per-frame state change never
re-renders the list; only the computed status string changes, and the wrapper
receives a different `status` prop.
- The forwarded-slot list is derived from `useSlots()` on each render, so adding
a slot is picked up without a restart.
- Role defaults are declared `as const`. An object literal inside a `computed`
widens `side`, `variant` and `color` to `string`, which no longer satisfies
the unions `ChatMessage` expects — a type error that only appears once the
merge is written naively.
- `user` and `assistant` are spread into fresh objects per change, so mutating a
role's props does not leak into the other role's defaults.


## Sitemap

See the full [sitemap](https://nuxt-motion-block.pages.dev/sitemap.md) for all pages.
