ivi: a declarative web UI library measured in kilobytes
Lighweight Embeddable Web UI Library
At a glance
- What is it?
- ivi is a small declarative UI library where a component is a function from state to a tree of nodes, with a tagged template syntax that keeps the markup in JavaScript and a compiler written in Rust that precompiles templates for size and cold start. The README claims the basic example, including the whole runtime, is 2.7 kilobytes.
- Who is it for?
- ivi suits a developer who wants a reactive UI without a framework's weight, or who is embedding a UI into a page that already loads a lot of JavaScript, since the whole runtime for the basic example is a couple of kilobytes and the library can be embedded with a custom scheduler.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A component is a function from state to UI
ivi describes itself as a lightweight embeddable declarative web UI library, and the whole model fits on one line: f(state) returns UI.
The example in the README shows what that means in code.
import { createRoot, update, component, useState, html } from "ivi";
const Example = component((c) => {
const [count, setCount] = useState(c, 0);
const inc = () => {
setCount(count() + 1);
};
return () => html`
<div class="app">
<div>${count()}</div>
<button @click=${inc}>Increment</button>
</div>
`;
});
update(
createRoot(document.body),
Example(),
);A component takes a context object and returns a function that renders the tree. State is a getter rather than a value, so calling it inside the template registers a dependency, and the setter invalidates only what depends on it. The template is a tagged function that returns a template object rather than DOM nodes, which is what makes precompilation possible.
The rendering model is therefore the same shape as any fine grained reactive system, but implemented without a virtual DOM and without a diffing step, since the tree it builds is a tree of real DOM nodes.
The README also links a TodoMVC implementation and a set of examples from the React learning material rewritten with the ivi API, the latter being the fastest way to see how much the two models differ.
The size claim, and what it includes
The README states that the precompiled example above is 2.7 kilobytes minified and brotli compressed, and that this includes the entire runtime for declarative UI rendering.
That inclusion is the important part of the claim. A small example usually means small output because the framework is tree shaken away or loaded separately. Here the runtime, the reactivity and the template engine are all in that figure, which is what makes it comparable with a hand written vanilla JavaScript application.
The other headline items in the feature list line up with that goal. Precompiled templates are optimized for code size and cold start performance, there is a small memory footprint, and the library is embeddable, which the README ties to the custom scheduler feature.
Embeddability is the interesting one for a library of this kind. A custom scheduler means you can decide when and how updates are flushed, which is what you need when the UI is one part of a larger page, a canvas based application or a system with its own frame loop. A library that assumes it owns the requestAnimationFrame callback is not embeddable in that sense.
So the pitch is a small, reactive, DOM based UI layer that you can drop into an existing page without taking over the whole runtime.
The template language
The template language is described as HTML-like with additional syntax for DOM properties, events and whitespace removal, and the two template functions are split by element type.
The html function creates a template with HTML element nodes, and the svg function creates a template with SVG element nodes. That split matters because the two element types behave differently in the DOM, and a single template function would have to guess.
The extra syntax beyond HTML is where the library earns its name as a template compiler rather than a tagged string function. There is a section on element properties that distinguishes attributes, which are set with setAttribute, from properties, which are assigned to the node, because for a controlled input the difference is the difference between the field working and the field being read only. There are sections for styles, for events, for directives, for conditionals, for arrays and for dynamic lists, plus rules for multiple root nodes, for childless elements and for whitespace.
The whitespace rules are worth calling out, because they are the usual source of size surprises in HTML-like template languages. A compiler that keeps every newline and indentation between elements produces output that a browser ignores but a bundler cannot drop, which is why a library that claims 2.7 kilobytes has to have an answer for whitespace.
The event syntax in the example is an at sign followed by the event name and an interpolated handler, which is a compact way of attaching a listener that the compiler can then hoist, an optimisation the README covers separately under event handlers hoisting.
The API surface
The table of contents lists the whole API, and it is short enough to enumerate.
The root API has createRoot, which attaches a tree to a parent element, update to render into it, unmount to tear it down, dirtyCheck to flush pending updates, and defineRoot for a root with an invalidation callback. There is a distinction in the documentation between a stateful tree and a stateless one, and between opaque types and the rest, which is the library's way of separating its internal representation from what you touch.
Components have component for defining one, getProps, invalidate to force an update, and useUnmount for a teardown hook. State has useState, useReducer and useMemo, each taking a comparison function where it makes sense.
Side effects have useEffect, useAnimationFrameEffect and useIdleEffect, the last two for the cases where a plain effect is not the right schedule. Lists have a List function taking entries, a key function and a render function, and there is a context function for context propagation.
There is a set of DOM utilities for the awkward cases, eventDispatcher, findDOMNode, containsDOMElement and hasDOMElement, and a family of equality functions, preventUpdates, strictEq, shallowEq and shallowEqArray, which you pass to the component and hook signatures to control when an update is considered a change.
That last group is what tells you the library is opinionated in a particular way. Rather than always doing the most precise comparison, you choose the comparison, and the library uses it consistently.
Dirty checking and the update order
The advanced section explains how updates actually happen, and it is the part to read before you rely on the behaviour.
Component invalidation and dirty checking is the core mechanism. When a state setter runs, the components that read that state are invalidated, and the next update pass re-renders them. The update function then walks the tree and applies only what changed, and dirtyCheck is the explicit way to flush that work.
Right to left updates is the second topic, and it is the one with practical consequences. Updates propagate from the invalidation towards the root, which means a change deep in the tree can cause a re-render of everything above it, and the ordering affects what a parent sees when a child updates in the same pass.
The documentation also covers template call-site unique identity, which is how the compiler distinguishes two identical templates used in different places, forcing component updates for the cases where you need to bypass the normal invalidation path, and event handler hoisting, where the compiler lifts handler functions out of the render function so that they are created once rather than on every render.
The internal data structures section describes the UI tree and the template representations, and the template optimisations section explains what the precompiler does to the output.
Precompilation, the Rust compiler and the bundler plugins
ivi templates work without any precompilation, but the README says precompilation is highly recommended for performance and code size.
The compiler is written in Rust and uses the oxc library for JavaScript parsing, described as a blazingly fast template compiler. The repository carries the Rust workspace with a crates directory and the compiler package, and the Cargo manifest lists a long set of oxc crates for the parser, the AST, the semantic analysis, code generation and diagnostics, plus the Node-API bindings that expose the compiler to the JavaScript toolchain.
The bundler integration is what makes precompilation practical, with separate packages for Vite, Rollup and Rolldown.
// vite.config.mjs
import { defineConfig } from "vite";
import { oveo } from "@oveo/vite";
import { ivi } from "@ivi/vite-plugin";
export default defineConfig({
plugins: [
ivi({
oveo: true,
}),
oveo({
hoist: true,
dedupe: true,
externs: {
import: ["ivi/oveo.json"],
},
}),
],
});Two plugins appear in that configuration. The ivi plugin does the template precompilation, and the oveo plugin does the hoisting and dedupe optimisations, which the README attributes to the separate oveo JavaScript optimiser. The externs entry tells oveo about an import specifier it should not touch.
The Rollup configuration is the same shape with the Rollup specific packages, and the README notes that a Vite setup with Rolldown integration should use the Rolldown plugin instead.
The repository, the build and the state of the project
This is a polyglot repository, and the layout shows it.
There is a Rust workspace with the crates directory and the compiler package, a packages directory holding the published npm packages, the main ivi package and the tests workspace, plus a docs directory, a justfile for task running, a Cargo manifest and lock file, a bun lock file, a TypeScript configuration set with project references, and a rust toolchain file.
The JavaScript side uses bun for installation and for the runtime tests, with a test task that runs the runtime test directory, and a TypeScript build with project references for the incremental compile. The npm packages are published individually, with separate publish tasks for the Rolldown, Rollup and Vite plugins and for the core package, and each publish step checks the version against the registry first so that a repeat publish is skipped.
The Rust release profile is tuned for size and speed, with optimisation level three, a single codegen unit, fat link time optimisation, abort on panic and symbol stripping, and there is a separate coverage profile with debug assertions and overflow checks enabled, which is the right way to find the undefined behaviour that a stripped release build would hide.
On maintenance, the last push was on 2026-10-15 and the repository has no GitHub releases, so the npm packages are the release channel. The licence is MIT, with the LICENSE file at the root.
The project is small and young, and the README is candid about the trade: you get a very small, very fast UI layer, and in exchange you are outside the React ecosystem, with the migration guide being a rewrite rather than a port.
Editorial conclusion
ivi suits a developer who wants a reactive UI without a framework's weight, or who is embedding a UI into a page that already loads a lot of JavaScript, since the whole runtime for the basic example is a couple of kilobytes and the library can be embedded with a custom scheduler. It is the wrong choice for a team that wants the React ecosystem, since the migration guide is a rewrite rather than a port, and the wrong choice if you need the mature ecosystem of components and tooling around a view layer. Verify first that your bundler is one of the three with a plugin, or accept the unoptimised path, read the dirty checking section before you rely on fine grained updates, and test the precompiled path early, because the compiler is the part that is most likely to surprise you.
Frequently asked questions
What is ivi?
ivi is a lightweight, embeddable declarative web UI library where a component is a function from state to UI, using a tagged template syntax that returns a template rather than DOM nodes, so the templates can be precompiled.
How small is ivi really?
The README states that the precompiled basic example is 2.7 kilobytes minified and brotli compressed, and that this includes the entire runtime for declarative UI rendering.
Do I need precompilation to use ivi?
No, the README says ivi templates work without any precompilation, but precompilation is highly recommended because the precompiled templates are optimized for code size and cold start performance.
Which bundlers does ivi support?
There are separate plugin packages for Vite, Rollup and Rolldown, and the README notes that a Vite setup with Rolldown integration should use the Rolldown plugin.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/localvoid-ivi)Was this useful?