JSX-like syntax using tagged template literals for Virtual DOM without transpilation
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
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;
}