Chapter 74: Vue v-html Directive
V-html
This directive is not like v-if, v-for, or v-model — it’s in a completely different league because it can either be extremely useful or extremely dangerous (XSS security hole) depending on how you use it.
I’m going to explain it very carefully, step by step, like I’m sitting next to you warning you before you touch a live wire.
1. What does v-html actually do?
v-html tells Vue:
“Take this string value, treat it as raw HTML, and inject it directly into the innerHTML of this element.”
In other words — it bypasses Vue’s normal text escaping and rendering system.
Normal text interpolation {{ }}:
|
0 1 2 3 4 5 6 |
<p>{{ userBio }}</p> |
→ If userBio = “<strong>Bold text</strong>” → user sees literal <strong>Bold text</strong> (escaped)
With v-html:
|
0 1 2 3 4 5 6 |
<p v-html="userBio"></p> |
→ User sees actual bold text: Bold text
Vue takes the string and does element.innerHTML = userBio under the hood.
2. The Golden Rule (Memorize This Forever)
Never use v-html with content that comes from a user or an untrusted source.
Why?
Because any HTML + JavaScript inside the string will be executed.
|
0 1 2 3 4 5 6 7 |
userBio = '<img src="x" onerror="alert(\'Hacked!\')">' + '<script>document.cookie = "stolen"</script>' |
→ If you do <div v-html=”userBio”></div> → the script runs → cookies stolen → XSS attack
This is exactly how XSS (Cross-Site Scripting) vulnerabilities happen in Vue apps.
3. Safe & Realistic Use-Cases (When v-html is Actually Okay)
You should only use v-html when:
- The HTML comes from your own trusted server (you control it 100%)
- The HTML is sanitized on the server (never trust client input)
- You’re rendering rich text from a CMS (Markdown → HTML, TinyMCE, CKEditor output)
- You’re showing pre-formatted content from admin panel / database
Real example 1 – Blog post content from trusted backend
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
<template> <article class="blog-post"> <h1>{{ post.title }}</h1> <!-- Safe: content comes from your own database, sanitized on server --> <div class="post-content" v-html="post.body"></div> <p>Published: {{ post.date }}</p> </article> </template> <script setup lang="ts"> import { ref, onMounted } from 'vue' const post = ref({ title: 'Vue 3 in 2026 – What Changed?', body: ` <p>Vue 3 brought <strong>Composition API</strong>, <code>&lt;script setup&gt;</code>, Teleport, Suspense...</p> <ul> <li>Reactivity with Proxy</li> <li>Better TypeScript support</li> </ul> <blockquote>Best framework ever!</blockquote> `, date: 'Feb 2026' }) // In real app → fetch from API onMounted(async () => { // await fetch('/api/posts/1').then(r => r.json()).then(data => post.value = data) }) </script> <style scoped> .post-content { line-height: 1.7; font-size: 1.1rem; } .post-content :is(h1,h2,h3) { margin: 1.5rem 0 0.8rem; } .post-content p { margin: 1rem 0; } .post-content code { background: #f1f5f9; padding: 0.2rem 0.4rem; border-radius: 4px; } .post-content blockquote { border-left: 4px solid #3b82f6; padding-left: 1rem; color: #4b5563; font-style: italic; } </style> |
→ post.body contains safe HTML from your own backend → v-html renders it beautifully
Real example 2 – Rendering sanitized Markdown
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<template> <div v-html="renderedMarkdown"></div> </template> <script setup lang="ts"> import { computed } from 'vue' import { marked } from 'marked' // or markdown-it const rawMarkdown = ref(` # Hello World This is **bold** and *italic*. - List item 1 - List item 2 `) const renderedMarkdown = computed(() => { // marked() returns safe HTML (no script tags executed) return marked(rawMarkdown.value, { sanitize: true }) }) </script> |
→ Markdown from trusted source → converted to HTML → safely rendered with v-html
4. The Danger Zone – Never Do This
Never ever do this:
|
0 1 2 3 4 5 6 7 8 |
<div v-html="userSubmittedComment"></div> <!-- or --> <div v-html="window.location.search"></div> <!-- URL params --> |
→ If attacker submits <script>alert(‘XSS’);</script> → it executes
Never trust:
- URL query params
- LocalStorage values (user can edit)
- API responses from untrusted users
- Any client-controlled string
5. Quick Summary Table – v-html Do’s & Don’ts (2026)
| Situation | Safe to use v-html? | Recommended Approach |
|---|---|---|
| Trusted backend / CMS content | Yes | Sanitize on server + v-html |
| Markdown / rich text from admin | Yes | marked / markdown-it + sanitize + v-html |
| User comments / forum posts | No | Use textContent or safe renderer (DOMPurify) |
| Dynamic HTML from API (trusted) | Yes | Sanitize + v-html |
| URL params / query string | No | Never – XSS risk |
| Any client-editable content | No | Use {{ text }} or textContent |
Pro Tips from Real Projects (Hyderabad 2026)
-
Always sanitize HTML on server-side before sending to client
-
If you must render user-generated HTML client-side → use DOMPurify library
JavaScript012345678import DOMPurify from 'dompurify'const safeHtml = DOMPurify.sanitize(userInput) -
Use v-html only when you really need HTML formatting — prefer {{ text }} or v-text when possible
-
In Vue Devtools → you can see exactly what HTML was injected via v-html
-
For email templates / newsletters → v-html is fine (trusted content)
-
Never use v-html inside user-facing areas without sanitization — one XSS vulnerability = big security incident
Your mini practice task:
- Create a component that shows blog post content with v-html (use safe static HTML)
- Try injecting <script>alert(‘test’)</script> → see it execute (danger demo)
- Add DOMPurify → sanitize before v-html → see script is removed
Any part confusing? Want full examples for:
- v-html + DOMPurify + user comments (safe way)?
- v-html vs v-text vs {{ }} security comparison?
- Real blog post renderer with Markdown → HTML → v-html?
- When to use v-html in production (trusted CMS cases)?
Just tell me — we’ll build the next safe, rich-text component together 🚀
