HTM provides a streamlined integration with React through a pre-configured
htmlcreateElementimport { html } from "htm/react";For TypeScript:
import { html } from "htm/react";declare const html: (strings: TemplateStringsArray, ...values: any[]) => React.ReactElement;Pre-bound HTM function configured for React's
createElementReturns:
React.ReactElementUsage Examples:
import React from "react";
import { createRoot } from "react-dom/client";
import { html } from "htm/react";
// Basic element
const element = html`<div className="container">Hello World</div>`;
// Dynamic content
const name = "Alice";
const greeting = html`<h1>Hello, ${name}!</h1>`;
// Event handlers
const handleClick = () => console.log('Clicked!');
const button = html`<button onClick=${handleClick}>Click me</button>`;
// Render to DOM
const root = createRoot(document.getElementById('root'));
root.render(greeting);import React, { useState, useEffect } from "react";
import { html } from "htm/react";
// Simple functional component
const Welcome = ({ name }) => html`
<div className="welcome">
<h1>Welcome, ${name}!</h1>
</div>
`;
// Component with hooks
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return html`
<div className="counter">
<p>Count: ${count}</p>
<button onClick=${() => setCount(count + 1)}>+</button>
<button onClick=${() => setCount(count - 1)}>-</button>
</div>
`;
};
// Usage
const App = () => html`
<div>
<${Welcome} name="Alice" />
<${Counter} />
</div>
`;import React, { Component } from "react";
import { html } from "htm/react";
class Timer extends Component {
constructor(props) {
super(props);
this.state = { seconds: 0 };
}
componentDidMount() {
this.interval = setInterval(() => {
this.setState({ seconds: this.state.seconds + 1 });
}, 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
return html`
<div className="timer">
<h2>Timer: ${this.state.seconds}s</h2>
<button onClick=${() => this.setState({ seconds: 0 })}>
Reset
</button>
</div>
`;
}
}HTM automatically handles React-specific attribute names:
// Use React attribute names
const element = html`
<div
className="container"
htmlFor="input-id"
onClick=${handleClick}
style=${{ backgroundColor: 'blue', fontSize: '16px' }}
>
<label htmlFor="input-id">Label</label>
<input id="input-id" onChange=${handleChange} />
</div>
`;React's synthetic event system works seamlessly:
const handleSubmit = (e) => {
e.preventDefault();
console.log('Form submitted');
};
const handleChange = (e) => {
console.log('Input value:', e.target.value);
};
const form = html`
<form onSubmit=${handleSubmit}>
<input
type="text"
onChange=${handleChange}
placeholder="Enter text..."
/>
<button type="submit">Submit</button>
</form>
`;React refs work with HTM:
import React, { useRef, useEffect } from "react";
import { html } from "htm/react";
const FocusInput = () => {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return html`
<div>
<input
ref=${inputRef}
type="text"
placeholder="Auto-focused input"
/>
</div>
`;
};React Context works with HTM components:
import React, { createContext, useContext } from "react";
import { html } from "htm/react";
const ThemeContext = createContext();
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = React.useState('light');
return html`
<${ThemeContext.Provider} value=${{ theme, setTheme }}>
${children}
</${ThemeContext.Provider}>
`;
};
const ThemedButton = ({ children }) => {
const { theme, setTheme } = useContext(ThemeContext);
return html`
<button
className="btn btn-${theme}"
onClick=${() => setTheme(theme === 'light' ? 'dark' : 'light')}
>
${children}
</button>
`;
};import React, { useState, useCallback } from "react";
import { createRoot } from "react-dom/client";
import { html } from "htm/react";
const TodoItem = ({ todo, onToggle, onRemove }) => html`
<li className=${todo.completed ? 'completed' : ''}>
<input
type="checkbox"
checked=${todo.completed}
onChange=${() => onToggle(todo.id)}
/>
<span className="todo-text">${todo.text}</span>
<button
className="remove-btn"
onClick=${() => onRemove(todo.id)}
>
×
</button>
</li>
`;
const TodoApp = () => {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
const addTodo = useCallback(() => {
if (input.trim()) {
setTodos(prev => [...prev, {
id: Date.now(),
text: input,
completed: false
}]);
setInput('');
}
}, [input]);
const toggleTodo = useCallback((id) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
}, []);
const removeTodo = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
const handleKeyPress = (e) => {
if (e.key === 'Enter') {
addTodo();
}
};
return html`
<div className="todo-app">
<h1>React Todo App</h1>
<div className="input-section">
<input
type="text"
value=${input}
onChange=${(e) => setInput(e.target.value)}
onKeyPress=${handleKeyPress}
placeholder="Add a todo..."
/>
<button onClick=${addTodo}>Add</button>
</div>
<ul className="todo-list">
${todos.map(todo => html`
<${TodoItem}
key=${todo.id}
todo=${todo}
onToggle=${toggleTodo}
onRemove=${removeTodo}
/>
`)}
</ul>
<div className="stats">
Total: ${todos.length},
Completed: ${todos.filter(t => t.completed).length}
</div>
</div>
`;
};
// Render app
const root = createRoot(document.getElementById('root'));
root.render(html`<${TodoApp} />`);import React, { useState } from "react";
import { html } from "htm/react";
const ContactForm = () => {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const [errors, setErrors] = useState({});
const validateForm = () => {
const newErrors = {};
if (!formData.name.trim()) {
newErrors.name = 'Name is required';
}
if (!formData.email.includes('@')) {
newErrors.email = 'Valid email is required';
}
if (!formData.message.trim()) {
newErrors.message = 'Message is required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validateForm()) {
console.log('Form submitted:', formData);
// Reset form
setFormData({ name: '', email: '', message: '' });
}
};
const handleChange = (field) => (e) => {
setFormData(prev => ({
...prev,
[field]: e.target.value
}));
// Clear error when user starts typing
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: '' }));
}
};
return html`
<form className="contact-form" onSubmit=${handleSubmit}>
<h2>Contact Us</h2>
<div className="form-group">
<label htmlFor="name">Name:</label>
<input
id="name"
type="text"
value=${formData.name}
onChange=${handleChange('name')}
className=${errors.name ? 'error' : ''}
/>
${errors.name && html`<span className="error-text">${errors.name}</span>`}
</div>
<div className="form-group">
<label htmlFor="email">Email:</label>
<input
id="email"
type="email"
value=${formData.email}
onChange=${handleChange('email')}
className=${errors.email ? 'error' : ''}
/>
${errors.email && html`<span className="error-text">${errors.email}</span>`}
</div>
<div className="form-group">
<label htmlFor="message">Message:</label>
<textarea
id="message"
value=${formData.message}
onChange=${handleChange('message')}
className=${errors.message ? 'error' : ''}
rows="4"
/>
${errors.message && html`<span className="error-text">${errors.message}</span>`}
</div>
<button type="submit">Send Message</button>
</form>
`;
};import { BrowserRouter, Route, Routes, Link } from "react-router-dom";
import { html } from "htm/react";
const Home = () => html`<h1>Home Page</h1>`;
const About = () => html`<h1>About Page</h1>`;
const Contact = () => html`<h1>Contact Page</h1>`;
const App = () => html`
<${BrowserRouter}>
<nav>
<${Link} to="/">Home<//>
<${Link} to="/about">About<//>
<${Link} to="/contact">Contact<//>
</nav>
<${Routes}>
<${Route} path="/" element=${html`<${Home} />`} />
<${Route} path="/about" element=${html`<${About} />`} />
<${Route} path="/contact" element=${html`<${Contact} />`} />
<//>
<//>
`;import { useReducer } from "react";
import { html } from "htm/react";
const todoReducer = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return [...state, { id: Date.now(), text: action.text, done: false }];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.id ? { ...todo, done: !todo.done } : todo
);
case 'REMOVE_TODO':
return state.filter(todo => todo.id !== action.id);
default:
return state;
}
};
const TodoApp = () => {
const [todos, dispatch] = useReducer(todoReducer, []);
return html`
<div>
<button onClick=${() => dispatch({ type: 'ADD_TODO', text: 'New todo' })}>
Add Todo
</button>
<ul>
${todos.map(todo => html`
<li key=${todo.id}>
<span style=${{ textDecoration: todo.done ? 'line-through' : 'none' }}>
${todo.text}
</span>
<button onClick=${() => dispatch({ type: 'TOGGLE_TODO', id: todo.id })}>
Toggle
</button>
<button onClick=${() => dispatch({ type: 'REMOVE_TODO', id: todo.id })}>
Remove
</button>
</li>
`)}
</ul>
</div>
`;
};// React HTML template function
declare const html: (strings: TemplateStringsArray, ...values: any[]) => React.ReactElement;
// React element type (from React)
interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
type: T;
props: P;
key: Key | null;
}
// Component types
type FunctionComponent<P = {}> = (props: P) => ReactElement | null;
type ComponentClass<P = {}, S = ComponentState> = new (props: P, context?: any) => Component<P, S>;
// Event handler types
type MouseEventHandler<T = Element> = (event: MouseEvent<T>) => void;
type ChangeEventHandler<T = Element> = (event: ChangeEvent<T>) => void;
type FormEventHandler<T = Element> = (event: FormEvent<T>) => void;
type KeyboardEventHandler<T = Element> = (event: KeyboardEvent<T>) => void;
// Ref types
type RefObject<T> = { readonly current: T | null };
type MutableRefObject<T> = { current: T };