Open-source project
jsdom/jsdom avatar
jsdom/jsdom

jsdom: a browser-shaped DOM for Node.js, and the security switch that decides how far it goes

A JavaScript implementation of various web standards, for use with Node.js

21,694 stars1,808 forksJavaScriptMIT

At a glance

What is it?
jsdom implements the WHATWG DOM and HTML standards in pure JavaScript so Node.js programs can parse, query and script HTML. Its most consequential design decision is that embedded scripts stay switched off unless you pass runScripts: "dangerously".
Who is it for?
Adopt jsdom when you need real DOM semantics in Node.js for tests or scraping of markup you trust, and accept that anything with a <script> tag needs runScripts: "dangerously" plus resources: "usable" before it behaves like a browser. Do not adopt it as a security boundary for untrusted HTML, and do not expect layout or rendering; there is no viewport engine here.
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 last received commits 8 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 29, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The gap jsdom fills between a parser and a browser

Node.js has no document object. If your code needs document.querySelector, element.closest, a MutationObserver, localStorage, or a form submission, you either run a real browser or you find something that implements those interfaces in JavaScript. jsdom is that second option. The README describes it as "a pure-JavaScript implementation of many web standards, notably the WHATWG DOM and HTML Standards, for use with Node.js," and states the goal plainly: emulate enough of a browser to be useful for testing and scraping real-world web applications.

The audience is therefore narrow and specific. Test authors who want component tests to run in a Node process rather than a headless browser. Scrapers that need to run a page's own DOM manipulation and then read the result. Tooling authors who need to parse HTML the way a browser parses it, including the implied html, head and body elements the README calls out. Anyone who needs pixel output, layout geometry or CSS cascade results is not in this group, because jsdom is not a rendering engine.

One constructor, a window, and a class that acts on it from outside

The entry point is the JSDOM constructor, a named export of the main module. You pass it a string of markup and get back a JSDOM instance whose window property is the emulated browsing context. Parsing follows browser rules rather than a strict XML reading, so a fragment gains the wrapper elements a browser would add.

The README draws a distinction worth internalising. The window is for code that behaves as if it were inside the page. The JSDOM object itself is for code acting "from the outside," doing things the ordinary DOM APIs cannot express. For simple cases the README suggests destructuring straight to what you need, which avoids holding a reference to the outer object at all.

A second constructor argument carries the configuration. The documented simple options include url, which sets window.location and document.URL and governs relative URL resolution, same-origin restrictions and the referrer used when fetching subresources; referrer, which only affects document.referrer; contentType, which decides whether the document is parsed as HTML or XML and throws for values that are neither; includeNodeLocations, which preserves parser location data for nodeLocation() and keeps line numbers in script stack traces correct, at a performance cost; and storageQuota, the per-origin ceiling in code units for localStorage and sessionStorage, defaulting to 5,000,000 and throwing a DOMException when exceeded. Both url and referrer are canonicalized, so an unparseable URL throws rather than silently degrading.

Installing jsdom and getting a first query to return

jsdom is published on npm under the name jsdom, and the README's basic usage example is a CommonJS require. Install it into your project's dependencies:

bash
npm install jsdom

The README notes that recent versions require newer Node.js releases and points to the engines field in package.json for the exact range, so check that field if your runtime is pinned. Then construct a document and read from it:

js
const jsdom = require("jsdom");
const { JSDOM } = jsdom;

