HTM provides advanced customization through custom hyperscript functions and integration with build tools.
HTM's core strength is its ability to work with any hyperscript-compatible function.
import htm from "htm";
const htmlString = (tag, props, ...children) => {
const attrs = props ?
Object.entries(props)
.filter(([key, value]) => value !== false)
.map(([key, value]) => value === true ? key : `${key}="${value}"`)
.join(' ') : '';
const attrsStr = attrs ? ` ${attrs}` : '';
const content = children.join('');
if (children.length === 0 && isSelfClosing(tag)) {
return `<${tag}${attrsStr} />`;
}
return `<${tag}${attrsStr}>${content}</${tag}>`;
};
const isSelfClosing = (tag) =>
['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
'link', 'meta', 'param', 'source', 'track', 'wbr'].includes(tag);
const html = htm.bind(htmlString);
const markup = html`
<div class="container">
<h1>Title</h1>
<img src="photo.jpg" alt="Photo" />
</div>
`;
console.log(markup);
// <div class="container"><h1>Title</h1><img src="photo.jpg" alt="Photo" /></div>import htm from "htm";
const jsonBuilder = (tag, props, ...children) => ({
type: 'element',
tagName: tag,
attributes: props || {},
children: children.map(child =>
typeof child === 'string' ?
{ type: 'text', content: child } :
child
)
});
const html = htm.bind(jsonBuilder);
const structure = html`
<article class="post">
<h1>Blog Title</h1>
<p>Content here...</p>
</article>
`;
console.log(JSON.stringify(structure, null, 2));import htm from "htm";
import styled from "styled-components";
// Use HTM with styled-components
const styledHtml = htm.bind(styled);
const StyledButton = styledHtml`
button\`
background: \${primary};
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
&:hover \{
opacity: 0.8;
\}
\`
`;HTM can be compiled at build time using Babel plugins:
// babel.config.js
module.exports = {
plugins: [
["babel-plugin-htm", {
"pragma": "React.createElement",
"pragmaFrag": "React.Fragment"
}]
]
};
// Before transformation
const element = html`<div>Hello ${name}</div>`;
// After transformation
const element = React.createElement("div", null, "Hello ", name);// Validate template syntax at runtime
function validateHTMLTemplate(template, values = []) {
try {
const html = htm.bind((tag, props, ...children) => ({ tag, props, children }));
const result = html(template, ...values);
return { valid: true, result };
} catch (error) {
return {
valid: false,
error: error.message,
suggestion: getSuggestion(error.message)
};
}
}
function getSuggestion(errorMessage) {
if (errorMessage.includes('Unexpected')) {
return 'Check for mismatched tags or invalid syntax';
}
if (errorMessage.includes('property')) {
return 'Verify all interpolated values are properly closed';
}
return 'Review template syntax and ensure proper nesting';
}
// Usage
const result = validateHTMLTemplate`<div><span></div>`; // Mismatched tags
if (!result.valid) {
console.error('Template error:', result.error);
console.log('Suggestion:', result.suggestion);
}HTM uses internal caching for performance. You can inspect and manage caches:
import htm from "htm";
// Access internal cache (for debugging/profiling)
const html = htm.bind(h);
// Templates are cached by their static strings
const template1 = html`<div>${value1}</div>`;
const template2 = html`<div>${value2}</div>`; // Same cache entry
// Different static parts create different cache entries
const template3 = html`<span>${value1}</span>`; // New cache entryUse the mini version for smaller bundle sizes:
// Regular version (~600 bytes)
import htm from "htm";
// Mini version (~400 bytes)
import htm from "htm/mini";
// Same API, slightly different internal representation
const html = htm.bind(h);import htm from "htm";
// Define strict return types
interface VNode {
tag: string | Function;
props: Record<string, any> | null;
children: any[];
}
// Type-safe hyperscript function
const h = (
tag: string | Function,
props: Record<string, any> | null,
...children: any[]
): VNode => ({
tag, props, children
});
// Bind with proper typing
const html = htm.bind(h);
// TypeScript infers VNode | VNode[] return type
const element = html`<div>Content</div>`;
// Component with props validation
interface ButtonProps {
variant: 'primary' | 'secondary';
disabled?: boolean;
onClick: () => void;
}
const Button: React.FC<ButtonProps> = ({ variant, disabled, onClick, children }) => {
return html`
<button
class="btn btn-${variant}"
disabled=${disabled}
onClick=${onClick}
>
${children}
</button>
`;
};// Create typed template functions
type HTMLTemplate<T> = (strings: TemplateStringsArray, ...values: any[]) => T;
// Custom hyperscript with specific return type
interface CustomElement {
type: string;
attributes: Record<string, any>;
content: string;
}
const customH = (tag: string, props: any, ...children: any[]): CustomElement => ({
type: tag,
attributes: props || {},
content: children.join('')
});
const customHtml: HTMLTemplate<CustomElement> = htm.bind(customH);
// Type-safe usage
const element: CustomElement = customHtml`<div class="test">Content</div>`;// Advanced hyperscript types
type CustomHyperscript<T> = (
type: any,
props: Record<string, any> | null,
...children: any[]
) => T;
type CustomTemplate<T> = (
strings: TemplateStringsArray,
...values: any[]
) => T | T[];
// Custom element structures
interface CustomVNode {
type: string;
attributes: Record<string, any>;
content: string;
}
interface CustomElement {
tag: string;
props: Record<string, any>;
children: any[];
}
// Validation result types
interface ValidationResult {
valid: boolean;
result?: any;
error?: string;
suggestion?: string;
}