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.

Build the chat

Install the packages

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:

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 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:

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 uses Nuxt UI chat components, such as UChatMessages and UChatPrompt. It reads the part state with the isPartStreaming() helper:

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.

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:

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

Next steps

© 2026 Vercel, Inc.