XSLT basics
XSLT describes what a document should become, not the steps for building it. You write rules that say "when you meet a book element, produce this", and the processor runs them by walking the source tree. XSLT 1.0 became a W3C Recommendation on 16 November 1999 and is still the only version any browser implements, so everything below is 1.0.
Two failures account for most first stylesheets: the output is empty, or it is a wall of unstyled text with none of your markup in it. Both have one cause. The processor already has default behaviour, it is doing something, and you have not overridden it.
#Declarative, and what that costs you
XSLT 1.0 section 5.1 gives the processing model: a stylesheet is a set of template rules, processing starts at the root node of the source tree, and for each node the processor finds the rule that best matches it and instantiates that rule's content. There is no main function and no statement order across rules. You control the walk, not the schedule.
- Variables are bound once and never reassigned. Section 11 binds a name to a value for the scope of its containing element. There are no counters and no i = i + 1. Where a procedural language would loop and mutate, XSLT recurses or uses position() and sum().
- The source tree is read only. Every transformation constructs a separate result tree.
- A node that nothing recurses into is never visited. Rules do not fire because a node exists, they fire because something applied templates to it.
The payoff is that stylesheets compose. Handling a new element type is one more rule at the bottom of the file, and you do not need to know where in the document that element turns up.
#Template rules and match patterns
A template rule is xsl:template with a match attribute holding a pattern. Section 5.2 defines a pattern as a restricted subset of XPath: a union of location paths using only the child and attribute axes, plus //, predicates, id() and key(). match="book[@year > 2000]" is a pattern; match="following-sibling::note" is not.
<catalogue>
<book id="b1" year="2004">
<title>XML in a Nutshell</title>
<author>Elliotte Rusty Harold</author>
</book>
<book id="b2" year="1999">
<title>XSLT Programmer's Reference</title>
<author>Michael Kay</author>
</book>
</catalogue><xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" indent="yes"/>
<xsl:template match="/">
<html>
<body>
<h1>Catalogue</h1>
<ul><xsl:apply-templates select="catalogue/book"/></ul>
</body>
</html>
</xsl:template>
<xsl:template match="book">
<li>
<xsl:value-of select="title"/>
<xsl:text> by </xsl:text>
<xsl:value-of select="author"/>
</li>
</xsl:template>
</xsl:stylesheet>When two rules match the same node, section 5.5 resolves the conflict by import precedence and then by priority. You can set priority yourself, but every pattern has a default derived from its shape, and those four values explain why a specific rule beats a general one without you asking for it.
| Pattern shape | Priority | Example |
|---|---|---|
| A QName, or processing-instruction with a literal target | 0 | match="book" |
| A prefix followed by a wildcard | -0.25 | match="atom:*" |
| A bare node test | -0.5 | match="*", match="text()" |
| Anything else: paths, predicates, unions, id(), key() | 0.5 | match="book[@id]" |
Two rules of equal priority matching the same node is an error, and section 5.5 lets a processor recover by taking the one that comes last in the stylesheet. Every processor does exactly that, silently. If a rule is not firing, look for a later rule of the same priority before suspecting the pattern.
#apply-templates versus for-each
xsl:for-each is the construct that looks familiar, which is why beginners reach for it. It is not really a loop: it rebinds the context node to each member of a node-set and instantiates its body inline, so the treatment is fixed at the call site. xsl:apply-templates hands each node to whichever rule matches it, so the treatment is decided by the node.
<xsl:template match="/">
<ul>
<xsl:for-each select="catalogue/book">
<li>
<xsl:value-of select="title"/>
<xsl:for-each select="author">
<span><xsl:value-of select="."/></span>
</xsl:for-each>
</li>
</xsl:for-each>
</ul>
</xsl:template><xsl:template match="/">
<ul><xsl:apply-templates select="catalogue/book"/></ul>
</xsl:template>
<xsl:template match="book">
<li>
<xsl:value-of select="title"/>
<xsl:apply-templates select="author"/>
</li>
</xsl:template>
<xsl:template match="author">
<span><xsl:value-of select="."/></span>
</xsl:template>With five element types the difference is cosmetic. With a recursive format, a DocBook chapter or an XHTML fragment where a list can contain a list, for-each cannot express the recursion without writing the nesting out by hand to whatever depth you guess. apply-templates handles arbitrary depth, because the rule for a list applies templates to its children and one of those children may be a list again.
apply-templates also has modes, section 5.7. The same node can be processed twice under mode="toc" and mode="body", so a table of contents and the document body come from one pass with no duplicated selection logic. for-each has no equivalent.
for-each is still right for a sorted selection at the point of use, or a one-off piece of formatting nothing else will want. Reaching for it as the default is the mistake, not using it at all.
#The built-in rules, and the wall of text
Section 5.8 defines built-in template rules that exist in every stylesheet, at lower import precedence than anything you write. They explain almost every confused bug report about XSLT.
<!-- Root and elements: recurse into children, produce nothing. -->
<xsl:template match="*|/">
<xsl:apply-templates/>
</xsl:template>
<!-- Text and attributes: copy the string value into the result. -->
<xsl:template match="text()|@*">
<xsl:value-of select="."/>
</xsl:template>
<!-- Comments and processing instructions: produce nothing. -->
<xsl:template match="processing-instruction()|comment()"/>The behaviour follows mechanically. Apply an empty stylesheet to any document and the result is that document's concatenated text content, whitespace and all, with every tag stripped. A stylesheet with only a rule for match="book" still works, because the built-in root rule recurses down until it reaches yours.
The failure case is a rule that matches something your apply-templates never reaches, combined with an xsl:apply-templates that has no select attribute. The built-ins take over, walk the rest of the tree and dump its text. One line stops it.
<xsl:template match="text()"/>The built-ins apply per mode too: an unhandled node inside mode m recurses in mode m, not in the default one.
#value-of, copy-of, copy
These three instructions are the whole of "put something in the output", and picking the wrong one loses data quietly.
- xsl:value-of, section 7.6.1, converts an expression to a string and inserts a text node. Markup in the selected nodes is discarded; only character data survives.
- xsl:copy-of, section 11.3, deep-copies the selected nodes themselves, with their attributes, descendants and namespace nodes.
- xsl:copy, section 7.5, copies the current node only: no attributes and no children unless you produce them inside it.
xsl:copy is the basis of the identity transform, the most useful six lines in the language. It copies a document unchanged, so you write it once and then override only the rules for the parts you want altered. Every "change one thing, leave the rest alone" task is this pattern.
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<!-- A more specific pattern wins on priority (0 beats -0.5), so this
strips every price element and everything else passes through
untouched, comments and processing instructions included. -->
<xsl:template match="price"/>#Output methods, parameters and sorting
xsl:output, section 16, declares how the result tree should be serialised: method is xml, html or text, plus indent, encoding, omit-xml-declaration, doctype-public, doctype-system and cdata-section-elements. method="html" writes empty elements as <br> rather than <br/> and does not escape the content of script and style. method="text" emits text nodes only.
A top-level xsl:param declares a stylesheet parameter with a default; the caller overrides it, which from script is XSLTProcessor.setParameter, whose first argument is a namespace URI and is almost always null. Inside the stylesheet, xsl:with-param passes values into a specific apply-templates or call-template. The only difference from xsl:variable is that a parameter can be set from outside. Neither can be reassigned.
xsl:sort, section 10, must be the first child of the xsl:apply-templates or xsl:for-each it applies to, and further xsl:sort elements give secondary keys. The attribute that catches everyone is data-type, which defaults to "text".
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:param name="order" select="'ascending'"/>
<xsl:template match="/">
<xsl:apply-templates select="catalogue/book">
<!-- Drop data-type and this becomes a string sort, in which
"10" sorts before "9". order is an attribute value
template, so a parameter can be substituted into it. -->
<xsl:sort select="@year" data-type="number" order="{$order}"/>
<xsl:sort select="title" data-type="text" case-order="upper-first"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="book">
<xsl:value-of select="@year"/>
<xsl:text>	</xsl:text>
<xsl:value-of select="title"/>
<xsl:text> </xsl:text>
</xsl:template>
</xsl:stylesheet>const parser = new DOMParser();
const xslDoc = parser.parseFromString(xslText, 'application/xml');
const xmlDoc = parser.parseFromString(xmlText, 'application/xml');
if (xslDoc.querySelector('parsererror') || xmlDoc.querySelector('parsererror')) {
throw new Error('input is not well-formed');
}
const proc = new XSLTProcessor();
proc.importStylesheet(xslDoc);
proc.setParameter(null, 'order', 'descending'); // (namespaceURI, name, value)
// Both transform methods return null on failure rather than throwing,
// and the underlying error is written only to the console.
const outDoc = proc.transformToDocument(xmlDoc);
if (!outDoc) throw new Error('transformation failed');
const text = new XMLSerializer().serializeToString(outDoc);#The namespace trap that produces nothing
A stylesheet written against un-namespaced XML matches nothing at all against a namespaced document, and it does so without raising a single error. This is the most common reason an XSLT transformation "does not work".
Two specifications meet here. Namespaces in XML 1.0 section 6.1 says a default namespace declaration applies to every unprefixed element name in its scope, so in a feed with xmlns="http://www.w3.org/2005/Atom" on the root, the element written as <entry> has the expanded name {http://www.w3.org/2005/Atom}entry. XPath 1.0, which XSLT patterns are built on, has no concept of a default namespace: an unprefixed name test matches only names in no namespace. match="entry" asks for {}entry, and those are different names.
<!-- feed.xml carries xmlns="http://www.w3.org/2005/Atom" on <feed> -->
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/feed">
<html><body>
<xsl:for-each select="entry">
<h2><xsl:value-of select="title"/></h2>
</xsl:for-each>
</body></html>
</xsl:template>
</xsl:stylesheet>
<!-- No error is reported. The rule never fires, the built-in rules
take over, and the output is the feed's text content with no
markup at all. --><xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:atom="http://www.w3.org/2005/Atom"
exclude-result-prefixes="atom">
<xsl:template match="/atom:feed">
<html><body>
<xsl:for-each select="atom:entry">
<h2><xsl:value-of select="atom:title"/></h2>
</xsl:for-each>
</body></html>
</xsl:template>
</xsl:stylesheet>Three details make the fix reliable. The prefix in the stylesheet need not match the prefix in the document, and usually the document has none; only the URI has to match. Namespace names are compared character by character with no normalisation, so a trailing slash makes a different namespace and will cost you an afternoon. And exclude-result-prefixes stops your working prefix leaking into the output as a stray xmlns:atom declaration.
Because the failure is silent, the transformer on this site checks for it: when a transformation succeeds but produces an empty result, it names the namespace mismatch as the likely cause instead of showing a blank panel. No processor can distinguish "matched nothing" from "deliberately produced nothing", so it is a suggestion rather than a diagnosis.
Common questions
Why does my stylesheet output all the text of my document with no tags?
Because the built-in template rules in XSLT 1.0 section 5.8 are handling the nodes your own rules did not. The built-in rule for elements recurses into children, and the one for text nodes copies their string value into the result, so a document nothing matches renders as its concatenated text.
Two fixes, and you usually want both. Add <xsl:template match="text()"/> so the default text handling produces nothing, and check whether your patterns match at all, which on a namespaced document usually means they do not.
Can I use XSLT 2.0 or 3.0 in a browser?
No. Chrome and Safari embed libxslt, Firefox uses its own TransforMiiX processor, and all three are XSLT 1.0 only. The namespace URI http://www.w3.org/1999/XSL/Transform is the same for all three versions, so a stylesheet declaring version="2.0" loads and then fails at the first 2.0 construct, sometimes silently.
The features people miss are xsl:function, xsl:for-each-group and xsl:analyze-string, all 2.0. In a browser that means Saxon-JS, which is an XSLT 3.0 implementation in JavaScript. On a server, Saxon has done 2.0 and 3.0 for years.
What is the difference between xsl:variable and xsl:param?
Only that a parameter can be given a value from outside: XSLTProcessor.setParameter from script, or --param on a command line processor, falling back to its own default. Neither can be reassigned once bound, which is why there is no way to build a running total by incrementing something. Use sum(), or recursion with xsl:call-template and xsl:with-param.
One trap in the same area: a variable whose value comes from its content rather than a select attribute holds a result tree fragment, and XSLT 1.0 forbids applying location paths to it. libxslt offers the EXSLT exsl:node-set() extension to convert one, so it works in Chrome and Safari; Firefox implements a narrower subset of EXSLT, so depending on it is not portable.
Why does my sort put 10 before 9?
Because xsl:sort defaults to data-type="text", and as strings "10" does sort before "9". Add data-type="number".
A related trap in the same instruction: xsl:sort must be the first child of the xsl:apply-templates or xsl:for-each it applies to. Put anything before it and most processors reject the stylesheet.
My xsl:output indent="yes" is ignored. Why?
Because you are running the transformation from JavaScript. transformToDocument and transformToFragment hand back a DOM, and serialisation is the step where indent, encoding, omit-xml-declaration and the doctype attributes take effect. That step never runs.
The same stylesheet indents fine under xsltproc or Saxon, which serialise the result themselves. In the browser, serialise with XMLSerializer and format the string afterwards if you need it readable.