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-templates hands control back to the engine, which finds the best matching template. for-each takes 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.
  • mode lets 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 methodProducesWatch out for
xmlWell-formed XMLEntities and empty-element collapsing
htmlHTML with void elementsNot well-formed, so not reusable as XML
textRaw textNo escaping at all; only for CSV and logs
xhtmlXML that browsers render as HTMLNamespace and content-type handling
jsonXSLT 3.0 outputRequires 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.

Parsing XML in code: DOM, SAX and pull parsers SVG, XHTML and XML in the browser

Last refreshed 2026-09-18.