HTML Content Rendering Examples
This document provides comprehensive examples of using the .asHTML() modifier for secure HTML content rendering in various real-world scenarios.
Basic Examples
Simple Rich Text
typescript
import { Text, VStack } from '@tachui/core'
// Basic formatted content
const WelcomeMessage = () => {
return Text('<p>Welcome to <strong>TachUI</strong>! Start building <em>amazing</em> applications today.</p>')
.modifier
.asHTML()
.fontSize(16)
.lineHeight(1.5)
.textAlign('center')
.padding(20)
.build()
}Newsletter Content
typescript
const NewsletterArticle = () => {
const content = `
<article>
<header>
<h1>The Future of Web Development</h1>
<p><em>Published March 15, 2024</em></p>
</header>
<section>
<p>Modern web development is evolving rapidly, with new frameworks and approaches emerging regularly.</p>
<h2>Key Trends</h2>
<ul>
<li><strong>Declarative UI</strong> - Framework that describe what the UI should look like</li>
<li><strong>Type Safety</strong> - Catching errors at compile time</li>
<li><strong>Component Architecture</strong> - Reusable, composable building blocks</li>
</ul>
<blockquote>
"The best way to predict the future is to build it."
</blockquote>
<p>
Learn more about modern development practices in our
<a href="/docs/guide" target="_blank" rel="noopener">comprehensive guide</a>.
</p>
</section>
</article>
`
return Text(content)
.modifier
.asHTML()
.maxWidth(600)
.padding(24)
.backgroundColor('#FFFFFF')
.border(1, '#E5E5E7')
.cornerRadius(12)
.shadow({ x: 0, y: 4, blur: 16, color: 'rgba(0,0,0,0.1)' })
.build()
}Security-Focused Examples
User-Generated Content (Comments)
typescript
import DOMPurify from 'dompurify'
interface Comment {
id: string
author: string
content: string
timestamp: string
}
const CommentComponent = (comment: Comment) => {
// Strict sanitization for user comments
const sanitizeUserContent = (html: string) => {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'strong', 'em', 'code', 'br'],
ALLOWED_ATTR: [],
KEEP_CONTENT: true,
RETURN_DOM_FRAGMENT: false
})
}
return VStack({
children: [
// Author (plain text, no HTML)
Text(comment.author)
.modifier
.fontWeight('600')
.fontSize(14)
.foregroundColor('#2C3E50')
.build(),
// Comment content (sanitized HTML)
Text(comment.content)
.modifier
.asHTML({
customSanitizer: sanitizeUserContent
})
.fontSize(14)
.lineHeight(1.4)
.marginTop(4)
.build(),
// Timestamp (plain text)
Text(comment.timestamp)
.modifier
.fontSize(12)
.opacity(0.6)
.marginTop(8)
.build()
]
})
.modifier
.padding(16)
.backgroundColor('#F8F9FA')
.border(1, '#E9ECEF')
.cornerRadius(8)
.marginBottom(12)
.build()
}
// Usage example
const CommentsSection = (comments: Comment[]) => {
return VStack({
children: [
Text('Comments')
.modifier
.fontSize(20)
.fontWeight('600')
.marginBottom(16)
.build(),
...comments.map(comment => CommentComponent(comment))
]
})
.modifier
.build()
}Blog Post with Custom Sanitization
typescript
interface BlogPost {
title: string
content: string
excerpt: string
tags: string[]
}
const BlogPostComponent = (post: BlogPost) => {
// Editorial content - balanced security for trusted authors
const sanitizeBlogContent = (html: string) => {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: [
// Structure
'article', 'section', 'header', 'footer', 'aside',
'div', 'span', 'p', 'br',
// Headers
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
// Text formatting
'strong', 'em', 'b', 'i', 'u', 's', 'sup', 'sub',
'mark', 'del', 'ins', 'small',
// Lists
'ul', 'ol', 'li', 'dl', 'dt', 'dd',
// Links and media
'a', 'img',
// Code and quotes
'code', 'pre', 'blockquote', 'cite',
// Tables
'table', 'thead', 'tbody', 'tr', 'td', 'th',
// Misc
'hr'
],
ALLOWED_ATTR: {
// Global attributes
'*': ['class', 'id'],
// Links
'a': ['href', 'target', 'rel', 'title'],
// Images
'img': ['src', 'alt', 'width', 'height', 'title'],
// Tables
'td': ['colspan', 'rowspan'],
'th': ['colspan', 'rowspan', 'scope'],
// Code blocks
'pre': ['class'], // For syntax highlighting
'code': ['class']
},
ALLOWED_URI_REGEXP: /^https?:|^\/|^#|^mailto:/i
})
}
return VStack({
children: [
// Title
Text(post.title)
.modifier
.fontSize(32)
.fontWeight('700')
.lineHeight(1.2)
.marginBottom(16)
.build(),
// Tags
Text(`Tagged: ${post.tags.join(', ')}`)
.modifier
.fontSize(14)
.opacity(0.7)
.marginBottom(24)
.build(),
// Content with rich HTML support
Text(post.content)
.modifier
.asHTML({
customSanitizer: sanitizeBlogContent
})
.fontSize(16)
.lineHeight(1.6)
.build()
]
})
.modifier
.maxWidth(800)
.padding(32)
.backgroundColor('#FFFFFF')
.cornerRadius(12)
.shadow({ x: 0, y: 2, blur: 16, color: 'rgba(0,0,0,0.08)' })
.build()
}Advanced Examples
Email Template Preview
typescript
const EmailTemplatePreview = (templateHtml: string, variables: Record<string, string> = {}) => {
// Process email template with variable substitution
const processEmailTemplate = (html: string) => {
let processed = html
// Replace template variables
Object.entries(variables).forEach(([key, value]) => {
const regex = new RegExp(`{{\\s*${key}\\s*}}`, 'g')
processed = processed.replace(regex, value)
})
// Remove remaining template syntax
processed = processed.replace(/\{\{.*?\}\}/g, '[VARIABLE]')
// Clean server-side tags
processed = processed.replace(/<\?.*?\?>/g, '')
processed = processed.replace(/<%.*?%>/g, '')
return processed
}
return Text(templateHtml)
.modifier
.asHTML({
customSanitizer: processEmailTemplate,
allowedTags: [
// Email HTML structure
'html', 'head', 'body', 'meta', 'title', 'style',
// Email layout (tables)
'table', 'tr', 'td', 'th', 'thead', 'tbody',
// Content
'div', 'span', 'p', 'br', 'a', 'img',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'strong', 'em', 'b', 'i', 'u',
'ul', 'ol', 'li', 'hr'
],
allowedAttributes: {
// Email needs inline styles and specific attributes
'*': ['class', 'id', 'style'],
'table': ['width', 'height', 'border', 'cellspacing', 'cellpadding', 'align'],
'td': ['width', 'height', 'align', 'valign', 'colspan', 'rowspan', 'bgcolor'],
'th': ['width', 'height', 'align', 'valign', 'colspan', 'rowspan'],
'a': ['href', 'target', 'rel', 'title'],
'img': ['src', 'alt', 'width', 'height', 'border', 'align'],
'meta': ['charset', 'name', 'content', 'http-equiv']
}
})
.modifier
.fontFamily('system-ui, -apple-system, "Segoe UI", sans-serif')
.fontSize(14)
.lineHeight(1.4)
.padding(20)
.backgroundColor('#F5F5F5')
.build()
}
// Usage
const emailContent = `
<table width="600" cellspacing="0" cellpadding="0">
<tr>
<td style="padding: 20px; background-color: #ffffff;">
<h1>Hello {{firstName}}!</h1>
<p>Thank you for signing up for our newsletter.</p>
<p>Your subscription is now active.</p>
<a href="{{unsubscribeUrl}}" style="color: #666;">Unsubscribe</a>
</td>
</tr>
</table>
`
const previewComponent = EmailTemplatePreview(emailContent, {
firstName: 'John',
unsubscribeUrl: 'https://example.com/unsubscribe'
})Markdown-to-HTML Rendering
typescript
import { marked } from 'marked'
import DOMPurify from 'dompurify'
const MarkdownRenderer = (markdown: string) => {
// Convert markdown to HTML
const htmlContent = marked(markdown)
// Sanitize the generated HTML
const sanitizeMarkdown = (html: string) => {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: [
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'p', 'br', 'strong', 'em', 'code', 'pre',
'ul', 'ol', 'li', 'blockquote', 'a', 'img',
'table', 'thead', 'tbody', 'tr', 'td', 'th',
'del', 'hr'
],
ALLOWED_ATTR: {
'a': ['href', 'title'],
'img': ['src', 'alt', 'title'],
'pre': ['class'], // For code language
'code': ['class'],
'th': ['align'],
'td': ['align']
},
ALLOWED_URI_REGEXP: /^https?:|^\/|^#/i
})
}
return Text(htmlContent)
.modifier
.asHTML({
customSanitizer: sanitizeMarkdown
})
.fontSize(16)
.lineHeight(1.6)
.build()
}
// Usage example
const DocumentationPage = (markdownContent: string) => {
return VStack({
children: [
MarkdownRenderer(markdownContent)
]
})
.modifier
.maxWidth(800)
.padding(24)
.build()
}Rich Text Editor Output
typescript
interface RichTextContent {
html: string
plainText: string
wordCount: number
}
const RichTextDisplay = (content: RichTextContent) => {
// Sanitization for rich text editor output (trusted but validate)
const sanitizeEditorContent = (html: string) => {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: [
'p', 'br', 'div', 'span',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'strong', 'em', 'b', 'i', 'u', 's',
'ul', 'ol', 'li', 'blockquote',
'a', 'img', 'hr'
],
ALLOWED_ATTR: {
'*': ['class', 'style'], // Rich text editors use classes and inline styles
'a': ['href', 'target', 'rel'],
'img': ['src', 'alt', 'width', 'height', 'style'],
'span': ['style'], // For text colors, fonts, etc.
'div': ['style'],
'p': ['style']
},
ALLOWED_URI_REGEXP: /^https?:|^\/|^#|^mailto:/i,
// Allow some safe inline styles
FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed'],
FORBID_ATTR: ['onclick', 'onerror', 'onload', 'onfocus', 'onmouseover']
})
}
return VStack({
children: [
// Content
Text(content.html)
.modifier
.asHTML({
customSanitizer: sanitizeEditorContent
})
.fontSize(16)
.lineHeight(1.6)
.build(),
// Meta information
Text(`${content.wordCount} words`)
.modifier
.fontSize(12)
.opacity(0.6)
.marginTop(16)
.textAlign('right')
.build()
]
})
.modifier
.padding(20)
.backgroundColor('#FFFFFF')
.border(1, '#E5E5E7')
.cornerRadius(8)
.build()
}Performance Optimization Examples
Large Content with Virtualization
typescript
import { createSignal, createMemo } from '@tachui/core'
const LargeHTMLContentViewer = (htmlContent: string) => {
const [isExpanded, setIsExpanded] = createSignal(false)
// Memoize expensive sanitization
const sanitizedContent = createMemo(() => {
const truncated = isExpanded()
? htmlContent
: htmlContent.substring(0, 1000) + '...'
return DOMPurify.sanitize(truncated, {
ALLOWED_TAGS: ['p', 'strong', 'em', 'a', 'ul', 'li'],
ALLOWED_ATTR: { 'a': ['href'] }
})
})
return VStack({
children: [
Text(sanitizedContent())
.modifier
.asHTML({
skipSanitizer: true, // Already sanitized in memo
validateDOM: false // Skip DOM validation for performance
})
.fontSize(16)
.lineHeight(1.6)
.build(),
!isExpanded() ?
Text('Show More')
.modifier
.foregroundColor('#007AFF')
.onTap(() => setIsExpanded(true))
.padding(8)
.build()
: null
]
})
.modifier
.build()
}Cached Sanitization
typescript
const sanitizationCache = new Map<string, string>()
const CachedHTMLRenderer = (content: string, cacheKey?: string) => {
const key = cacheKey || content
let sanitizedContent: string
if (sanitizationCache.has(key)) {
sanitizedContent = sanitizationCache.get(key)!
} else {
sanitizedContent = DOMPurify.sanitize(content)
sanitizationCache.set(key, sanitizedContent)
}
return Text(sanitizedContent)
.modifier
.asHTML({
skipSanitizer: true, // Already sanitized and cached
validateDOM: false
})
.build()
}Testing Examples
Security Testing Component
typescript
const SecurityTestComponent = () => {
const testVectors = [
'<script>alert("xss")</script>',
'<img src="x" onerror="alert(1)">',
'<a href="javascript:alert(1)">Click</a>',
'<div onclick="alert(1)">Click</div>',
'<iframe src="javascript:alert(1)"></iframe>',
'<object data="javascript:alert(1)"></object>',
'<style>body{background:url("javascript:alert(1)")}</style>'
]
return VStack({
children: [
Text('Security Test Results')
.modifier
.fontSize(18)
.fontWeight('600')
.marginBottom(16)
.build(),
...testVectors.map((vector, index) =>
VStack({
children: [
Text(`Test ${index + 1}: ${vector.substring(0, 50)}...`)
.modifier
.fontSize(12)
.fontFamily('monospace')
.backgroundColor('#F8F9FA')
.padding(8)
.build(),
Text(vector)
.modifier
.asHTML()
.fontSize(14)
.padding(8)
.border(1, '#E5E5E7')
.marginBottom(12)
.build()
]
})
.modifier.build()
)
]
})
.modifier
.padding(20)
.build()
}Real-World Integration
CMS Content Renderer
typescript
interface CMSContent {
id: string
type: 'article' | 'page' | 'snippet'
content: string
metadata: {
author: string
publishedAt: string
updatedAt: string
}
}
const CMSContentRenderer = (content: CMSContent) => {
// Different sanitization rules based on content type
const getSanitizationRules = (type: CMSContent['type']) => {
switch (type) {
case 'article':
return {
ALLOWED_TAGS: [
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'p', 'br', 'strong', 'em', 'u', 'i',
'ul', 'ol', 'li', 'blockquote', 'a', 'img',
'table', 'thead', 'tbody', 'tr', 'td', 'th'
],
ALLOWED_ATTR: {
'*': ['class'],
'a': ['href', 'target', 'rel'],
'img': ['src', 'alt', 'width', 'height']
}
}
case 'page':
return {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a'],
ALLOWED_ATTR: { 'a': ['href'] }
}
case 'snippet':
return {
ALLOWED_TAGS: ['strong', 'em'],
ALLOWED_ATTR: {}
}
default:
return { ALLOWED_TAGS: [], ALLOWED_ATTR: {} }
}
}
const rules = getSanitizationRules(content.type)
return Text(content.content)
.modifier
.asHTML({
customSanitizer: (html) => DOMPurify.sanitize(html, rules)
})
.fontSize(content.type === 'article' ? 16 : 14)
.lineHeight(1.6)
.build()
}These examples demonstrate the flexibility and security features of the .asHTML() modifier across various real-world scenarios. Always remember to:
- Match sanitization to your content source - user content needs stricter rules than editorial content
- Use comprehensive libraries like DOMPurify for production applications
- Test your security measures regularly with known XSS vectors
- Consider performance implications for large content
- Follow the principle of least privilege when configuring allowed tags and attributes
For more information, see the HTML Content Security Guide.