Transforming XML with XSLT templates
How a stylesheet is structured, template matching and apply-templates, choosing an output method, and reshaping one vocabulary into another with identity transforms.
A stylesheet is a set of templates
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:s="urn:example:shop"
exclude-result-prefixes="s">
<xsl:output method="xml" indent="yes" omit-xml-declaration="no"/>
<!-- entry point: match the document node -->
<xsl:template match="/">
<html>
<body>
<h1>Orders</h1>
<table>
<xsl:apply-templates select="//s:order"/>
</table>
</body>
</html>
</xsl:template>
<xsl:template match="s:order">
<tr>
<td><xsl:value-of select="@id"/></td>
<td><xsl:value-of select="s:total"/></td>
<td><xsl:value-of select="count(s:items/s:item)"/></td>
</tr>
</xsl:template>
</xsl:stylesheet>apply-templateshands control back to the engine, which finds the best matching template.for-eachtakes control instead and is harder to extend.- A more specific match wins, and priorities and import order break any remaining tie.
- Built-in templates exist for every node type, which is why an unmatched element still emits its text.
modelets you define two different treatments for the same element in one stylesheet.
The identity transform
<!-- copy everything unchanged by default -->
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<!-- then override just what you want to change -->
<xsl:template match="s:total">
<xsl:copy>
<xsl:attribute name="currency">EUR</xsl:attribute>
<xsl:value-of select="."/>
</xsl:copy>
</xsl:template>The identity transform plus a handful of overrides is the safe way to make a small change to a large vocabulary. It preserves everything you did not mention, instead of silently dropping it.
| Output method | Produces | Watch out for |
|---|---|---|
xml | Well-formed XML | Entities and empty-element collapsing |
html | HTML with void elements | Not well-formed, so not reusable as XML |
text | Raw text | No escaping at all; only for CSV and logs |
xhtml | XML that browsers render as HTML | Namespace and content-type handling |
json | XSLT 3.0 output | Requires an XSLT 3.0 processor |
Reshaping and grouping
<!-- XSLT 1.0 has no group-by: use the Muenchian method -->
<xsl:key name="bySku" match="s:item" use="@sku"/>
<xsl:template match="/">
<summary>
<xsl:for-each select="//s:item[generate-id() = generate-id(key('bySku', @sku)[1])]">
<product sku="{@sku}">
<quantity><xsl:value-of select="sum(key('bySku', @sku)/@qty)"/></quantity>
</product>
</xsl:for-each>
</summary>
</xsl:template># run a transform on the command line
xsltproc --output report.html report.xsl orders.xml
# validate the stylesheet itself first
xmllint --noout report.xsl
# test with a tiny fixture that exercises each template
xsltproc report.xsl fixture-minimal.xml- XSLT 2.0 and 3.0 add
xsl:for-each-group, which replaces the Muenchian key trick entirely. - Attribute value templates use braces inside an attribute, which is how
sku="{@sku}"works. - A transform over a huge document should stream; that requires XSLT 3.0 and a streaming-capable processor.
- Keep business logic out of the stylesheet where you can — it is hard to unit test and easy to duplicate in three places.
⚠️
Do not transform untrusted XML with a stylesheet from an untrusted source. XSLT is a programming language with access to functions such as
document(), which can read files or make network requests from inside a transform.FAQ
Which XSLT version should I target?
1.0 runs everywhere, including browsers. 2.0 or 3.0 is far more capable for grouping and functions, but requires a processor such as Saxon and is not available in the browser.
Why did my transform drop an element?
There was no matching template and no identity transform, so the built-in rule emitted only its text. Add the identity template and override selectively.
Related
Parsing XML in code: DOM, SAX and pull parsers SVG, XHTML and XML in the browser
Last refreshed 2026-09-18.