const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`);
console.log(dom.window.document.querySelector("p").textContent); // "Hello world"

Running that prints Hello world. The README also notes that jsdom parses the string the way a browser would, adding implied html, head and body tags, so the markup you passed is not the tree you get back. For the common case where only the document matters, the README suggests destructuring directly to it:

js
const { document } = (new JSDOM(`...`)).window;

One packaging detail matters at install time. canvas is declared as an optional peer dependency, so npm will not fail if it is absent, but code paths that expect canvas APIs will not have them. Decide early whether your tests touch those paths.

runScripts defaults to off, and that default is the whole security story

The README is unusually direct here: executing scripts is jsdom's most powerful ability and also highly dangerous with untrusted content. The sandbox is not foolproof, and script code inside the DOM can, if it tries hard enough, reach the Node.js environment and therefore the machine. So embedded scripts do not run by default. The README's own example shows a <script> appending an hr element and the child count printing 0.

Turning it on requires runScripts: "dangerously", after which the same example prints 1. The name is not decoration. The README states that using it on arbitrary user-supplied code or code from the Internet is effectively running untrusted Node.js code, with machine compromise as the outcome. Event handler attributes such as onclick are governed by the same setting and stay inert unless runScripts is "dangerously", while handler properties assigned in JavaScript, such as div.onclick = ..., work regardless.

There is a middle setting. runScripts: "outside-only" installs fresh copies of the JavaScript spec globals on window, including window.Array and window.Promise, and notably window.eval, which runs script with the jsdom window as the global. That is the option for driving a page from your own test code without letting the page's own tags execute. External scripts loaded via <script src=""> need resources: "usable" in addition, and the README suggests setting url alongside it for the same-origin and relative-resolution reasons described above. The README also scopes the guarantee: it covers web content processed by jsdom, not a host Node.js environment already compromised through something like prototype pollution, and it points to a separate security policy for that case.

Where jsdom is the wrong tool

The clearest boundary is untrusted input. If you are scraping pages you do not control and you need their scripts to run, you are asking jsdom to execute code you have not audited, and the README says so without hedging. A real browser in a sandboxed process is the appropriate container for that job, not a Node library that shares a process with your application.

The second boundary is anything visual. jsdom implements DOM and HTML interfaces; the README frames the goal as emulating enough of a subset of a browser for testing and scraping, not rendering. Layout, scrolling geometry and computed visual results are outside what the documentation claims, so a test asserting on element position is testing something jsdom does not promise to produce.

The third is XML location data. includeNodeLocations cannot be combined with an XML content type, because the README states the XML parser does not support location information. If your workflow needs both, one of the two has to give. And storageQuota is a hard ceiling rather than a soft one: writes past it throw a DOMException, which will surface in tests as an exception rather than a truncated value.

How jsdom differs from happy-dom

The most common comparison people search for is jsdom against happy-dom, and the difference is one of philosophy rather than features. jsdom is built as a standards implementation: the README names the WHATWG DOM and HTML specifications as its target, and the dependency list reflects that orientation, with parse5 for HTML parsing, saxes for XML, whatwg-url, whatwg-mimetype, webidl-conversions and w3c-xmlserializer handling the specification-shaped pieces. Correctness against the specs is the organising principle.

happy-dom takes the opposite starting point. It is a lighter DOM implementation aimed at speed for test environments, and it does not claim to track the specifications the way jsdom does. In practice that means happy-dom tends to start faster and use less memory, while jsdom tends to match browser behaviour more closely on edge cases in parsing, URL handling and the less-travelled DOM APIs. The trade-off is not that one is correct and the other is not; it is that you are choosing between fidelity to a specification and the cost of that fidelity. If your tests exercise ordinary markup and common DOM calls, the lighter option may be sufficient. If they depend on parser quirks, URL semantics or interfaces that only exist because a spec defines them, jsdom's approach is the one that matches what a browser would do.

Version cadence, licence and the cost of upgrading

jsdom is licensed under the MIT licence, per both the repository metadata and the LICENSE.txt file at the top level. MIT is permissive: it allows use, modification and redistribution provided the copyright notice and permission notice are retained. That is a statement about the licence text, not legal advice for your situation; if your organisation has a policy on dependency licences, the file to read is LICENSE.txt.

Upgrade cost is concentrated in two places. The first is the Node.js floor, which the README explicitly says moves with newer versions and directs you to the engines field in package.json to check. A major version bump is the moment to verify your runtime. The second is the dependency graph, which is large and includes packages that themselves track web specifications, so a jsdom major can pull in several coordinated majors underneath it. The repository keeps a changelog, and the release history shows a steady cadence of patch and major releases rather than long quiet periods; the last push to the default branch was on 2026-09-19, two days after the v30.1.0 release on 2026-09-17. Practically, that means pinning a version and reading the changelog before moving across a major is cheaper than discovering a parsing behaviour change through a failing test suite. The repository also carries a benchmark directory, so performance claims about a given version are best checked there rather than assumed.

Editorial conclusion

Adopt jsdom when you need real DOM semantics in Node.js for tests or scraping of markup you trust, and accept that anything with a <script> tag needs runScripts: "dangerously" plus resources: "usable" before it behaves like a browser. Do not adopt it as a security boundary for untrusted HTML, and do not expect layout or rendering; there is no viewport engine here. Before committing, check the engines field in package.json against your Node.js version, confirm whether your code path needs the optional canvas peer dependency, and decide whether the fetch behaviour you depend on is documented for the url and resources combination you plan to use.

Frequently asked questions

What is jsdom used for?

The README states that jsdom emulates enough of a subset of a web browser to be useful for testing and scraping real-world web applications, and that it is a pure-JavaScript implementation of web standards including the WHATWG DOM and HTML Standards for use with Node.js.

Is jsdom safe to use?

Scripts embedded in the HTML are disabled by default, and the README says the sandbox is not foolproof. Enabling runScripts: "dangerously" on arbitrary user-supplied code or code from the Internet means effectively running untrusted Node.js code, and the README says the machine could be compromised. The guarantee covers web content processed by jsdom, not a host Node.js environment already compromised through something like prototype pollution.

What are some alternatives to jsdom?

happy-dom is the usual alternative, and the difference is approach: jsdom targets the WHATWG DOM and HTML specifications, while happy-dom is a lighter DOM implementation aimed at test environments that does not claim the same specification tracking. The trade-off is fidelity against cost.

How do I install jsdom?

jsdom is published on npm under the name jsdom, so it installs with npm install jsdom. Recent versions require newer Node.js releases, and the README points to the engines field in package.json for the exact range.

How do I use jsdom in Node.js?

Require the module and use the JSDOM constructor, which is a named export. Passing a markup string returns a JSDOM object whose window property holds the emulated browsing context, so dom.window.document.querySelector("p").textContent reads from the parsed document.

Official sources

  1. Issues
  2. jsdom/jsdom on GitHub
  3. License: MIT
  4. README
  5. Releases
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/jsdom-jsdom.svg)](https://hysenlabs.com/projects/jsdom-jsdom)