A powerful and flexible Vue library for rendering arbitrary HTML content with two distinct rendering modes: Direct Mode (with script execution) and Shadow Mode (with style isolation). Compatible with Vue 2.7+ and Vue 3.
β οΈ SECURITY WARNING
This library does NOT sanitize or validate HTML content. If you render HTML containing malicious scripts in Direct Mode, those scripts WILL execute. Always sanitize untrusted HTML content before passing it to this component, or use Shadow Mode (which disables script execution) when rendering content from untrusted sources.
- Overview
- Why Not iFrame?
- Features
- Architecture
- Installation
- Usage
- API Reference
- Rendering Modes Comparison
- Examples
- Best Practices
This library provides a unified solution for rendering HTML content in Vue applications (Vue 2.7+ and Vue 3) with full control over rendering behavior. It addresses common challenges when working with dynamic HTML content, such as:
- Script Execution: Execute embedded JavaScript with proper browser-like semantics
- Style Isolation: Prevent CSS conflicts using Shadow DOM
- Font Loading: Proper @font-face handling in Shadow DOM
- HTML Structure Preservation: Maintain complete HTML structure including
<html>,<head>, and<body>tags
You might wonder: "Why not just use an <iframe>?" Here are the key reasons:
-
Manual Size Management
- iFrames require explicit width and height
- Content doesn't naturally flow with the parent layout
- Responsive sizing requires complex JavaScript solutions
-
Complex Security Configuration
- Sandbox flags must be manually configured
- Easy to misconfigure and create security vulnerabilities
- Different browsers have different default behaviors
-
Communication Overhead
- Parent-child communication requires postMessage API
- Complex bidirectional data flow
- Difficult to share state or context
-
Performance Impact
- Each iframe creates a complete browser context
- Higher memory usage
- Slower initial load times
-
SEO and Accessibility Issues
- Search engines may not index iframe content properly
- Screen readers may have difficulty navigating
- URL management is more complex
β
Automatic Layout Integration: Content flows naturally with the parent document
β
Smart Script Handling: Controlled execution with proper async/defer/sequential semantics
β
Efficient Style Isolation: Shadow DOM provides isolation without the overhead
β
Better Performance: Lower memory footprint, faster rendering
β
Seamless Integration: Direct access to Vue context and reactivity
β
Font Loading: Automatic handling of @font-face declarations in Shadow DOM
- β Full script execution support
- β Async, defer, and sequential script handling
- β
Module script support (
type="module") - β Inline and external scripts
- β Browser-like execution semantics
- β No style isolation (uses parent styles)
- β Complete style isolation using Shadow DOM
- β
Preserves full HTML structure (
<html>,<head>,<body>) - β Automatic @font-face extraction and injection
- β No script execution (by design, for security)
- β Perfect for rendering formatted documents
- β CSS encapsulation (no style leakage)
- β Vue Composition API (Vue 2.7+ and Vue 3)
- β Full TypeScript support
- β Comprehensive documentation
- β Custom Element compatibility
- β Clean lifecycle management
- β Framework-agnostic utilities
The library is organized by responsibility for easy maintenance and potential library distribution:
vue-html-renderer/
βββ src/
β βββ App.vue # Main Vue component
β βββ main.ts # Library entry point (exports)
β βββ extras/
β β βββ types.ts # TypeScript type definitions
β β βββ utils.ts # Shared utility functions
β βββ composables/
β β βββ useHtmlRenderer.ts # Composable (internal use)
β βββ renderers/
β βββ shadowRenderer.ts # Shadow DOM rendering logic
β βββ directRenderer.ts # Direct rendering with script execution
βββ README.md # This file
- Separation of Concerns: Each module has a single, well-defined responsibility
- Type Safety: Full TypeScript coverage with comprehensive type definitions
- Extensibility: Easy to add new rendering modes or features
- Reusability: Framework-agnostic utilities can be used outside Vue
- Documentation: Every function and type is thoroughly documented
import HtmlRenderer from '@/components/htmlRenderer/HtmlRenderer.vue'
// or
import { useHtmlRenderer } from '@/components/htmlRenderer/composables/useHtmlRenderer'For distribution as a standalone npm package:
npm install @your-org/html-renderer
# or
yarn add @your-org/html-renderer<template>
<HtmlRenderer :html="htmlContent" />
</template>
<script setup lang="ts">
import HtmlRenderer from '@/components/htmlRenderer/HtmlRenderer.vue';
const htmlContent = `
<div>
<h1>Hello World</h1>
<script>
console.log('This script will execute!');
</script>
</div>
`;
</script><template>
<HtmlRenderer :html="htmlContent" :is-shadow="true" />
</template>
<script setup lang="ts">
import HtmlRenderer from '@/components/htmlRenderer/HtmlRenderer.vue'
const htmlContent = `
<!doctype html>
<html>
<head>
<style>
body { background: #f0f0f0; font-family: Arial; }
h1 { color: blue; }
</style>
</head>
<body>
<h1>Isolated Styles</h1>
<p>These styles won't affect the parent document!</p>
</body>
</html>
`
</script><template>
<div ref="hostRef"></div>
</template>
<script setup lang="ts">
import { useHtmlRenderer } from '@/components/htmlRenderer/composables/useHtmlRenderer'
const { hostRef, clear } = useHtmlRenderer({
html: '<div>Content</div>',
isShadow: false,
})
// Manually clear content if needed
// clear();
</script>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
html |
string |
Yes | - | The HTML string to render |
isShadow |
boolean |
No | false |
Whether to use Shadow DOM mode |
<HtmlRenderer :html="myHtmlString" :is-shadow="true" />interface IHtmlRendererOptions {
html: string // The HTML string to render
isShadow?: boolean // Whether to use Shadow DOM mode (default: false)
}interface IHtmlRendererComposable {
hostRef: Ref<HTMLElement | undefined> // Template ref for the host element
clear: () => void // Function to clear rendered content
shadowRoot?: Ref<ShadowRoot | undefined> // Shadow root ref (shadow mode only)
}const { hostRef, clear, shadowRoot } = useHtmlRenderer({
html: '<div>Content</div>',
isShadow: true,
})// Generate a unique ID
function uid(): string
// Normalize HTML (handle escaping/encoding)
function normalizeHtml(raw: string): string
// Normalize attribute values
function normalizeAttr(val: string): string
// Find placeholder comment node
function findPlaceholderNode(root: ParentNode, id: string): Comment | nullSee types.ts for complete type definitions:
IHtmlRendererOptionsIHtmlRendererComposableIHtmlRendererPropsIScriptMetaRenderModeIFontFaceExtractionOptions
| Feature | Direct Mode | Shadow Mode |
|---|---|---|
| Script Execution | β Yes (full support) | β No (by design) |
| Style Isolation | β No (uses parent styles) | β Yes (complete isolation) |
| HTML Structure | Partial | β
Complete (<html>, <body>, <head>) |
| Font Loading | β Automatic | β Automatic (@font-face injection) |
| Performance | Fast | Very Fast |
| Security | Requires trust in HTML source | Higher (no scripts) |
| Use Cases | Interactive content, widgets | Documents, formatted content |
- β You need JavaScript to execute
- β You trust the HTML source
- β You want interactive content
- β You need to share parent document styles
- β You're rendering dynamic widgets
- β You need style isolation
- β You don't need scripts
- β You're rendering formatted documents (coupons, certificates, vouchers)
- β You want to prevent CSS conflicts
- β You need to preserve complete HTML structure
- β Security is a priority (no script execution)
<template>
<HtmlRenderer :html="couponHtml" :is-shadow="true" />
</template>
<script setup lang="ts">
import HtmlRenderer from '@/components/htmlRenderer/HtmlRenderer.vue'
const couponHtml = `
<!doctype html>
<html>
<head>
<style>
@font-face {
font-family: 'CustomFont';
src: url('https://example.com/font.woff2') format('woff2');
}
body {
font-family: 'CustomFont', sans-serif;
background: white;
width: 18cm;
height: 26.7cm;
}
.coupon-title {
font-size: 24pt;
color: #333;
text-align: center;
}
</style>
</head>
<body>
<div class="coupon-title">$50 Gift Certificate</div>
<p>Valid until: 2025-12-31</p>
</body>
</html>
`
</script><template>
<HtmlRenderer :html="widgetHtml" :is-shadow="false" />
</template>
<script setup lang="ts">
import HtmlRenderer from '@/components/htmlRenderer/HtmlRenderer.vue';
const widgetHtml = `
<div id="widget">
<button id="clickMe">Click Me</button>
<span id="counter">0</span>
</div>
<script>
let count = 0;
document.getElementById('clickMe').addEventListener('click', () => {
count++;
document.getElementById('counter').textContent = count;
});
</script>
`;
</script><template>
<HtmlRenderer :html="scriptHtml" :is-shadow="false" />
</template>
<script setup lang="ts">
const scriptHtml = `
<div id="map"></div>
<script src="https://cdn.example.com/map-library.js" defer></script>
<script defer>
// This runs after map-library.js loads
initMap('map');
</script>
`;
</script>- Always sanitize untrusted HTML before rendering in direct mode
- Use shadow mode for content from untrusted sources (no script execution)
- Validate external script sources when using direct mode
- Be cautious with inline event handlers (
onclick, etc.)
- Avoid re-rendering - The component renders once on mount
- Use keys if you need to force re-rendering with different content
- Minimize HTML size for faster parsing
- Consider lazy loading for heavy content
- Shadow mode: Include all styles in the HTML string (they won't leak)
- Direct mode: Be aware of style inheritance from parent document
- Use scoped styles in parent components to avoid conflicts
- Test font loading in shadow mode (fonts are automatically injected)
- Understand execution order: Sequential β Async (fire-and-forget) β Defer
- Use
deferfor scripts that need DOM to be ready - Use
asyncfor independent scripts - Module scripts (
type="module") are always deferred by default
When contributing to this library, please follow these guidelines:
- Maintain separation of concerns: Keep renderers, composables, and utilities separate
- Document everything: All functions, types, and modules should have JSDoc comments
- Write tests: Add tests for new features or bug fixes
- Follow TypeScript best practices: Use strict typing, avoid
any - Update this README: Keep documentation in sync with code changes
Free to use.
This library was built to solve real-world challenges in rendering dynamic HTML content in Vue applications (Vue 2.7+ and Vue 3), specifically for rendering formatted documents like coupons and vouchers with proper style isolation and font loading.
Built with β€οΈ for Vue developers (Vue 2.7+ and Vue 3) who need powerful HTML rendering capabilities.