---
title: "Stream Markdown in Vue and Nuxt with the AI SDK"
description: "Build a Nuxt chat that streams AI SDK responses and renders each message as Markdown with Comark while it arrives."
canonical_url: "https://comark.dev/use-cases/vue-ai-chat"
---
# Stream Markdown in Vue and Nuxt with the AI SDK

> Build a Nuxt chat that streams AI SDK responses and renders each message as Markdown with Comark while it arrives.

Use the `Chat` class from `@ai-sdk/vue` to receive messages, and render each text part with Comark's `<Markdown>` component. Bind `streaming` to the part state, so Comark closes unfinished syntax until the part is complete.

This guide uses Nuxt with the `@comark/nuxt` module, which auto-imports `<Markdown>`. In a Vue app without Nuxt, import `Markdown` from `@comark/vue`. The steps follow the [AI SDK example](https://comark.dev/examples/ai/nuxt-ai-sdk).

## Build the chat

::steps{level="3"}
### Install the packages

```bash [Terminal]
npx nuxt add comark
npm install ai @ai-sdk/vue
```

The AI SDK reads your provider key from the environment. The example uses the Vercel AI Gateway with the `AI_GATEWAY_API_KEY` variable.

### Create the server route

The route streams the model response in the format that `Chat` expects:

```typescript [server/api/chat.post.ts]
import { convertToModelMessages, streamText } from 'ai'

export default defineEventHandler(async (event) => {
  const { messages } = await readBody(event)

  const result = streamText({
    model: 'anthropic/claude-sonnet-4.6',
    system: 'You are a helpful assistant. Answer in Markdown.',
    messages: await convertToModelMessages(messages),
  })

  return result.toUIMessageStreamResponse()
})
```

The server API can change between AI SDK versions. Check the [AI SDK docs](https://ai-sdk.dev/docs) for your version.

### Render the messages

`Chat` sends requests to `/api/chat` by default. Render each text part with `<Markdown>`, and wrap it in `<Suspense>` because the Vue component is async:

```vue [app/pages/index.vue]
<script setup lang="ts">
import { Chat } from '@ai-sdk/vue'

const chat = new Chat({})
const input = ref('')

function onSubmit() {
  chat.sendMessage({ text: input.value })
  input.value = ''
}
</script>

<template>
  <div v-for="message in chat.messages" :key="message.id">
    <template v-for="(part, index) in message.parts" :key="`${message.id}-${index}`">
      <template v-if="part.type === 'text'">
        <p v-if="message.role === 'user'">{{ part.text }}</p>
        <Suspense v-else>
          <Markdown :value="part.text" :streaming="part.state === 'streaming'" caret />
        </Suspense>
      </template>
    </template>
  </div>

  <form @submit.prevent="onSubmit">
    <input v-model="input" placeholder="Ask something…">
  </form>
</template>
```
::

User messages render as plain text in this example. Only the assistant output goes through the Markdown parser.

## Use Nuxt UI chat components

The [full example](https://comark.dev/examples/ai/nuxt-ai-sdk) uses Nuxt UI chat components, such as `UChatMessages` and `UChatPrompt`. It reads the part state with the `isPartStreaming()` helper:

```vue [app/pages/index.vue]
<script setup lang="ts">
import { isPartStreaming } from '@nuxt/ui/utils/ai'
</script>

<template>
  <Suspense>
    <Markdown :value="part.text" :streaming="isPartStreaming(part)" caret />
  </Suspense>
</template>
```

When `@nuxt/ui` is installed, Comark also renders Markdown elements with the Nuxt UI prose components. See [Nuxt UI integration](https://comark.dev/rendering/nuxt#nuxt-ui-integration).

## Add components and plugins

Pass components and plugins to `<Markdown>`, for example to highlight code in answers. To keep untrusted output safe, add the [security plugin](https://comark.dev/plugins/built-in/security):

```vue [app/pages/index.vue]
<script setup lang="ts">
import shiki from '@comark/vue/plugins/shiki'
import security from '@comark/vue/plugins/security'

const plugins = [
  shiki(),
  security({ blockedTags: ['script', 'iframe', 'embed', 'form', 'base', 'meta', 'link', 'style', 'object'] }),
]
</script>

<template>
  <Suspense>
    <Markdown :value="part.text" :streaming="part.state === 'streaming'" :plugins="plugins" caret />
  </Suspense>
</template>
```

## FAQ

::accordion
  :::accordion-item{label="Why do I need Suspense?"}
  The Vue `<Markdown>` component is async, and it must be inside a `<Suspense>` boundary. The example puts one boundary around each assistant part.
  :::

  :::accordion-item{label="Can I render the answer on the server?"}
  Chat messages arrive in the browser, so client rendering is enough. For server-rendered content, parse on the server and render with [`<MarkdownDocument>`](https://comark.dev/rendering/vue#code-markdowndocument).
  :::

  :::accordion-item{label="Can the model write Comark components?"}
  Yes. Register the components in the `components` prop and tell the model which ones exist. See [generative UI](https://comark.dev/use-cases/generative-ui).
  :::
::

## Next steps

- [Run the full Nuxt AI SDK example](https://comark.dev/examples/ai/nuxt-ai-sdk)
- [Render streaming Markdown from an LLM](https://comark.dev/use-cases/ai-chat-streaming)
- [Let an LLM render UI components](https://comark.dev/use-cases/generative-ui)
- [Vue streaming reference](https://comark.dev/rendering/vue#streaming)

---

- [Full example](https://comark.dev/examples/ai/nuxt-ai-sdk)
- [Nuxt rendering](https://comark.dev/rendering/nuxt)


## Sitemap

See the full [sitemap](https://comark.dev/sitemap.md) for all pages.
