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.
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:
@import CSS files recursively and fetching linked stylesheets via <link rel="stylesheet" href="ā¦"><html>, <head>, and <body> tagsYou might wonder: "Why not just use an <iframe>?" Here are the key reasons:
Manual Size Management
Complex Security Configuration
Communication Overhead
Performance Impact
SEO and Accessibility Issues
ā
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
<html>, <head>, <body>)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
renderers/shadowRenderer.ts
extractAndInjectFontFaces, renderIntoShadowRoot, clearShadowRootrenderers/directRenderer.ts
renderDirectly, clearElement, extractScriptsWithPlaceholders, createExecutableScript, insertScriptAtPlaceholderstyles/cssUtils.ts
stripComments, extractFontFaceBlocks, createImportRegex, resolveUrl, rebaseUrls, getDocBaseUrlstyles/fontFaceCollector.ts
@font-face rules from inline styles, @import chains, and external stylesheetsstyles/fontInjector.ts
<style id="shadow-dom-fonts"> in document.headnpm install shadow-html-renderer
# or
yarn add shadow-html-renderer
# or
pnpm add shadow-html-renderer
import { 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)
renderIntoShadowRoot(shadowRoot, html)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>
clearShadowRoot(shadowRoot)Clears all content from a shadow root.
| Parameter | Type | Description |
|---|---|---|
shadowRoot |
ShadowRoot |
The shadow root to clear |
extractAndInjectFontFaces(doc, styleElementId?)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>
renderDirectly(target, html)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>
clearElement(target)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 | null
interface 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>
`,
)
onclick, etc.)defer for scripts that need DOM to be readyasync for independent scriptstype="module") are always deferred by defaultTo ensure readability and prevent subtle bugs, this project mandates using braces on all control statements.
if, else, else if, for, while, and do...while blocks ā even for single statements.Example:
// ā
Correct
if (condition) {
doSomething()
}
// ā Incorrect
// if (condition) doSomething()
When contributing to this library, please follow these guidelines:
anyMIT 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.