# isomorphic-dompurify makes DOMPurify work the same on server and client

> isomorphic-dompurify is a thin wrapper that gives DOMPurify a DOM on Node so the same sanitize call works in a server render and in the browser. It fixes an environment problem, not a sanitisation one, and the sanitising itself is entirely DOMPurify's.

**kkomelin/isomorphic-dompurify** — Use DOMPurify on server and client in the same way

- Repository: https://github.com/kkomelin/isomorphic-dompurify
- Website: https://www.npmjs.com/package/isomorphic-dompurify
- Stars: 605 · Forks: 17
- Language: TypeScript
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/kkomelin-isomorphic-dompurify

## The problem is a missing DOM, not a missing sanitizer

The project states its role plainly: it makes it possible to use DOMPurify on both server and client in the same way, and it does nothing by itself except for providing an isomorphic or universal wrapper around DOMPurify, so all credits go to DOMPurify authors and contributors. The underlying problem is described in the motivation section. DOMPurify needs a DOM tree to work with, which is not available in Node by default, so working on the server means creating a fake DOM and supplying it to DOMPurify, which means the initialization logic on the server is not the same as on the client. That is the entire gap this project closes. It was born with the idea of encapsulating DOMPurify initialization details and providing an easy way to import the library on both server and client, for example in Next.js apps, and it was inspired by Isomorphic Unfetch, which solved the same shape of problem for fetch. The audience is anyone with a React or Next.js codebase where untrusted HTML arrives from somewhere and has to be cleaned before it reaches the DOM.

## The same import works in both places

The usage is a single import and a single call, which is the entire point of the wrapper. The default import gives you the DOMPurify object with a sanitize method:

```javascript
import DOMPurify from "isomorphic-dompurify";

const clean = DOMPurify.sanitize(dirtyString);
```

The config object DOMPurify takes is passed as a second argument, unchanged:

```javascript
const clean = DOMPurify.sanitize(dirtyString, { USE_PROFILES: { html: true } });
```

Named imports are supported too, for code that prefers them:

```javascript
import { sanitize } from "isomorphic-dompurify";

const clean = sanitize(dirtyString);
```

The package manifest shows how the dual target is achieved. The main entry points at dist/index.js and the module entry at dist/index.mjs, with types at dist/index.d.ts, and the browser field maps those two server entries onto browser.js and browser.mjs, so a bundler resolving for the browser automatically gets the build that uses the real DOM rather than the jsdom one. The exports map is more explicit still, with separate node and default conditions, and within each an import and a require branch with their own type declaration files. That structure is what makes a single import statement work in a Node server, in a Next.js server render and in a client bundle, and it is the reason the wrapper is a package rather than a snippet.

## A factory form for tests and sandboxed windows

There is one more API shape worth knowing, and it exists for a specific reason. The default export is also callable as a factory, matching the dompurify API, and the documentation says this is useful when you need a DOMPurify instance bound to a specific window, for example in tests or sandboxed environments:

```javascript
import DOMPurify from "isomorphic-dompurify";
import { JSDOM } from "jsdom";

const purify = DOMPurify(new JSDOM().window);
const clean = purify.sanitize(dirtyString);
```

So instead of the package deciding which window DOMPurify binds to, you can hand it one. That matters in a test suite, where you may want a clean document per assertion, and in an environment where the ambient window is not the one whose document you want sanitised against, such as an iframe or a worker scope. The TypeScript story is handled with the same care. The hook related types from dompurify are re-exported, so you can type your addHook callbacks without redeclaring the signatures:

```typescript
import { addHook, type NodeHook } from "isomorphic-dompurify";

const stripTargetBlank: NodeHook = function (node) {
  if ("target" in node) (node as Element).removeAttribute("target");
};

addHook("afterSanitizeAttributes", stripTargetBlank);
```

The full list of re-exported types is given in the documentation, including Config, DOMPurify, DocumentFragmentHook, ElementHook, HookName, NodeHook, RemovedAttribute, RemovedElement, the UponSanitizeAttributeHook and UponSanitizeElementHook types with their event counterparts, and WindowLike.

## Long-running servers need the jsdom window cleared

There is a memory management note in the documentation that addresses a real operational problem rather than a theoretical one. In long-running Node.js processes, the internal jsdom window accumulates DOM state across sanitization calls, which can cause progressive slowdown and memory growth. The fix is a clearWindow function you call periodically to release those resources:

```javascript
import { sanitize, clearWindow } from "isomorphic-dompurify";

// Sanitize as usual
const clean = sanitize(dirtyString);

// Release jsdom resources when appropriate (e.g. after a request, after a batch)
clearWindow();
```

clearWindow closes the current jsdom window and creates a fresh one, and all import styles, default and named, continue to work afterwards. There is one consequence to be aware of, given in a note. Any hooks or config set via addHook or setConfig need to be re-applied after calling clearWindow. In the browser build, clearWindow is a no-op, which is the correct behaviour since there is no jsdom window to release. If you are running this in a long-lived server, that re-application step is the part to get right, because a hook that silently stops applying after the first clear is the kind of bug that is hard to notice and easy to exploit.

## Version support, releases and the Node.js floor

