@cnvx/nodal
v1.0.1
Published
Draw edges between HTML elements with zero JavaScript — <n-edge> declarations compiled to CSS anchor positioning
Maintainers
Readme
nodal
Draw edges between HTML elements with zero JavaScript.
@cnvx/nodal lets you write connections in plain HTML with <n-edge>. A build-time compiler turns those declarations into CSS anchor-positioned edge elements: no measurement loop, no observers, nothing to hydrate.
Zero JavaScript, precisely: on Chrome/Edge 125+ and Firefox 147+ a diagram renders and tracks layout with static HTML and CSS alone. Safari — at every version — and older engines run a compiled 5,460-byte fallback that measures the endpoints instead. Compile with fallback: "none" and no script ships at all, at the cost of hiding edges on engines without CSS anchor positioning. Full matrix below.
npm add -D @cnvx/nodalRendered docs and live demos: https://nodal.convex.works
Quick Start
nodal draws the edge; your CSS lays out the nodes. from-anchor="right" declares that the target sits to the right of the source, so the surface needs a layout that puts it there. Without one — plain block layout stacks the nodes — the edge box collapses to zero width and nothing is painted.
<div
class="nodal"
style="display: flex; align-items: center; justify-content: space-between"
>
<div id="start">
Start
<n-edge to="#end" from-anchor="right" to-anchor="left">next</n-edge>
</div>
<div id="end">End</div>
</div>Any layout works — flex, grid, absolute positioning, your own classes. The rule is only that the layout must agree with the anchors you declared. Anchorless edges (<n-edge to="#end">) connect center to center and survive any arrangement.
With plain Vite HTML:
import { nodal } from "@cnvx/nodal/vite";
export default {
plugins: [nodal()],
};With SvelteKit:
// src/hooks.server.ts
import { nodalHandle } from "@cnvx/nodal/sveltekit";
export const handle = nodalHandle();<!-- in a page that hydrates -->
<script>
import { Nodal } from "@cnvx/nodal/svelte";
</script>
<Nodal>
<!-- nodes and <n-edge> declarations -->
</Nodal>The handle renders <Nodal> edges into the server response, and the component
adopts them during hydration. On a client-only navigation or in an SPA, the
same component compiles them in the browser.
For HTML emitted by another build tool:
npx @cnvx/nodal distBrowser Support
nodal renders through CSS anchor positioning, which shipped in Chrome/Edge 125 (May 2024), Safari 26.0 (September 2025) and Firefox 147 (January 2026) — about 81.7% of global traffic (caniuse, July 2026). Everything else is covered by the compiled fallback, which is on by default.
| Engine | Anchor positioning | Default output (fallback: "inline") | fallback: "none" |
| ---------------------------------------- | ------------------ | ----------------------------------------- | --------------------- |
| Chrome / Edge 125+ | yes | native CSS; the runtime exits at load | native CSS, no script |
| Firefox 147+ | yes | native CSS; the runtime exits at load | native CSS, no script |
| Safari 26+ | yes | compiled fallback runtime (see below) | native CSS, no script |
| Chrome < 125, Firefox < 147, Safari < 26 | no | compiled fallback runtime | edges are hidden |
The fallback is not a polyfill of anchor positioning. The compiler has already resolved every endpoint, so the runtime only measures those elements and writes ordinary pixel insets, then re-measures on resize, relevant DOM/style mutation, and font load.
Safari is always on the fallback path. src/lib/fallback.ts disqualifies WebKit by vendor, at every version, because Safari 26 accepts anchor positioning but can latch its size-container probes at zero under layout churn — a timing-dependent failure no one-shot feature probe can catch. Set fallback: "none" to put Safari 26+ on the native zero-JS path and accept that constraint yourself; see Limitations.
Payload
What the compatibility path costs, measured on the Quick Start page above (one edge with a label):
| Output | One-edge page | Script on the page |
| ------------------------------------- | ------------- | ------------------------------------------------------------- |
| compile(html) (default) | 9,014 bytes | 5,460 bytes inline (≈1.7 KB gzipped), inert on Chrome/Firefox |
| compile(html, { fallback: "none" }) | 2,698 bytes | none |
Turn the compatibility output off when you need strictly script-free HTML. This removes its manifests, CSS, and JavaScript rather than leaving dormant fallback bytes:
compile(html, { fallback: "none" });npx @cnvx/nodal dist --fallback noneDocumentation
The chapters live in this repository and render at https://nodal.convex.works/docs:
- Overview — install, first edge, core model.
- Reference — every attribute, option, export, and CLI flag.
- Integrations — Vite, SvelteKit, Svelte, CLI, CSP.
- Limitations — the constraints that matter in production.
- Examples — copy-paste patterns.
- Troubleshooting — start here when edges are invisible.
The site also hosts a playground that compiles <n-edge> in the browser with the same compile() the build carriers use.
Public API
import { compile, compileFragment } from "@cnvx/nodal";
import { nodal } from "@cnvx/nodal/vite";
import { nodalHandle } from "@cnvx/nodal/sveltekit";
import { Nodal } from "@cnvx/nodal/svelte";
import { mount, scanNodalFallback } from "@cnvx/nodal/fallback";
import "@cnvx/nodal/elements";compile(html, options?)transforms an HTML string.compileFragment(fragment, options?)returns a compiled island (html+css+baseCss) for hydrated apps.nodal(options?)compiles Vite HTML entries and emitted HTML assets.nodalHandle(options?)compiles SvelteKit server-rendered pages, including hydrated<Nodal>surfaces.<Nodal>from@cnvx/nodal/svelteadopts server-compiled edges when the handle ran, and compiles the same<n-edge>HTML client-side for SPAs and client navigation.mount(surface, artifact)wires a compiled island to the DOM;scanNodalFallback(root?)is the low-level compatibility runtime.@cnvx/nodal/elementsadds Svelte template types for<n-edge>.
The compiler relies on no Node.js APIs or filesystem access and runs in any modern JavaScript runtime — Node, Bun, browsers, workers (verified by the browser/worker smoke test).
License
MIT
