isomorphic-dompurify
isomorphic-dompurify packages DOMPurify behind one import that works in browsers and Node.js. Browsers use the real window and DOM; Node uses jsdom to create the DOM tree DOMPurify needs. The exported sanitizer, configuration methods, hooks, types, and factory follow DOMPurify, while the wrapper adds server lifecycle handling through clearWindow. Its job is to remove unsafe HTML, SVG, and MathML markup at an SSR or universal JavaScript boundary, not to encode plain text or validate URLs by itself.
A practical wrapper when one import must work in SSR and the browser, with the same well-studied DOMPurify policy. Browser-only projects should install DOMPurify itself, and server users must accept jsdom's weight, runtime floor, lifecycle cleanup, and module-compatibility testing.
Use it if
- The same module must sanitize user-authored markup during server rendering and again in the browser
- You want DOMPurify's allowlist, profiles, hooks, Trusted Types support, and TypeScript types without hand-wiring jsdom in every Node entry
- Your SSR framework respects conditional exports so Node receives the jsdom build and browser bundles receive the browser build
- You can stay on the package's narrow current Node versions and actively update sanitizer dependencies when security fixes ship
- You sanitize only in browsers: use dompurify directly and avoid shipping or installing the wrapper's jsdom dependency for server support you do not need
- Your server is below Node 22.22.2: version 3.22.0 declares only Node 22.22.2, 24.15.0, or 26 and newer within its specified ranges
- You need a light server dependency: the wrapper installs jsdom 30 plus DOMPurify, bringing a full server-side DOM implementation for one operation
- Your stack is CommonJS and cannot pin transitive packages: the project lists an ERR_REQUIRE_ESM problem in CommonJS environments and documents a jsdom override as the workaround
- You plan to loosen allowlists casually or modify sanitized markup afterward: DOMPurify's documentation warns that post-sanitize mutation can void protection, and custom tags, attributes, URI rules, or hooks expand the security surface
Setup reality
`npm install isomorphic-dompurify` installs DOMPurify 3 and jsdom 30. The current package engine is unusually strict: Node `^22.22.2 || ^24.15.0 || >=26.0.0`, so many otherwise supported Node 22 and 24 deployments cannot install 3.22.0 without upgrading. The package publishes conditional Node and browser entries in both import and require forms, but its README still documents a CommonJS `ERR_REQUIRE_ESM` failure caused by jsdom dependencies and suggests a package-manager override to jsdom 25.0.1 for affected deployments. Test the exact framework, bundler, and module mode you ship. Server sanitization owns a long-lived jsdom window; the README says DOM state can accumulate and cause progressive slowdown and memory growth. Call `clearWindow()` at a measured boundary such as after a batch, but remember it closes the window and discards hooks and persistent config, so reapply them afterward. DOMPurify does not follow semantic versioning according to this project's README, so wrapper changes are released as minor versions even when the upstream patch might alter behavior. Pin and test sanitizer updates with malicious regression fixtures. Use the default configuration unless requirements force a narrower allowlist. Treat `removed` as debugging information only, never as proof that input was safe. Sanitize at the final markup boundary, do not mutate the result with libraries that can reintroduce unsafe nodes, and still apply normal URL, CSP, template, and output-context controls.
Patterns
Sanitize untrusted HTMLsanitize-html
import DOMPurify from 'isomorphic-dompurify';
const dirty = '<img src=x onerror=alert(1)><p>Hello</p>';
const clean = DOMPurify.sanitize(dirty);
renderHtml(clean);Sanitize at the last markup boundary and do not pass the result through code that can add unsafe attributes or nodes afterward.
Restrict output to the HTML profileuse-html-profile
import {sanitize} from 'isomorphic-dompurify';
const clean = sanitize(input, {
USE_PROFILES: {html: true},
});USE_PROFILES overrides ALLOWED_TAGS. This profile excludes SVG and MathML rather than adding to a custom tag list.
Allow only a small formatting subsetallow-minimal-markup
import {sanitize} from 'isomorphic-dompurify';
const clean = sanitize(input, {
ALLOWED_TAGS: ['p', 'strong', 'em', 'a'],
ALLOWED_ATTR: ['href'],
});A narrower allowlist is easier to reason about. URL policy still deserves tests for every scheme and context your application permits.
Remove style tags and attributesforbid-style-content
import {sanitize} from 'isomorphic-dompurify';
const clean = sanitize(input, {
FORBID_TAGS: ['style'],
FORBID_ATTR: ['style'],
});This adds restrictions to the default policy. It is not a complete CSS security policy for markup you later modify.
Remove target attributes with a typed hookstrip-link-targets
import {addHook, removeHook, sanitize, type NodeHook} from 'isomorphic-dompurify';
const stripTarget: NodeHook = node => {
if ('removeAttribute' in node) node.removeAttribute('target');
};
addHook('afterSanitizeAttributes', stripTarget);
const clean = sanitize(input);
removeHook('afterSanitizeAttributes', stripTarget);Hooks are global to the shared instance. Remove them after scoped work, and reapply needed hooks after clearWindow on the server.
Inspect what the last call removedinspect-removed-items
import DOMPurify from 'isomorphic-dompurify';
const clean = DOMPurify.sanitize(input);
for (const item of DOMPurify.removed) {
console.debug('sanitizer removed', item);
}DOMPurify explicitly says removed is only a curiosity and must not drive security decisions. It describes the most recent shared-instance sanitization.
Set and clear a persistent configurationset-shared-config
import {clearConfig, sanitize, setConfig} from 'isomorphic-dompurify';
setConfig({USE_PROFILES: {html: true}});
try {
const clean = sanitize(input);
} finally {
clearConfig();
}While setConfig is active, extra configuration passed to sanitize is ignored. Shared mutable config is risky under concurrent request code.
Release jsdom state after a batchclear-server-window
import {clearWindow, sanitize} from 'isomorphic-dompurify';
try {
for (const record of records) record.clean = sanitize(record.html);
} finally {
clearWindow();
}On Node this closes and recreates the internal jsdom window. Hooks and setConfig state must be reapplied; in browsers the call is a no-op.
Create an isolated sanitizer instancebind-explicit-window
import createDOMPurify from 'isomorphic-dompurify';
import {JSDOM} from 'jsdom';
const window = new JSDOM('').window;
const purify = createDOMPurify(window);
try {
const clean = purify.sanitize(input);
} finally {
window.close();
}The factory mirrors DOMPurify and is useful for isolated tests or policies, but importing jsdom directly makes lifecycle ownership your responsibility.
Return a DOM fragmentreturn-dom-fragment
import {sanitize} from 'isomorphic-dompurify';
const fragment = sanitize(input, {RETURN_DOM_FRAGMENT: true});
for (const child of fragment.childNodes) {
inspectNode(child);
}The returned nodes belong to the active browser or jsdom window. Do not reuse them after clearWindow closes the server window.
Request Trusted Types output in browsersreturn-trusted-html
import {sanitize} from 'isomorphic-dompurify';
const trusted = sanitize(input, {RETURN_TRUSTED_TYPE: true});
element.innerHTML = trusted;TrustedHTML is returned only where the Trusted Types API is available. Keep CSP and an application Trusted Types policy aligned with this path.
Regression-test sanitizer behaviortest-malicious-fixtures
import {sanitize} from 'isomorphic-dompurify';
import {expect, test} from 'vitest';
test('removes event handlers', () => {
const clean = sanitize('<a href="/" onclick="steal()">go</a>');
expect(clean).not.toContain('onclick');
});Keep fixtures for your exact config and markup sinks. Upstream behavior can change in wrapper minor releases because DOMPurify does not follow semantic versioning.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| dompurify | npm | You sanitize in a browser or are willing to create and manage the server DOM instance yourself |
| sanitize-html | npm | You need a Node-focused parser sanitizer with explicit tag, attribute, iframe, and URL-scheme policies |
| xss | npm | You want a smaller Node-oriented whitelist sanitizer and its parsing and policy model fits your threat cases |