---
title: "Render Comark in Nuxt"
description: "Learn how to use Comark in a Nuxt application with auto-imported components and Nuxt UI integration."
canonical_url: "https://comark.dev/rendering/nuxt"
---
# Render Comark in Nuxt

> Learn how to use Comark in a Nuxt application with auto-imported components and Nuxt UI integration.

The `@comark/nuxt` module provides **zero-config** Comark setup for Nuxt:

- Auto-imported components.
- [`~/components/prose`](#overriding-html-elements) directory for overriding HTML elements.
- [Nuxt UI](https://ui.nuxt.com) integration.
- [SSR/SSG support](#server-side-rendering) out of the box.

## Installation

### Automatic

This will automatically install the dependency and register the module in `nuxt.config.ts`:

```bash [Terminal]
npx nuxt add comark
```

### Manual

Add `@comark/nuxt` to your dependencies:

<code-group>
```bash [pnpm]
pnpm add @comark/nuxt
```


```bash [npm]
npm install @comark/nuxt
```


```bash [yarn]
yarn add @comark/nuxt
```


```bash [bun]
bun add @comark/nuxt
```
</code-group>

Then add it to the `modules` in your `nuxt.config`:

```typescript [nuxt.config.ts]
export default defineNuxtConfig({
  modules: ['@comark/nuxt']
})
```

---

## `<Markdown>`

The `<Markdown>` component is automatically available in all your templates, no imports needed. It handles parsing and rendering in one step.

### Usage

Pass markdown via the default slot or the `value` prop:

<code-group>
```vue [Default Slot]
<template>
  <Markdown>{{ content }}</Markdown>
</template>
```


```vue [Markdown Prop]
<template>
  <Markdown :value="content" />
</template>
```
</code-group>

### Props

| Prop                                                                 | Type                            | Default     | Description                                                                                                                                      |
| -------------------------------------------------------------------- | ------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `value`                                                              | `string \| MarkdownDocument`    | `undefined` | Markdown string or pre-parsed document (alternative to default slot)                                                                             |
| [`options`](#code-markdown-props-code-options)                       | `ParserOptions`                 | `{}`        | Parser options (autoUnwrap, autoClose, etc.)                                                                                                     |
| [`plugins`](#code-markdown-props-code-plugins)                       | `ComarkPlugin[]`                | `[]`        | Array of plugins                                                                                                                                 |
| `unwrap`                                                             | `boolean \| string \| string[]` | `false`     | Strip wrapper tags (MDC `unwrap`) — `true` unwraps `<p>`; a comma/space-separated string or array peels tags sequentially, for example `"ul li"` |
| [`components`](#code-markdown-props-code-components)                 | `Record<string, Component>`     | `{}`        | Custom Vue component mappings                                                                                                                    |
| [`componentsManifest`](#code-markdown-props-code-componentsmanifest) | `ComponentManifest`             | `undefined` | Dynamic component resolver                                                                                                                       |
| [`streaming`](#streaming)                                            | `boolean`                       | `false`     | Enable streaming mode                                                                                                                            |
| `summary`                                                            | `boolean`                       | `false`     | Only render content before `<!-- more -->`                                                                                                       |
| [`caret`](#streaming-caret)                                          | `boolean \| { class: string }`  | `false`     | Append caret to last text node                                                                                                                   |

#### `options`

See [ParserOptions](https://github.com/comarkdown/comark/blob/main/packages/comark/src/types.ts#L247) for available options.

```vue [app.vue]
<template>
  <Markdown :options="{ autoUnwrap: true, autoClose: true }">
    {{ content }}
  </Markdown>
</template>
```

#### `plugins`

See [ComarkPlugin](https://comark.dev/plugins) for available plugins.

```vue [app.vue]
<script setup lang="ts">
import shiki from '@comark/nuxt/plugins/shiki'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'

const plugins = [
  shiki({
    themes: { light: githubLight, dark: githubDark }
  })
]
</script>

<template>
  <Markdown :plugins="plugins">{{ content }}</Markdown>
</template>
```

For math and mermaid plugins, also pass the companion components:

```vue [app.vue]
<script setup lang="ts">
import math, { Math } from '@comark/nuxt/plugins/math'
import mermaid, { Mermaid } from '@comark/nuxt/plugins/mermaid'
import 'katex/dist/katex.min.css'
</script>

<template>
  <Markdown
    :value="content"
    :components="{ math: Math, mermaid: Mermaid }"
    :plugins="[math(), mermaid()]"
  />
</template>
```

#### `components`

Use this prop to map custom Vue components to Comark elements and use them in your markdown.

<steps level="4">
#### Create a Vue component


Save a component such as `components/Alert.vue`:


```vue [components/Alert.vue]
<script setup lang="ts">
defineProps<{
  type?: 'info' | 'warning' | 'error' | 'success'
}>()
</script>

<template>
  <div class="alert" :class="`alert-${type || 'info'}`" role="alert">
    <slot />
  </div>
</template>
```


#### Map the tag to your component


Pass the `components` prop to `<Markdown>`:


```vue [app.vue]
<script setup lang="ts">
import Alert from '~/components/Alert.vue'
import Card from '~/components/Card.vue'

const components = { alert: Alert, card: Card }
</script>

<template>
  <Markdown :components="components">{{ content }}</Markdown>
</template>
```


<note>
Components receive props from the markdown and render children via slots.
</note>
#### Use it in your Markdown content


```mdc
::alert{type="warning"}
This is a warning message!
::
```
</steps>

<tip>
To override how native HTML elements render (such as `h1` or `pre`), place components in `~/components/prose` instead. See [Overriding HTML elements](#overriding-html-elements).
</tip>

#### `componentsManifest`

For lazy-loading components on demand. Components are resolved once and cached.

```vue [app.vue]
<script setup lang="ts">
const manifest = (name: string) => {
  return import(`./components/prose/${name}.vue`)
}
</script>

<template>
  <Markdown :components-manifest="manifest">{{ content }}</Markdown>
</template>
```

---

## Server-side rendering

Comark fully supports multiple rendering modes in Nuxt:

### Static generation

~~~vue [pages/index.vue]
<script setup lang="ts">
// Content is parsed at build time
const content = `# Static Content

This is rendered at build time.
`
</script>

<template>
  <Markdown>{{ content }}</Markdown>
</template>
~~~

### Dynamic SSR

```vue [pages/blog/[slug\\].vue]
<script setup lang="ts">
// Fetch and render on the server
const { data: content } = await useFetch('/api/article')
</script>

<template>
  <Markdown v-if="content">{{ content }}</Markdown>
</template>
```

### Prerendering

```typescript [nuxt.config.ts]
export default defineNuxtConfig({
  modules: ['@comark/nuxt'],
  nitro: {
    prerender: {
      routes: ['/blog', '/docs']
    }
  }
})
```

---

## Overriding HTML elements

Place Vue components in `~/components/prose` to override how native HTML elements render. They are automatically registered, no imports or `components` prop needed.

### Directory structure

```
~/components/
  prose/
    ProseH1.vue
    ProseH2.vue
    ProsePre.vue
    ProseA.vue
```

### Example

```vue [components/prose/ProseH1.vue]
<script setup lang="ts">
defineProps<{
  id?: string
}>()
</script>

<template>
  <h1 :id="id" class="custom-heading">
    <a v-if="id" :href="`#${id}`">#</a>
    <slot />
  </h1>
</template>
```

### Resolution order

When Comark encounters a tag, it looks for a matching component in this order, stopping at the first match:

1. **`Prose{PascalTag}`**: for example, `ProseH1` for `h1`. Nuxt auto-registers all components from `~/components/prose` matching this convention.
2. **`{PascalTag}`**: for example, `Alert` for `alert`. PascalCase version of the tag name.
3. **`{tag}`**: for example, `alert`. Exact tag name as-is.
4. **Global**: any component registered via `app.component()` or Nuxt auto-imports.

If none match, the tag renders as a native HTML element.

---

## Component bindings

Comark automatically bridges the gap between Comark syntax and your component's interface.

### Prop binding

Attributes in Comark syntax are passed as props to your component. Use the `:` prefix to pass typed values:

| Markdown                    | Prop value                |
| --------------------------- | ------------------------- |
| `{type="warning"}`          | `"warning"` (string)      |
| `{:count="5"}`              | `5` (number)              |
| `{:active="true"}`          | `true` (boolean)          |
| `{:config='{"key":"val"}'}` | `{ key: 'val' }` (object) |

### Named slots

Named slots in Comark (`#slotname`) map to Vue named slots:

- **Default slot** → `<slot />`
- **Named slots** → `<slot name="slotname" />` (for example, `#footer` → `<slot name="footer" />`)

<code-group>
```vue [Vue]
<script setup lang="ts">
defineProps<{
  title?: string
}>()
</script>

<template>
  <div class="card">
    <h3 v-if="title">{{ title }}</h3>
    <slot />
    <footer>
      <slot name="footer" />
    </footer>
  </div>
</template>
```


```mdc [Comark]
::card{title="My Card"}
Default slot content.

#footer
Footer slot content.
::
```
</code-group>

---

## Nuxt UI integration

When [`@nuxt/ui`](https://ui.nuxt.com) is installed, Comark automatically uses Nuxt UI's prose components for enhanced styling.

<callout icon="i-lucide-sparkles" color="info">
No extra configuration needed. The `@comark/nuxt` module detects `@nuxt/ui` in your dependencies and configures prose components automatically.
</callout>

```typescript [nuxt.config.ts]
export default defineNuxtConfig({
  modules: ['@comark/nuxt', '@nuxt/ui']
})
```

### CSS setup

```css [app/assets/css/main.css]
@import "tailwindcss";
@import "@nuxt/ui";
```

```typescript [nuxt.config.ts]
export default defineNuxtConfig({
  modules: ['@comark/nuxt', '@nuxt/ui'],
  css: ['~/assets/css/main.css']
})
```

---

## Streaming

Enable real-time rendering as content arrives, ideal for AI chat interfaces and live previews.

### Setup

Set `streaming` to `true` while content is being received, then `false` when done:

```vue [app.vue]
<script setup lang="ts">
const content = ref('')
const isStreaming = ref(false)

async function askAI(prompt: string) {
  content.value = ''
  isStreaming.value = true

  const response = await fetch('/api/chat', {
    method: 'POST',
    body: JSON.stringify({ prompt }),
  })

  const reader = response.body!.getReader()
  const decoder = new TextDecoder()

  while (true) {
    const { done, value } = await reader.read()
    if (done) break
    content.value += decoder.decode(value, { stream: true })
  }

  isStreaming.value = false
}
</script>

<template>
  <Markdown :streaming="isStreaming" caret>
    {{ content }}
  </Markdown>
</template>
```

<callout icon="i-lucide-info" color="info">
`autoClose` is enabled by default: incomplete syntax like `**bold text` is automatically closed on every parse. Disable with `:options="{ autoClose: false }"`.
</callout>

### Caret

The `caret` prop appends a blinking cursor to the last text node while `streaming` is `true`:

```vue [app.vue]
<!-- Default caret -->
<Markdown :streaming="isStreaming" caret>{{ content }}</Markdown>

<!-- Custom caret class -->
<Markdown :streaming="isStreaming" :caret="{ class: 'my-caret' }">{{ content }}</Markdown>
```

```css
.my-caret {
  display: inline-block;
  width: 2px;
  height: 1em;
  background: currentColor;
  animation: blink 1s step-end infinite;
  vertical-align: text-bottom;
}

@keyframes blink {
  50% { opacity: 0; }
}
```

---

## TypeScript support

Use `ComarkPlugin` from `comark` to type plugin arrays, and `ElementNode` to type the `__node` prop in components that override HTML elements:

```vue [components/ComarkWrapper.vue]
<script setup lang="ts">
import type { Component } from 'vue'
import type { ComarkPlugin } from 'comark'

interface Props {
  content: string
  components?: Record<string, Component>
  plugins?: ComarkPlugin[]
}

const props = defineProps<Props>()
</script>

<template>
  <Markdown :components="props.components" :plugins="props.plugins">
    {{ props.content }}
  </Markdown>
</template>
```

```vue [components/prose/ProseH1.vue]
<script setup lang="ts">
import type { ElementNode } from 'comark'

defineProps<{
  __node?: ElementNode
  id?: string
}>()
</script>
```

---

---

- [Plugins](https://comark.dev/plugins)
- [Streaming API](https://comark.dev/reference/auto-close)


## Sitemap

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