Introduction
VeGen is a compiler for tiny, efficient, updatable TypeScript HTML templates. A lower-level, less meaty alternative to view libraries like React.
What Is It?
VeGen is a compiler that takes HTML templates and generates TypeScript code (with types!) that builds the HTML and allows you to efficiently update it.
Instead of DOM diffing like React, VeGen compiles templates into small, easy to understand TypeScript objects that directly track any DOM nodes that may change.
The resulting TypeScript code contains a tiny library and is dependency-free, meaning it packs down very small. The examples app has ~4.5 kB of compressed Javascript in total.
Example
Here’s a simple counter example:
counter.vg:
<view name="Counter">
<div>
<h1>Counter example</h1>
<div>
<button onclick={clickHandler}>Clicked {count | numberToString} times</button>
</div>
</div>
</view>
main.ts:
import { Counter, run } from "./counter.ts";
const root = document.querySelector<HTMLDivElement>("#app")!;
root.append(
run(Counter, (update) => ({
clickHandler: () => {
update((s) => ({ ...s, count: s.count + 1 }));
},
count: 0,
}))
);
This generates a Counter function and CounterInput type, along with a run helper for managing component state.
The TypeScript generated (plus some additional comments) is:
export type CounterInput = {
clickHandler: (this: GlobalEventHandlers, ev: PointerEvent) => any;
count: number;
};
export function Counter(input: CounterInput): ViewState<CounterInput> {
// 't' is a helper for text nodes
// 'h' is a helper for DOM nodes
// Build the initial DOM:
const node0 = t(numberToString(input.count));
const node1 = h("button", { onclick: input.clickHandler }, [
t("Clicked "),
node0,
t(" times"),
]);
const root = h("div", {}, [
h("h1", {}, [t("Counter example")]),
h("div", {}, [node1]),
]);
// maintain state
let currentInput = input;
return {
// the root DOM element
root,
// the function to update the view
update(input) {
if (input.clickHandler !== currentInput.clickHandler) {
node1["onclick"] = input.clickHandler;
}
if (input.count !== currentInput.count) {
node0.textContent = numberToString(input.count);
}
currentInput = input;
},
};
}
Who Is This For?
Why Would You Want to Use It?
- Generates a very small amount of dependency-free TypeScript.
- Generates TypeScript types for you.
- Easy to embed into other frameworks and libraries - views are just a simple DOM
Elementandupdatefunction. - Updates views efficiently (see ‘Performance’).
- You like to understand things end to end - easy to understand the generated output.
- You like to work at a lower level, with very little abstraction in-between you and the DOM API.
Why Wouldn’t You Want to Use It?
- VeGen is in an early development stage and lacks tools and conveniences for larger projects.
- Simpler than other tools, may be too restrictive for your needs.
- Adds another compile step, and some people hate that!
- Templates are not JavaScript, but a bespoke templating language (but it’s pretty simple).
Installation
Cargo
cargo install vegen
MacOS
brew install kmahoney/tap/vegen
Linux Binary
Available on the releases page.
Usage
CLI
Provide the vegen CLI command with .vg template files. Every view in every template will be compiled into TypeScript functions. Views can reference other views, including in other files.
A .vg template is a XML-like template that defines a series of views and can use several special forms. Each file consists of a series of view elements, e.g.
<view name="SimpleExample1">
<div>view content</div>
</view>
<view name="SimpleExample2">
<div>view content</div>
</view>
which will generate the TypeScript functions Example1, Example2 and their corresponding input types Example1Input, Example2Input.
VSCode Extension
An extension with syntax highlighting, language server integration, and viewable types is available on the releases page.

