XSLT Transformer
Apply a stylesheet. Falls back to WebAssembly when the browser drops XSLT.
Everything runs in this tab. Nothing you paste is uploaded, logged or sent anywhere. Open your network panel and check.
Paste a document in the left pane and a stylesheet in the right, and the transformation runs in this tab. The result comes back as text, with the engine that produced it and the time it took above the output. Both inputs are checked for well-formedness first, so a missing angle bracket in the stylesheet is reported as one rather than as a compilation failure.
You reach for it when a stylesheet is not doing what you expect and you want a fast loop: a transform inherited from someone who left, a match pattern you are not sure fires, a template that produces an empty file on one input and not another.
The tool has the shape it does because browser XSLT is being removed on a published schedule. It uses the native processor while there is one and falls back to a WebAssembly build of libxslt when there is not, and it names which engine ran.
Chrome is removing XSLT, and the dates are published
Google filed an intent to deprecate and remove in 2025. The rationale: the xml-stylesheet processing instruction appears in roughly one page load in eight thousand, while libxslt carries a long memory-safety record and lost its long-time maintainer in June 2025. Mozilla and Apple have both indicated support. The timeline is dated:
Firefox and Safari have announced no removal of their own, so XSLTProcessor keeps working there for now. Firefox uses its own TransforMiiX processor rather than libxslt, which is why a handful of stylesheets already behave differently between browsers.
This page probes for a working native processor by running a one-line transformation rather than checking whether the constructor exists: one that exists and fails silently is worse than one that is absent. If the probe fails, it loads a WebAssembly build of libxslt, the polyfill Chrome's own migration guidance points at.
- Chrome 143, 2 December 2025: official deprecation, console and Lighthouse warnings.
- Chrome 145, December 2025: XSLT off by default in Canary, Dev and Beta.
- Chrome 146, 10 March 2026: enterprise policy escape hatch.
- Chrome 152, 25 August 2026: origin trial opens.
- Chrome 158, 17 November 2026: XSLT stops working on stable, except under the trial or the policy.
- Chrome 176, 17 August 2027: both hatches expire. XSLTProcessor and the xml-stylesheet processing instruction are gone. The type="text/css" form of that instruction is not affected.
Templates are rules, not a script
A stylesheet is a set of template rules, each with a match pattern. The processor starts at the document root, finds the best-matching template, runs it, and where that template says xsl:apply-templates it repeats the process for the selected children. Nothing runs in the order you wrote it.
xsl:apply-templates hands control back to the rule set: each selected node finds its own template, so heterogeneous children get their own rules instead of a chain of xsl:choose branches. xsl:for-each iterates inline, applying one body to every selected node, which reads better for a flat list of rows. Both change the context node, which is where most reports of a suddenly-wrong select expression come from.
XSLT also has built-in rules you did not write: the default for elements applies templates to their children, and the default for text nodes copies the text through. That pair is why a stylesheet whose patterns match nothing still produces a page of run-together text.
When the transformation produces nothing
An empty result is the second most reported XSLT problem after run-together text, and it has one dominant cause: namespaces. A stylesheet written for un-namespaced XML will not match elements that are in a namespace, even though the names look identical in both files. If your source root carries xmlns="http://example.com/orders", <xsl:template match="order"> asks for {}order while the document holds {http://example.com/orders}order. No template fires, and XSLT does not consider that an error.
Declare the namespace in the stylesheet under a prefix of your choosing and use it in every match and every select. XSLT 1.0 shares the XPath 1.0 limitation, so the prefix is not optional, and xpath-default-namespace does not help because it is XSLT 2.0 and browsers ignore it.
A stylesheet declaring version="2.0" or "3.0" is flagged before it runs, because both engines are 1.0 and later constructs can fail silently, giving output that is subtly wrong rather than absent. And if the stylesheet root is not in http://www.w3.org/1999/XSL/Transform, nothing works at all: that URI is identical for all three XSLT versions, and a misspelt one compiles to a stylesheet with no rules in it.
What no browser XSLT engine can do
XSLT 1.0 only. No browser has ever shipped 2.0 or 3.0, and libxslt does not implement them either, so the WebAssembly fallback does not raise the ceiling. That rules out xsl:function, xsl:for-each-group (grouping is still the Muenchian key() technique), xsl:analyze-string and xsl:result-document. Saxon-JS is the only route to 2.0 or 3.0 in a browser.
External resources are the other hard limit. document(), xsl:include and xsl:import of remote URLs are governed by the same-origin policy, so a stylesheet pulling in a lookup table over HTTP will fail. Nothing here fetches on your behalf and there is no proxy. A few engine-specific gaps also catch people out:
- disable-output-escaping is not implemented in Firefox: TransforMiiX produces a tree rather than a serialised string, and the spec permits a processor to recover by treating the attribute as no. Use a numeric character reference such as   instead.
- xsl:namespace-alias is unsupported in Firefox, as is the XPath namespace:: axis.
- xsl:output is largely advisory when the processor is driven from script, because the result is a DOM: indent, encoding and the doctype properties have little effect. This page reads method from it to decide between text and serialised markup.
- EXSLT coverage differs. libxslt, used by Chrome, Safari and the WebAssembly fallback, implements most of it. Firefox implements a subset.
- transformToDocument and transformToFragment return null on failure instead of throwing, and the real reason appears only in the console.
Doing this in code
A stylesheet is executable input. Every engine below can read files, open network connections or write to disk through document(), xsl:import or an extension function, so each sample turns that off. Treat a stylesheet you did not write like a script you did not write.
// Native first, WebAssembly fallback. A probe, not a feature check:
// after Chrome 158 the constructor is gone, but a constructor that
// exists and fails is the case that actually misleads you.
function nativeXsltWorks() {
if (typeof XSLTProcessor === 'undefined') return false;
try {
const p = new DOMParser();
const xsl = p.parseFromString(
'<xsl:stylesheet version="1.0" ' +
'xmlns:xsl="http://www.w3.org/1999/XSL/Transform">' +
'<xsl:template match="/"><ok/></xsl:template></xsl:stylesheet>',
'application/xml');
const proc = new XSLTProcessor();
proc.importStylesheet(xsl);
const out = proc.transformToDocument(
p.parseFromString('<a/>', 'application/xml'));
return out?.documentElement?.nodeName === 'ok';
} catch {
return false;
}
}
async function transform(xmlText, xslText) {
// xslt-polyfill is libxslt compiled to WebAssembly. Importing it
// installs a replacement XSLTProcessor on globalThis.
if (!nativeXsltWorks()) await import('xslt-polyfill');
const p = new DOMParser();
const xml = p.parseFromString(xmlText, 'application/xml');
const xsl = p.parseFromString(xslText, 'application/xml');
if (xml.querySelector('parsererror') || xsl.querySelector('parsererror')) {
throw new Error('one of the two inputs is not well-formed');
}
const proc = new XSLTProcessor();
proc.importStylesheet(xsl);
proc.setParameter(null, 'lang', 'en'); // (namespaceURI, name, value)
// Returns null rather than throwing. Do not skip this check.
const out = proc.transformToDocument(xml);
if (!out) throw new Error('no template matched the document root');
return new XMLSerializer().serializeToString(out);
}from lxml import etree
# lxml wraps libxslt, the same engine Chrome and Safari use.
parser = etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False)
xml = etree.fromstring(xml_bytes, parser)
xsl = etree.fromstring(xsl_bytes, parser)
# DENY_ALL blocks document(), xsl:include, xsl:import and every EXSLT
# file or network operation. Without it a stylesheet is a file-read
# primitive that runs whatever its author wrote.
transform = etree.XSLT(xsl, access_control=etree.XSLTAccessControl.DENY_ALL)
try:
result = transform(xml, lang="'en'") # params are XPath: quote strings twice
except etree.XSLTApplyError as e:
raise SystemExit(f'transform failed: {e}')
# xsl:message output and warnings land here, not in the result.
for entry in transform.error_log:
print(f'{entry.line}: {entry.message}')
print(str(result)) # honours xsl:output method, encoding and indentimport javax.xml.XMLConstants;
import javax.xml.transform.*;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
TransformerFactory factory = TransformerFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
// An empty string means "no protocol is permitted", which blocks
// document(), xsl:import and xsl:include of anything outside the
// stylesheet you handed in. Neither is restricted by default.
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
// Without a listener, compilation warnings go to stderr and are lost.
factory.setErrorListener(new ErrorListener() {
public void warning(TransformerException e) {
System.err.println("warning: " + e.getMessageAndLocation());
}
public void error(TransformerException e) throws TransformerException {
throw e;
}
public void fatalError(TransformerException e) throws TransformerException {
throw e;
}
});
// Templates is compiled once and is thread-safe; Transformer is not.
Templates templates = factory.newTemplates(new StreamSource(stylesheetStream));
Transformer transformer = templates.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setParameter("lang", "en");
transformer.transform(new StreamSource(xmlStream), new StreamResult(System.out));
// The JDK's built-in processor is Xalan, XSLT 1.0. For 2.0 or 3.0 put
// Saxon-HE on the classpath; it registers itself as the factory.using System.Xml;
using System.Xml.Xsl;
var readerSettings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null,
};
var transform = new XslCompiledTransform();
// XsltSettings.Default disables both the document() function and
// embedded <msxsl:script>. XsltSettings.TrustedXslt enables both and
// should never be used on a stylesheet from outside your codebase.
// The null resolver blocks xsl:import and xsl:include as well.
using (var xslReader = XmlReader.Create(stylesheetPath, readerSettings))
{
transform.Load(xslReader, XsltSettings.Default, null);
}
var args = new XsltArgumentList();
args.AddParam("lang", "", "en");
using var xmlReader = XmlReader.Create(inputPath, readerSettings);
using var writer = XmlWriter.Create(Console.Out, transform.OutputSettings);
transform.Transform(xmlReader, args, writer);
// XslCompiledTransform is XSLT 1.0. Microsoft never shipped 2.0 for
// .NET; Saxonica's Saxon-HE has a .NET build if you need it.<?php
// Requires ext-xsl, which wraps libxslt.
$xml = new DOMDocument();
$xml->loadXML($xmlSource, LIBXML_NONET);
$xsl = new DOMDocument();
$xsl->loadXML($xslSource, LIBXML_NONET);
$proc = new XSLTProcessor();
// Block every filesystem and network operation a stylesheet can reach
// for. Only the write prefs are blocked by default.
$proc->setSecurityPrefs(
XSL_SECPREF_READ_FILE
| XSL_SECPREF_WRITE_FILE
| XSL_SECPREF_CREATE_DIRECTORY
| XSL_SECPREF_READ_NETWORK
| XSL_SECPREF_WRITE_NETWORK
);
$proc->importStylesheet($xsl);
$proc->setParameter('', 'lang', 'en');
$result = $proc->transformToXml($xml);
if ($result === false) {
fwrite(STDERR, "transformation failed\n");
exit(1);
}
echo $result;# xsltproc ships with libxslt: the same engine Chrome and Safari use,
# and the same one this page falls back to in WebAssembly.
# --nonet stops it fetching anything the stylesheet references.
xsltproc --nonet style.xsl doc.xml > out.html
# --stringparam passes a string. --param takes an XPath expression, so
# a literal string needs its own quotes inside the shell quotes.
xsltproc --nonet --stringparam lang en style.xsl doc.xml
xsltproc --nonet --param year "'2026'" style.xsl doc.xml
# xsl:message output goes to stderr, so separate it before piping.
xsltproc --nonet style.xsl doc.xml 2> messages.log | xmllint --format -
# XSLT 2.0 and 3.0 need Saxon. libxslt will never implement them.
java -cp saxon-he.jar net.sf.saxon.Transform \
-s:doc.xml -xsl:style.xsl -o:out.html lang=enThe parameter syntax differs in every one of these, and not cosmetically: in lxml, in xsltproc --param and in Saxon, a parameter value is an XPath expression, so the string en must be written "'en'" or it is read as a path step selecting elements named en. setParameter in JavaScript and XsltArgumentList in .NET take plain strings.
Common questions
Will this tool stop working when Chrome removes XSLT?
No. Chrome 158, on 17 November 2026, stops XSLT working on stable, and Chrome 176 on 17 August 2027 removes XSLTProcessor and the xml-stylesheet processing instruction outright. A tool built on the native processor alone would break on those dates.
This page runs a one-line probe transformation first. While your browser has a working native engine that is what gets used; when the probe fails, a WebAssembly build of libxslt is loaded instead and the panel names which engine ran. The fallback is fetched only when needed, so Firefox and Safari users never download it.
Can I run XSLT 2.0 or 3.0 here?
No, and neither can any browser. Chrome and Safari use libxslt, Firefox uses its own TransforMiiX, and all three implement 1.0. The WebAssembly fallback is libxslt too, so it does not change the ceiling.
If your stylesheet declares version="2.0" or "3.0", this page warns before it runs rather than after, because a 1.0 processor does not reject a 2.0 stylesheet outright: it runs what it recognises and quietly mishandles the rest. Saxon is the practical answer, Saxon-JS in a browser and Saxon-HE on the JVM or .NET.
Why does my stylesheet produce an empty result?
Almost always a namespace mismatch. If your source declares a default namespace on its root, such as xmlns="http://example.com/orders", every unprefixed element inside it belongs to that namespace. A template written as <xsl:template match="order"> asks for an order element in no namespace, so no rule fires and the output is empty. XSLT does not treat that as an error.
Declare the namespace in the stylesheet with a prefix, say xmlns:o="http://example.com/orders", then write match="o:order" and select="o:line" everywhere. Two other causes to rule out: a stylesheet root not in http://www.w3.org/1999/XSL/Transform, and no template matching the document root.
Are my XML and stylesheet uploaded?
Neither. Both panes stay in this tab: the well-formedness check runs in a Web Worker, and the transformation runs either on your browser's built-in processor or on a WebAssembly module loaded into the page. There is no server here to receive them.
That is a sharper question for XSLT than for most tools, because the stylesheet is often the valuable half: a transform mapping a partner's schema onto your internal one encodes business rules, field names and sometimes endpoints. One consequence of running locally is that the output is shown as text and never inserted into the page as markup, since a stylesheet can generate script elements.
Why does my output contain all the text of my document with no markup?
You have hit XSLT's built-in template rules. The spec defines defaults that exist whether you write them or not: the rule for elements applies templates to their children, and the rule for text nodes copies the text straight through.
So when none of your own templates matches, the processor does not stop. It walks the whole tree using those defaults and prints every text node it finds, source indentation included. The cause is usually a namespace. The quick diagnostic is to add <xsl:template match="text()"/>, which suppresses the text default: if the output goes empty, no rule of yours was firing.
What is the difference between apply-templates and for-each?
xsl:for-each iterates inline. It selects a node list and runs its body once per node in document order, and that body is fixed, so every node is treated the same way. It reads like a loop and suits a flat, homogeneous list, such as turning every row element into a table row.
xsl:apply-templates hands control back to the rule set, letting each selected node find the template that best matches it, so different element types get different handling with no conditionals. It also takes a mode attribute, so the same node can be processed twice under different rules, once for a contents list and once for the body. for-each has no equivalent.
Can the stylesheet load another file with document() or xsl:import?
Not here. document(), xsl:include and xsl:import all resolve a URL, and in a browser those loads are subject to the same-origin policy, so anything remote is blocked. This page also has no fetch proxy, deliberately: proxying would mean the target document, and the URL itself, passing through a server this site controls.
Inline the second document into your source and select it with plain XPath, move the lookup into a stylesheet parameter, or run the transform locally with xsltproc, where document() resolves against your filesystem.