Reactive properties and formulas
Any property on any object in Formo can be bound to a formula instead of a fixed value - and a formula isn't a proprietary mini-language, it's real JavaScript/TypeScript, parsed with Acorn and the same @sveltejs/acorn-typescript plugin that gives you actual TS syntax, not a look-alike. The mental model is a spreadsheet cell: a formula reads other values, and recalculates automatically, in dependency order, whenever something it depends on changes.
Opening the formula editor
Any bindable field carries an fx badge — click it, or just type = directly into the field, and the plain value input becomes a code editor for that property. What you type there is compiled, not interpreted line-by-line at runtime: syntax errors surface immediately, inline, rather than failing silently later.
Referencing other objects and properties
Inside a formula you can reference other nodes by name and read their properties directly — Circle1.x, Rectangle2.opacity, and so on — the same object-reference model you'd expect from any object-oriented scripting surface, not a special lookup function. Reference chains resolve through the document's live object graph, so a formula on one object recalculates automatically the moment the property it references actually changes — you never have to manually trigger a refresh.
The sandbox: what's actually reachable, and what isn't
This is the part worth being precise about rather than hand-wavy, since as a developer you'll want to know the real boundary, not a marketing description of one.
The compiler works from an explicit allowlist. Only a specific, curated set of host globals compile at all — Math, console, Array, Number, String, JSON, Object, the error constructors, Map, and Set - and each of those exposes only a curated list of its own members; a member not on that list is a compile error, not a silent no-op. A small set of bare math helpers are available without even prefixing them with Math: sin, cos, abs, min, max, round, floor, ceil, plus two animation-specific helpers, clamp and lerp, and a functional if.
What's deliberately excluded, and why: network access (fetch, XMLHttpRequest, WebSocket, EventSource), persistent storage (localStorage, sessionStorage, indexedDB, caches), dynamic codegeneration (eval, Function), threads (Worker), host/ambient identity (navigator, location, history), file access (Blob, File, FileReader), and crypto (Math.random covers the actual use case — nothing in a formula needs cryptographic randomness). None of these is things a formula bound to a property should be able to reach, and the compiler enforces it by simply never compiling a reference to them.
One honest caveat, worth knowing if you're relying on this as a hard security boundary rather than a footgun-prevention default: window itself is still currently in the exposed-globalsset, for legacy compatibility, and window reaches everything the exclusion list above is trying to keep out - window.fetch(...) still compiles today. This is a known, flagged gap in the codebase rather than an oversight you're the first to notice, and it's specifically called out as the first thing that has to be closed before this sandbox can be treated as safe against untrusted peer-authored code in a multiplayer context. For your own single-author documents it's not a practical concern; if you're building something where another user's formulas run in your session, it's worth knowing the isolation isn't complete yet.
Dependency-order recalculation
A formula doesn't run on a timer or a fixed frame tick — it recalculates specifically when something it actually reads changes, and Formo resolves the whole dependency graph so that if A depends on B and B depends on C, a change to C propagates through B to A in the correct order, in one pass, rather than needing multiple frames to settle.
Worked examples with reactive formulas
Driving one shape's size from another's position: Rectangle1.width = Math.abs(Circle1.x - 200) — as Circle1 moves, the rectangle's width tracks the distance live, with no manual keyframing involved.
Driving opacity from time: Shape1.opacity = (Math.sin(time) + 1) / 2 — a smooth, continuous pulse, driven by the same play-clock value that animations and shader parameters can also reference.
Next up — Events and methods: running real code in response to a click, a collision, or a frame tick, and calling functions across objects.