Reference
Expressions
VeGen supports expressions within {} bindings, including variables, function calls, pipes, and string templates.
Variables and Property Access
Variables can be bound using simple names or dotted property paths:
<view name="VariableExample">
<div>
<h1>Welcome {user.name}!</h1>
<p>Age: {user.age}</p>
<p>Location: {user.address.city}, {user.address.country}</p>
</div>
</view>
Function Calls
Expressions can include function calls with arguments:
<view name="FunctionCallExample">
<div>
<div>Count: {numberToString(count)}</div>
<div>Price: {currency(amount, "USD")}</div>
</div>
</view>
They are useful for creating closures, such as binding event handlers in for loops:
<view name="ClosureExample">
<div>
<for seq={items} as="item">
<button onclick={clickItem(item.id)}>{item.name}</button>
</for>
</div>
</view>
where clickItem is (id) => (event) => void.
They are also useful for displaying data derived from the view inputs, but in this case you should make sure the output only changes when the inputs change, i.e. they’re referentially transparent. Functions will be re-run when they are re-bound or their arguments change.
Built-in functions include:
boolean<T>(boolean, T, T) -> TnumberToString(number) -> stringlookup<T>(dict, key, default: T) -> T
Pipe Operations
Use the pipe operator | to chain transformations:
<view name="PipeExample">
<div>
<div class="display">Count: { count | numberToString }</div>
<div>Status: { status | toUpperCase }</div>
</div>
</view>
String Templates
Create dynamic strings with interpolation:
<view name="StringTemplateExample">
<div class="popup {boolean(visible, "show", "hide")}">
<p>Hello!</p>
</div>
</view>
Special Forms
VeGen provides several special forms for control flow and dynamic content:
Conditional Rendering
<if condition={showHeader}>
<then>
<h1>Welcome!</h1>
</then>
<else>
<p>Please log in</p>
</else>
</if>
This will conditionally show content and infer showHeader to be a boolean. The <else> block is optional.
Loops
<ul>
<for seq={todos} as="todo">
<li>Title: {todo.title}</li>
</for>
</ul>
This will loop through todos, introducing each element as the variable todo, and infer todos to be {title: string}[].
Switch
Render one of several branches based on a discriminant “type” field on a value.
<switch on={example}>
<case name="a">
<div>{a.foo}</div>
</case>
<case name="b">
<div>{b.bar}</div>
</case>
<case name="c">
<div>{c.baz | numberToString}</div>
</case>
</switch>
- The
onexpression must be a discriminated union with a string literal tag in atypefield. For example:
type Example =
| { type: "a"; foo: string }
| { type: "b"; bar: string }
| { type: "c"; baz: number };
Component Composition
VeGen supports composing views as reusable components within a template. Define multiple views in the same file, then use them as custom elements in parent views:
<view name="Button">
<button onclick={onClick} class={class}>{text}</button>
</view>
<view name="UserCard">
<div class="card">
<h3>{user.name}</h3>
<p>Age: {user.age | numberToString}</p>
<Button onClick={onEdit} class="btn-primary" text="Edit" />
</div>
</view>
<view name="UserList">
<div class="user-list">
<for seq={users} as="user">
<UserCard user={user} onEdit={editHandler(user.id)} />
</for>
</div>
</view>
Sharing Views Across Files
Use <require src="..." /> at the top level of a template to pull in views defined in another .vg file. Required files are resolved relative to the current template, and all referenced views must be explicitly required. For example:
<require src="./components/header.vg" />
<view name="Page">
<Header title={title} />
</view>
Dynamically Using Components
You can also use a dynamically changing view with the ‘use’ form, as shown:
<use view={myView} attr={example} />
Where myView is a View<T> and T is the attribute object type.
The run Helper
The generated TypeScript includes a run helper function that manages component state and provides reactive updates. It takes two parameters:
- A view function (generated from your
.vgtemplate) - A builder function that receives an
updatefunction for state management
run(viewFunction, (update) => {
// Return the initial input state
return {
// ... your state properties
// ... event handlers that can call get() and set()
};
});
The update(currentState => newState) function updates the state and triggers a re-render of only the changed parts of the DOM.
Performance
VeGen generates highly efficient update code by tracking only the DOM nodes that may change in the ViewState. When you call the update function with new input, VeGen directly updates only the parts of the DOM that have actually changed.
However, to get the best performance, you need to be careful about reusing values from the previous state to avoid unnecessary re-renders. This is especially important with derived state.
Avoiding Unnecessary Re-Renders
The key principle is: if the data hasn’t changed, pass the same object reference. VeGen can then skip updating that part of the DOM entirely.
This is particularly important for:
- Arrays used in
<for>loops - Objects used in conditional rendering
- Any derived state created with
map,filter, or similar operations
When you pass the same object reference, VeGen’s update functions can quickly determine that no DOM changes are needed for that section.