A powerful and flexible framework-agnostic library for rendering HTML content into Shadow DOM with complete style isolation and full script execution support. Works with any JavaScript framework (React, Vue, Angular, Svelte, etc.) or vanilla JavaScript.
β οΈ SECURITY WARNING
This library does NOT sanitize or validate HTML content. If you render HTML containing malicious scripts, those scripts WILL execute. Always sanitize untrusted HTML content before passing it to this library.
- Overview
- Why Not iFrame?
- Features
- Architecture
- Installation
- Usage
- API Reference
- Examples
- Best Practices
- Code Style
- Contributing
This library provides a unified solution for rendering HTML content in any JavaScript application 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, including resolving
@importCSS files recursively and fetching linked stylesheets via<link rel="stylesheet" href="β¦"> - 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
β
Framework Agnostic: Works with any JavaScript framework or vanilla JS
β
Font Loading: Automatic handling of @font-face declarations in Shadow DOM
- β Complete style isolation using Shadow DOM
- β Full script execution support (async, defer, sequential, module)
- β
Preserves full HTML structure (
<html>,<head>,<body>) - β Automatic @font-face extraction and injection
- β Browser-like script execution semantics
- β CSS encapsulation (no style leakage)
- β Framework agnostic (works with React, Vue, Angular, Svelte, vanilla JS)
- β Full TypeScript support
- β Comprehensive documentation
- β Zero dependencies
- β Clean lifecycle management
The library is organized by responsibility for easy maintenance:
shadow-html-renderer/
βββ src/
β βββ main.ts # Library entry point with exports
β βββ extras/
β β βββ types.ts # TypeScript type definitions
β β βββ utils.ts # Shared utility functions
β βββ renderers/
β β βββ shadowRenderer.ts # Shadow DOM rendering orchestrator
β β βββ directRenderer.ts # Direct rendering with script execution
β βββ styles/ # Font-face extraction utilities
β βββ cssUtils.ts # Pure CSS/text helpers
β βββ fontFaceCollector.ts # Recursively collect @font-face rules
β βββ fontInjector.ts # Inject fonts into document head
βββ README.md # This file
- Framework Agnostic: No dependencies on any JavaScript framework
- Separation of Concerns: Each module has a single, well-defined responsibility
- Type Safety: Full TypeScript coverage with comprehensive type definitions
- Zero Dependencies: Pure JavaScript/TypeScript with no external dependencies
- Documentation: Every function and type is thoroughly documented
-
renderers/shadowRenderer.ts- Orchestrates Shadow DOM rendering
- Parses HTML and delegates font work to style modules
- Public API:
extractAndInjectFontFaces,renderIntoShadowRoot,clearShadowRoot
-
renderers/directRenderer.ts- Direct DOM rendering with script execution
- Public API:
renderDirectly,clearElement,extractScriptsWithPlaceholders,createExecutableScript,insertScriptAtPlaceholder
-
styles/cssUtils.ts- Pure functions for CSS manipulation and URL handling
stripComments,extractFontFaceBlocks,createImportRegex,resolveUrl,rebaseUrls,getDocBaseUrl
-
styles/fontFaceCollector.ts- Recursively collects
@font-facerules from inline styles,@importchains, and external stylesheets
- Recursively collects
-
styles/fontInjector.ts- Injects collected rules into a single
<style id="shadow-dom-fonts">indocument.head
- Injects collected rules into a single
npm install shadow-html-renderer
# or
yarn add shadow-html-renderer
# or
pnpm add shadow-html-rendererimport { renderIntoShadowRoot, clearShadowRoot } from 'shadow-html-renderer'
// Create a host element
const host = document.createElement('div')
document.body.appendChild(host)
// Attach shadow root
const shadowRoot = host.attachShadow({ mode: 'open' })
// Render HTML into shadow root
await renderIntoShadowRoot(
shadowRoot,
`
<!doctype html>
<html>
<head>
<style>
body { background: #f0f0f0; font-family: Arial; }
h1 { color: blue; }
</style>
</head>
<body>
<h1>Hello World</h1>
<p>Styles are isolated and won't affect the parent document!</p>
<script>
console.log('Scripts execute with full support!');
</script>
</body>
</html>
`,
)
// Clear content when needed
clearShadowRoot(shadowRoot)import { useEffect, useRef } from 'react'
import { renderIntoShadowRoot, clearShadowRoot } from 'shadow-html-renderer'
function HtmlRenderer({ html }: { html: string }) {
const hostRef = useRef<HTMLDivElement>(null)
const shadowRootRef = useRef<ShadowRoot | null>(null)
useEffect(() => {
if (!hostRef.current) return
// Attach shadow root on mount
if (!shadowRootRef.current) {
shadowRootRef.current = hostRef.current.attachShadow({ mode: 'open' })
}
// Render HTML
renderIntoShadowRoot(shadowRootRef.current, html)
// Cleanup on unmount
return () => {
if (shadowRootRef.current) {
clearShadowRoot(shadowRootRef.current)
}
}
}, [html])
return <div ref={hostRef} />
}<template>
<div ref="hostRef"></div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { renderIntoShadowRoot, clearShadowRoot } from 'shadow-html-renderer'
const props = defineProps<{ html: string }>()
const hostRef = ref<HTMLElement>()
let shadowRoot: ShadowRoot | null = null
onMounted(async () => {
if (!hostRef.value) return
shadowRoot = hostRef.value.attachShadow({ mode: 'open' })
await renderIntoShadowRoot(shadowRoot, props.html)
})
onBeforeUnmount(() => {
if (shadowRoot) {
clearShadowRoot(shadowRoot)
}
})
</script>import { Component, ElementRef, Input, OnInit, OnDestroy, ViewChild } from '@angular/core'
import { renderIntoShadowRoot, clearShadowRoot } from 'shadow-html-renderer'
@Component({
selector: 'app-html-renderer',
template: '<div #host></div>',
})
export class HtmlRendererComponent implements OnInit, OnDestroy {
@Input() html: string = ''
@ViewChild('host', { static: true }) hostRef!: ElementRef<HTMLDivElement>
private shadowRoot: ShadowRoot | null = null
async ngOnInit() {
this.shadowRoot = this.hostRef.nativeElement.attachShadow({ mode: 'open' })
await renderIntoShadowRoot(this.shadowRoot, this.html)
}
ngOnDestroy() {
if (this.shadowRoot) {
clearShadowRoot(this.shadowRoot)
}
}
}If you don't need style isolation, you can use direct rendering:
import { renderDirectly, clearElement } from 'shadow-html-renderer'
const container = document.getElementById('content')
// Render HTML directly into element
await renderDirectly(container, '<div><h1>Hello</h1><script>console.log("Hi")</script></div>')
// Clear when needed
clearElement(container)Renders HTML content into a Shadow Root with style isolation and script execution.
| Parameter | Type | Description |
|---|---|---|
shadowRoot |
ShadowRoot |
The shadow root to render into |
html |
string |
The HTML string to render |
Returns: Promise<void>
Clears all content from a shadow root.
| Parameter | Type | Description |
|---|---|---|
shadowRoot |
ShadowRoot |
The shadow root to clear |
Extracts @font-face rules from a document and injects them into the main document.
| Parameter | Type | Default | Description |
|---|---|---|---|
doc |
Document |
- | The parsed document containing style elements |
styleElementId |
string |
"shadow-dom-fonts" |
ID for the injected style element |
Returns: Promise<void>
Renders HTML content directly into an element with script execution but without style isolation.
| Parameter | Type | Description |
|---|---|---|
target |
HTMLElement |
The target element to render into |
html |
string |
The HTML string to render |
Returns: Promise<void>
Clears all children from a target element.
| Parameter | Type | Description |
|---|---|---|
target |
HTMLElement |
The element to clear |
// 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 | nullinterface IHtmlRendererOptions {
html: string
}
interface IScriptMeta {
id: string
attrs: Record<string, string>
code: string | null
hasSrc: boolean
isAsync: boolean
isDefer: boolean
isModule: boolean
}
interface IFontFaceExtractionOptions {
styleElementId?: string
preventDuplicates?: boolean
}import { renderIntoShadowRoot } from 'shadow-html-renderer'
const host = document.createElement('div')
document.body.appendChild(host)
const shadowRoot = host.attachShadow({ mode: 'open' })
await renderIntoShadowRoot(
shadowRoot,
`
<!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>
`,
)import { renderIntoShadowRoot } from 'shadow-html-renderer'
const host = document.createElement('div')
document.body.appendChild(host)
const shadowRoot = host.attachShadow({ mode: 'open' })
await renderIntoShadowRoot(
shadowRoot,
`
<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>
`,
)import { renderIntoShadowRoot } from 'shadow-html-renderer'
const host = document.createElement('div')
document.body.appendChild(host)
const shadowRoot = host.attachShadow({ mode: 'open' })
await renderIntoShadowRoot(
shadowRoot,
`
<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>
`,
)- Always sanitize untrusted HTML before rendering
- Validate external script sources before including them
- Be cautious with inline event handlers (
onclick, etc.) - Review scripts in HTML content from external sources
- Avoid re-rendering - The renderer is optimized for single renders
- Minimize HTML size for faster parsing
- Consider lazy loading for heavy content
- Include all styles in the HTML string - they are isolated and won't leak to the parent document
- Use @font-face declarations - they are automatically extracted and injected into the main document
- Take advantage of style isolation - parent document styles won't affect rendered content
- Test font loading - fonts are automatically injected into the main document
- 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
To ensure readability and prevent subtle bugs, this project mandates using braces on all control statements.
- Always use braces for
if,else,else if,for,while, anddo...whileblocks β even for single statements.
Example:
// β
Correct
if (condition) {
doSomething()
}
// β Incorrect
// if (condition) doSomething()When contributing to this library, please follow these guidelines:
- Maintain separation of concerns: Keep renderers 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
MIT License - Free to use.
This library was built to solve real-world challenges in rendering dynamic HTML content in JavaScript applications, specifically for rendering formatted documents like coupons and vouchers with proper style isolation and font loading.
Built with β€οΈ for developers who need powerful, framework-agnostic HTML rendering capabilities.