HTMP is designed to be intuitive. If you know HTML and JavaScript, you already know how to use it. Below is the complete API reference for v1.2.2.
HTMP is built on a simple, declarative philosophy that separates UI structure from data and logic:
{{ }}, :, @, :for). It is what the user sees.setProgram(). These handle events (like clicks or inputs) and update the Proxy. No magic hooks, just standard JS functions.The templateString passed to HTMP can be created in three ways, depending on your project structure and developer experience (DX) preferences:
Define your HTML directly as a JavaScript string. This is useful when separating UI logic into .js files.
Con: IDEs (like VS Code) will not provide syntax highlighting or HTML validity checking inside standard strings.
const pattern = `<div id="app"><h1>{{ title }}</h1></div>`;
const app = new HTMP('app', pattern);
Write HTML directly in your index.html inside a <template> tag. This is the recommended approach for Landing Pages or static HTML files.
Pro: IDEs provide full syntax highlighting, autocomplete, and HTML validity checking. It also prevents FOUC (Flash of Unstyled Content) since template contents are not rendered until HTMP mounts them.
<!-- In index.html -->
<template id="page-template">
<div id="app">
<h1>{{ title }}</h1>
</div>
</template>
// In app.js
const pattern = document.getElementById('page-template').innerHTML;
const app = new HTMP('app', pattern);
Take over existing HTML already rendered by the server (PHP, Laravel, etc.). You must use .replace('id="rootId"', '') to remove the original ID from the string, preventing duplicate IDs in the DOM when HTMP clones and mounts it.
// HTML is already in the body, rendered by backend
// <div id="app"><h1>{{ title }}</h1></div>
const rootEl = document.getElementById('app');
// Clone outerHTML and strip the ID to avoid duplication
const pattern = rootEl.outerHTML.replace('id="app"', '');
const app = new HTMP('app', pattern);
app.mount('app'); // Mounts and replaces the original SSR element
HTMP requires zero build steps. The recommended way to use it in production is via the jsDelivr CDN (ESM). This also allows you to monitor download statistics for the library.
Or import it directly inside your JS module:
import { HTMP } from 'https://cdn.jsdelivr.net/npm/htm-projection@v1.1.0/+esm';
For TypeScript or bundled projects (Vite/Webpack), you can install it via NPM:
npm install htm-projection
Creates a new HTMP instance, parses the HTML template, and builds the internal Registry Map.
TypeScript Support: You can pass a TypeScript interface to the class for full Type-Safety.
import { HTMP } from 'htm-projection';
interface AppState {
title: string;
count: number;
}
// Initialize with Type
const app = new HTMP<AppState>('app', templateString);
HTMP uses a direct state manipulation pattern. State changes are intercepted by a root Proxy, which automatically triggers the render pipeline.
Registers initial state values. Accepts an Object Literal.
app.setProxy({
title: "Todo App",
todos: []
});
The app.proxy object is a live reference. Because it is attached to the app instance, it can be accessed and mutated from anywhere within your JavaScript module, not just inside setProgram.
// Updating a primitive from anywhere in your JS file
app.proxy.title = "New Title";
// Updating an array immutably
app.proxy.todos = [...app.proxy.todos, { id: 1, text: "New Task" }];
// Deep Reactivity (Modifying nested properties directly)
app.proxy.todos[0].done = true;
Registers functions that can be triggered by @event bindings in the template.
Accepts an Object Literal containing multiple functions. These functions are bound to the app.programs object.
app.setProgram({
handleInput: (e: Event) => {
app.proxy.title = (e.target as HTMLInputElement).value;
},
addTodo: () => {
// Logic here
}
});
Because handlers are stored in app.programs, you can trigger them programmatically from anywhere in your module (e.g., from a WebSocket event or a third-party script).
// Manually calling a registered handler
app.programs.addTodo();
// Useful for external integrations like WebSockets
socket.on('new_notification', (data) => {
app.proxy.notificationCount++;
// You can also call programs if needed
app.programs.showToast(data.message);
});
<h1>{{ title }}</h1>
<p>{{ todos.length === 0 ? 'No tasks' : 'Tasks available' }}</p>
<button :disabled="isLocked">Save</button>
<div :class="isActive ? 'active' : 'hidden'"></div>
<button @click="addTodo()">Add</button>
<input @input="handleInput(e)" />
<ul>
<li :for="todo in todos">
{{ todo.text }}
</li>
</ul>
HTMP does not include a custom JavaScript animation API (like GSAP or Framer Motion). Instead, it embraces native Web APIs. Because HTMP only surgically patches the DOM (changing class or nodeValue), you get smooth 60FPS animations by combining HTMP reactivity with pure CSS.
When HTMP updates an attribute like :class, the browser automatically triggers CSS transitions. No JavaScript animation logic needed.
/* CSS */
.button { transition: background-color 0.3s ease; }
.active { background-color: #1267F5; }
<!-- HTML -->
<button :class="isActive ? 'active' : ''">Toggle</button>
For complex UI changes (like swapping a code box or transitioning between SPA pages), wrap your Proxy update inside the native document.startViewTransition() function. HTMP will patch the DOM, and the browser will handle the snapshot morphing.
// JavaScript
app.setProgram({
switchTab: (tabName) => {
// 1. Tell browser to start transition snapshot
document.startViewTransition(() => {
// 2. HTMP updates the Proxy and patches DOM
app.proxy.activeTab = tabName;
app.proxy.activeCode = examples[tabName].code;
});
}
});
To animate specific elements (e.g., a sliding code box instead of the whole page), assign a unique view-transition-name in your CSS. The browser will isolate the animation to that specific element.
/* CSS: Target specific element, not root */
#active-code-box {
view-transition-name: code-slide;
}
/* Define slide-in/slide-out animation */
::view-transition-old(code-slide) {
animation: slide-out-left 0.3s ease-in forwards;
}
::view-transition-new(code-slide) {
animation: slide-in-right 0.3s ease-out forwards;
}
Why this approach? The UX Designer owns the visual effects (CSS), and the Developer owns the data flow (Proxy). HTMP stays at 3.9 KB, and the browser's native C++ engine handles the heavy lifting for 60FPS rendering.
app.onMount(() => {
console.log('Component is mounted.');
});
app.onUnmount(() => {
console.log('Component hidden. State preserved.');
});
app.onDestroy(() => {
console.log('Component destroyed completely.');
});
app.mount(); // Mounts to rootId
app.mount('new-root'); // Mounts to different container
app.unmount(); // Detaches from DOM, state remains in memory
app.remount(); // attach back to DOM with state remains
app.destroy(); // Obliterates component, clears memory