@vexralabs/zerodom
The visual action layer for AI web agents — TypeScript edition.
See the page. Know exactly what to click.

TypeScript port of ZeroDOM's core engine —
same interaction graph, same selector guarantees, for the Node/TS agent stack
(LangChain.js, the Vercel AI SDK, Playwright for Node).
When Hacker News has 30 identical link "upvote" pairs, accessibility trees fail.
ZeroDOM assigns 1:1 deterministic handles, resolving [45] to the exact DOM
element while keeping CSS selectors entirely out of the context window.
Install
npm install @vexralabs/zerodom
Quickstart
import { ZeroDOM } from "@vexralabs/zerodom";
const graph = await ZeroDOM.fromPage(page);
console.log(graph.toCompactText());
const selectors = graph.selectorMap();
import { parseHtml } from "@vexralabs/zerodom";
const graph = parseHtml(html, url);
Real output, 11,882 tokens of Hacker News → 2,326:
PAGE: Hacker News | https://news.ycombinator.com
[01] a 'Show HN: ZeroDOM — agents only need to know what they can click'
[02] a 'dev'
[03] a '214 comments'
Core features
- Deterministic parse, no LLM in the loop. lxml-equivalent engine on linkedom,
identical output every run. Sub-50ms on most pages.
- Selectors never enter the context window. The model sees
[03]; the CSS path
stays in selectorMap() on your side.
- Shadow DOM handled. Open shadow roots are parsed and light-DOM selectors are
scoped with
:light(…). Pages with no shadow root pay nothing.
- Structural action diffs.
+ appeared, - gone, ~ value changed — agents
stop re-reading entire pages.
- iframe support. Pass
{ frames: true } to read same- and cross-origin frames.
API
import { ZeroDOM, parseHtml } from "@vexralabs/zerodom";
const graph = await ZeroDOM.fromPage(page);
const graph = parseHtml(html, url);
graph.toCompactText();
graph.toCompactText({ selectors: true });
graph.toCompactText({ hrefs: true });
graph.selectorMap();
graph.metadata;
Why linkedom, not jsdom
linkedom gives a real querySelector/getElementById DOM — needed so every
generated selector can be self-verified, the same guarantee the Python version
makes against a real browser — at a fraction of jsdom's footprint. One caveat this
port works around: unlike jsdom or a real browser, linkedom's parser doesn't
normalize a bare HTML fragment into a full <html><body> document, so
ZeroDOMParser wraps non-document input itself before handing it to linkedom
(page.content() output is unaffected — it's always a full document already).
Development
npm install
npm run build
npm test
License
Apache 2.0 — see LICENSE.