Skip to main content

Nodality

A user interface is two arrays: what exists, and what is done to it.

Both are plain data – so the pipeline that builds your page is inspectable, diffable, and executable at build time and in the browser.

v1.2.7 · zero runtime dependencies · MIT

The whole idea, in two arrays

What exists

const elements = [
{ id: "hero", type: "h1", text: "Ship it" },
{ id: "sub", type: "p", text: "Two arrays." },
];

What is done to it

const nodes = [
{
op: { name: "gradient", gradient: "linear-gradient(#0f53a8, #111)" },
target: ["hero"],
},
];
new Des().nodes(nodes).add(elements).set({ mount: "#app" });

No JSX, no class strings, no build step required. The same pair renders in the browser or prerenders to static HTML – and because it is data, you can diff two builds and see exactly what changed.

And what it compiles to

new Text("Ship it").set({
id: "hero",
size: "S1",
font: "Arial",
gradient: {
op: { name: "gradient", gradient: "linear-gradient(#0f53a8, #111)" },
target: ["hero"],
},
});

The pipeline does not disappear into the framework. It compiles to ordinary component code, with every default it applied written out – the size and font nobody typed, and the node resolved onto the element it targeted. Nodality prints that alongside the page, so you can read what the build decided, copy it out, or drop the declarative form entirely and keep the code.

A whole navigation graph is one node

Three views transitioning into one another, each hop running a different shader effect
{
op: "morph", effect: "t-vhs",
duration: 620, back: true,

chain: [
{ from: "home",
to: { Work: "work", Contact: "contact" } },

{ from: "work", effect: "t-split",
to: { Aurora: "aurora" } },

{ from: "aurora", effect: "t-bloom",
to: { Contact: "contact" } },
],
}

That is the entire source of the transitions above. The entries are edges, not keyframes – edge two is reachable from the state edge one lands on, so a landed view becomes a source in its turn. Settings on the node are defaults each edge may override, and back unwinds the path the user actually took.

Effects run on live DOM, and the DOM stays clickable

A warp effect following the pointer across a bar of links, then a link being clicked through it
const nodes = [
{ op: "flow", target: ["bar"],
by: "mouse", amount: 0.32 },

{ op: "dither", target: ["bar"],
levels: 10, size: 2, amount: 0.14 },
];

Two raster nodes aimed at one element compose into a single shader pass, in array order. The source is your live DOM, not an image you exported – and the links underneath still work: pointer input is inverted through the compiled transformation and re-dispatched, so a click lands where the user aimed.

Install

npm create nodality@latest my-app

Or load it straight from a CDN, with no build step at all:

<div id="app"></div>
<script type="module">
import { Des } from "https://www.unpkg.com/nodality@1.2.7/dist/index.esm.js";

new Des()
.nodes([])
.add([{ type: "h1", text: "Hello" }])
.set({ mount: "#app" });
</script>

What else it does

Static site generation

Prerender every route to indexable HTML, across locales, then hand off to the runtime. A crawler that runs no JavaScript sees the whole page.

Prerendering →

Deterministic builds

The same pair always compiles to the same bytes. Nodes that touch different elements can be written in any order; when two contend, the first declared wins.

The model →

Made for agents

One node publishes your page's navigation, forms and readable content as tools an agent can call – derived from the same arrays that render it, so it cannot drift.

Agent surface →

Written by a model, checked before it renders

Because the target is data bounded by a schema rather than free-form code, a generator's output either validates or it does not – and it costs roughly half the tokens of the equivalent React and Tailwind. A tool server exposes the vocabulary, the validator and a renderer over MCP, so a model can check its own work before you see it.