The requirements table maps versions of this package to the Node.js versions they need, and the trend is steep. Versions up to 0.19.0 need Node 12, and from 0.20.0 the floor is Node 14, then Node 16 at 1.4.0, Node 18 at 1.10.0, Node 20 at 2.27.0 and 2.30.0, and from 3.0.0 the range is ^20.19.0, ^22.12.0 or >=24.0.0, tightened at 3.4.0 to ^20.19.0, ^22.13.0 or >=24.0.0, and from 4.0.0 it is ^22.22.2, ^24.15.0 or >=26.0.0. If you are on a long term support Node release, check where it falls before upgrading, because the current major line requires a recent 22 or a very recent 24. The release cadence explains itself as well. The current version is 4.6.0, released on 2026-10-16, with 4.5.0 and 4.4.0 in the two weeks before it, and the reason for the minor bumps is that DOMPurify does not follow Semantic Versioning, so every change has to be released as a minor version because it cannot be certain whether new features were added to a patch release of DOMPurify. The last push to the repository was on 2026-10-16 and it is not archived. The package is built with tsup, linted and formatted with biome, tested with vitest, and shipped with type declarations for both module systems.

## The CommonJS break and what to do about it

There is one known issue that is worth reading before you deploy, because it is a build failure rather than a runtime surprise. From version 3.0.0, jsdom@28 pulls in an ESM-only dependency that breaks require in environments like Next.js on Vercel, and the documented workaround is to pin jsdom to 25.0.1 via package manager overrides. That is a specific instruction and a specific version, and it matters because the failure mode is a broken build in the environment most likely to be running the server side of a React application, which is exactly where this library is supposed to help. The current package manifest depends on dompurify at ^3.4.17 and jsdom at ^30.1.3, so the ecosystem has moved on from the jsdom 28 that caused the problem, and the workaround is documented for the versions that had it. Related to that is a limit on what this library can promise in a browser. The isomorphic-dompurify library is compatible with Web Workers, but dompurify, which it depends upon, is not, at least not yet. The distinction is in the dependency, not in the wrapper.

## Where a wrapper like this is the wrong answer

The honest assessment is that this is a convenience layer, and a convenience layer is the wrong thing to reach for when the underlying constraint is not the one it solves. If you sanitise only in the browser, DOMPurify on its own is enough and this adds a dependency and a jsdom install for nothing. If you sanitise only on the server and you need to strip tags rather than parse them, a server side HTML sanitiser built for that job avoids loading a full DOM implementation per process, and the memory behaviour of jsdom in a high throughput service is the wrong shape. If you need a sanitiser that also works for something other than HTML, such as SVG or MathML, DOMPurify does handle those and this wrapper inherits that capability, but there are server side alternatives that do too. A fourth point is about where sanitising belongs. A wrapper that lets one call site work in both environments makes it easy to sanitise on the client and forget that the server also renders the same untrusted string, and a string that reaches the server render is exactly the case that matters. The correct placement for sanitising is on the server, before the value is stored or sent, and this library helps you share the code, not decide where the boundary is. There is also a licensing detail that is easy to misread. The DOMPurify portion is Apache 2.0 or MPL 2.0, while the isomorphic wrapper itself is MIT, and the two apply to different code.

## Conclusion

Adopt isomorphic-dompurify if your application renders on the server and in the browser and you want one sanitising call site instead of a browser only import guarded by a window check. Do not adopt it as a sanitiser in its own right, since the work is done by DOMPurify and the wrapper adds no rules of its own, and check the CommonJS caveat before deploying to a serverless platform. Before you upgrade, read the known issues section, because version 3 brought an ESM only dependency that breaks require in some environments, and confirm the Node.js version your runtime provides against the requirements table.

## FAQ

### What does isomorphic-dompurify actually do?

It provides an isomorphic or universal wrapper around DOMPurify, supplying it with a DOM on Node so that the same import and the same sanitize call work on the server and in the client. It does nothing by itself, and all of the sanitising behaviour comes from DOMPurify itself.

### How do I install and use isomorphic-dompurify?

Install it with `npm i isomorphic-dompurify`, then import it and call sanitize. Both the default import and a named sanitize import work, and the DOMPurify config object is passed as a second argument, for example `DOMPurify.sanitize(dirtyString, { USE_PROFILES: { html: true } })`.

### What Node.js version does isomorphic-dompurify need?

The requirements table maps package versions to Node.js versions, and the current 4.x line needs Node ^22.22.2, ^24.15.0 or >=26.0.0. Earlier lines have lower floors, with 3.0.0 requiring ^20.19.0, ^22.12.0 or >=24.0.0 and 2.30.0 requiring Node 20.

### What is clearWindow for in isomorphic-dompurify?

In long-running Node.js processes the internal jsdom window accumulates DOM state across sanitization calls, causing progressive slowdown and memory growth. clearWindow closes the current jsdom window and creates a fresh one, and it is a no-op in the browser build. Hooks or config set via addHook or setConfig need to be re-applied afterwards.

### Is isomorphic-dompurify usable in CommonJS environments?

From version 3.0.0, jsdom@28 pulls in an ESM-only dependency that breaks require in environments such as Next.js on Vercel. The documented workaround is to pin jsdom to 25.0.1 through package manager overrides, and the current package depends on dompurify ^3.4.17 with jsdom ^30.1.3.

## Sources

- [kkomelin/isomorphic-dompurify on GitHub](https://github.com/kkomelin/isomorphic-dompurify)
- [License: MIT](https://github.com/kkomelin/isomorphic-dompurify/blob/master/LICENSE)
- [Project website](https://www.npmjs.com/package/isomorphic-dompurify)
- [README](https://github.com/kkomelin/isomorphic-dompurify/blob/master/README.md)
- [Releases](https://github.com/kkomelin/isomorphic-dompurify/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/kkomelin-isomorphic-dompurify
