Template matching is the mechanism at the heart of XSLT, and it is where the most subtle bugs live. Unlike a function call where you explicitly name the code to execute, template matching relies on pattern declarations and priority rules to determine which template fires for each node. When things go wrong, the symptoms are usually silent: wrong output, missing output, or output from an unexpected template.

Over-Generic Match Patterns

The most common pitfall is writing templates with match patterns that are more generic than intended:

<xsl:template match="*">
  <!-- This matches every element -->
</xsl:template>

A wildcard template like this overrides all default behavior and can silently suppress output from elements that lack more specific templates. Use wildcards only when you genuinely want to handle all unmatched elements, and always give specific templates a higher priority.

Priority Conflicts

When multiple templates can match the same node, XSLT uses a priority system to choose. Default priorities are computed from the match pattern specificity:

  • match="*" has priority -0.5
  • match="element" has priority 0
  • match="parent/element" has priority 0.5
  • match="ns:element" has priority 0

When two templates have the same computed priority for the same node, the XSLT processor should report an error but is allowed to choose either template. In practice, most processors choose the last template in document order, which makes template ordering significant in ways that are easy to overlook.

If you have competing templates and want deterministic behavior, set explicit priorities:

<xsl:template match="line" priority="2">
  <!-- This always wins over a default-priority match -->
</xsl:template>

Import Precedence Traps

When stylesheets import other stylesheets via xsl:import, the importing stylesheet’s templates have higher precedence than imported templates. This is usually what you want, but it creates problems when you import multiple stylesheets that define templates for the same elements.

The precedence rule: later imports override earlier imports, and the importing stylesheet overrides everything. But the interaction between import precedence and template priority is non-obvious. A high-priority template in an imported stylesheet still loses to a default-priority template in the importing stylesheet.

In practice, keep import chains shallow and document which stylesheet is responsible for which element types. Deep import chains with overlapping template coverage are a debugging nightmare.

Mode Confusion

Template modes allow the same element to be processed differently in different contexts. But modes must be used consistently. Calling xsl:apply-templates without a mode will not trigger templates declared with a mode, and vice versa.

<!-- This template only fires in "toc" mode -->
<xsl:template match="section" mode="toc">
  <li><xsl:value-of select="title"/></li>
</xsl:template>

<!-- This call will NOT trigger the template above -->
<xsl:apply-templates select="section"/>

<!-- This call WILL trigger it -->
<xsl:apply-templates select="section" mode="toc"/>

Missing or mismatched mode specifications produce missing output without any error message. When output is unexpectedly absent, check mode consistency between template declarations and apply-templates calls.

Built-in Template Rules

XSLT has built-in template rules that handle unmatched nodes. The most important one recursively processes child elements. This means that if you have no template for a container element, its children will still be processed by their own templates.

This is usually helpful, but it masks missing templates for container elements. If you expect a container element to produce wrapper HTML but forget to write a template for it, the built-in rule processes the children directly, producing output without the wrapper. The output looks almost right but the structure is wrong.

Add explicit templates for all significant structural elements, even if they just wrap apply-templates in a div or section element.

Diagnosis Approach

When template matching behaves unexpectedly:

  1. Add xsl:message to all candidate templates to see which one fires.
  2. Check namespace declarations if no template fires at all.
  3. Check priorities if the wrong template fires.
  4. Check import precedence if templates from imported stylesheets are being overridden.
  5. Check modes if templates fire in one context but not another.

For a systematic debugging approach, see the XSLT debugging workflow guide. For namespace-specific matching issues, see the namespace handling note.