dom-serialize
A CommonJS HTML serializer for browser-style DOM nodes. Unlike reading outerHTML, it also accepts text, attribute, comment, document, doctype, document-fragment, NodeList, and array inputs. Its unusual feature is an extensibility hook: it dispatches a bubbling, cancelable serialize event for each node, letting a listener replace that node with a string or another node, or omit it. That flexibility comes from a 2015 codebase and is the main reason to consider it over native DOM properties.
The event-based override hook is genuinely distinctive, so retaining it in a tested legacy browser pipeline can be reasonable. For new code, native outerHTML or XMLSerializer, or a maintained serializer matched to your tree type, is safer than adopting this 2015 package.
Use it if
- You must serialize a mixed input that may be an Element, Text, Attr, Document, DocumentFragment, NodeList, or array
- You need per-node custom output through bubbling serialize events without cloning or rewriting the DOM first
- You maintain a browser-oriented CommonJS project that already relies on this package's exact HTML output
- You want text and attribute entity encoding handled while serializing a small, trusted DOM tree
- You only serialize elements in a modern browser: element.outerHTML and XMLSerializer cover the common cases without four old runtime dependencies
- You need standards-accurate XML, SVG, or namespace handling: the source lowercases every element name and applies an HTML void-element table without checking namespaces
- You cannot allow serialization to run application event handlers: the implementation dispatches a bubbling, cancelable serialize CustomEvent on every node it visits
- You need TypeScript declarations, ESM exports, or an explicit Node support range: version 2.2.1 provides none of them and ships CommonJS only
- You need active fixes: the last npm release was November 2015, while open reports include an ent recursion failure and use of Node's deprecated built-in punycode path
Setup reality
npm install dom-serialize brings four runtime dependencies: custom-event, ent, extend, and void-elements. There are no peer dependencies, native builds, credentials, or config files. In a browser with real DOM nodes, require('dom-serialize') is enough. In Node, the package does not create a DOM, so you need jsdom, @xmldom/xmldom, or another implementation yourself. Its custom-event dependency chooses a constructor from the global environment, not from node.ownerDocument. When using an isolated jsdom window, make sure the CustomEvent used by dom-serialize belongs to that window before requiring the package; otherwise dispatchEvent can reject an event from the wrong realm or the polyfill can look for a missing global document. Serialization is synchronous and recursive, mutates no nodes, and returns an empty string for null, canceled nodes, and unsupported node types, which makes mistakes easy to miss. Each visited node receives a real bubbling serialize event. Existing listeners can therefore change output or cause side effects, and preventDefault removes a node unless detail.serialize is set. The serializer is HTML-shaped rather than a round-trip browser serializer: element names are lowercased, HTML void elements omit closing tags, attributes are enumerated in DOM order, text escapes less-than, greater-than, and ampersand, and comments are wrapped without validating forbidden comment sequences. The README's callback example contains a serialze typo; the working property is event.detail.serialize. There are no TypeScript declarations, ESM entry point, browser export map, or stated compatibility table. Pinning 2.2.1 and testing your target DOM implementation is the sensible approach for legacy use.
Patterns
Serialize an element and its childrenserialize-element
const serialize = require('dom-serialize');
const article = document.createElement('article');
article.setAttribute('data-id', '42');
article.textContent = 'Fish & chips';
console.log(serialize(article));
// <article data-id="42">Fish & chips</article>The package produces the element's outer markup and encodes ampersands in text nodes.
Escape a standalone text nodeserialize-text
const text = document.createTextNode('<strong>A & B</strong>');
console.log(serialize(text));
// <strong>A & B</strong>Text serialization escapes less-than, greater-than, and ampersand, but deliberately leaves quote characters alone.
Serialize a DocumentFragment without a wrapperserialize-fragment
const fragment = document.createDocumentFragment();
fragment.append(document.createElement('b'), document.createTextNode('hello'));
console.log(serialize(fragment));
// <b></b>helloA fragment serializes only its child nodes; no synthetic fragment tag is added.
Serialize a NodeListserialize-node-list
const list = document.querySelectorAll('.result');
const html = serialize(list);Any object with a numeric length and no nodeType is treated as a NodeList or array and concatenated in index order.
Serialize selected nodes in a chosen orderserialize-array
const nodes = [
document.createTextNode('before'),
document.createElement('hr'),
document.createTextNode('after'),
];
console.log(serialize(nodes));
// before<hr>afterHTML void elements such as hr and br are emitted without closing tags.
Override one node with a serialize listeneroverride-node-output
const secret = document.querySelector('[data-secret]');
secret.addEventListener('serialize', (event) => {
event.detail.serialize = '<span>[redacted]</span>';
});
const html = serialize(secret.parentNode);Replacement strings are inserted verbatim, so never build them from untrusted text without escaping it first.
Omit a node with preventDefaultomit-node
const root = document.querySelector('main');
root.addEventListener('serialize', (event) => {
if (event.serializeTarget.matches?.('[data-private]')) {
event.preventDefault();
}
});
const html = serialize(root);The event bubbles only through an actual DOM ancestry. A detached root may not provide the event-delegation behavior you expect for descendants.
Customize one serialization calluse-one-time-callback
const html = serialize(root, (event) => {
if (event.serializeTarget.matches?.('[data-summary]')) {
event.detail.serialize = '…';
}
});When the second argument is a function, it is attached for this call and removed afterward. The correct property is serialize, not the serialze typo in the README.
Pass context to bubbling listenerspass-context
root.addEventListener('serialize', (event) => {
if (event.detail.context === 'email' && event.serializeTarget.nodeName === 'VIDEO') {
event.detail.serialize = '<a href="/watch">Watch video</a>';
}
});
const emailHtml = serialize(root, 'email');The context is arbitrary data passed unchanged as event.detail.context; the implementation does not interpret it.
Replace a node with another DOM nodereplace-with-node
const html = serialize(root, (event) => {
if (event.serializeTarget.nodeName === 'TIME') {
const text = document.createTextNode(event.serializeTarget.dateTime);
event.detail.serialize = text;
}
});A replacement carrying a numeric nodeType is serialized recursively and receives its own serialize event.
Serialize one Attr node with numeric entitiesserialize-attribute
const link = document.createElement('a');
link.setAttribute('title', 'A & "B"');
const output = serialize.serializeAttribute(link.attributes[0], { named: false });
console.log(output);serializeAttribute and serializeText accept ent encoding options, but these named helpers are documented only by source and tests.
Serialize a complete HTML documentserialize-doctype
const type = document.implementation.createDocumentType('html', '', '');
const doc = document.implementation.createDocument(
'http://www.w3.org/1999/xhtml',
'html',
type,
);
console.log(serialize(doc));
// <!DOCTYPE html><html></html>The doctype serializer handles publicId and systemId, but the surrounding element serializer still follows its HTML-oriented lowercase and void-element rules.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| w3c-xmlserializer | npm | You need a maintained, specification-oriented XML serializer for DOM nodes |
| dom-serializer | npm | Your tree comes from htmlparser2, domhandler, or Cheerio rather than the browser DOM |
| parse5 | npm | You need standards-focused HTML parsing and serialization around a parse5 syntax tree |
| jsdom | npm | Node code needs a complete web DOM plus document serialization, and the larger dependency is acceptable |