How to Use Tunnel Parameters in Modern XSLT

In modern XSLT (XSLT 2.0 and later), tunnel parameters provide a mechanism to pass values from an ancestor template directly to a descendant template without requiring intermediate templates to explicitly declare or forward those parameters. This article explains how tunnel parameters work, demonstrates their syntax, and illustrates how they streamline XML transformation pipelines by eliminating repetitive parameter boilerplate.

The Problem with Standard Parameters

In standard XSLT parameter passing, if a template at the top level needs to pass a parameter to a template several layers deep in the XML tree, every intermediate template must participate in the chain. Each intermediate template must declare <xsl:param> and pass it forward using <xsl:with-param>. This tightly couples templates and clutters stylesheets with redundant parameter propagation code.

How Tunnel Parameters Work

Tunnel parameters solve this issue by allowing values to “tunnel” through the call tree. You declare a tunnel parameter when calling or applying templates, and only the templates that actually need the value declare it. Intermediate templates simply invoke <xsl:apply-templates/> or <xsl:call-template> without mentioning the parameter at all.

To pass a tunnel parameter: 1. Set tunnel="yes" on the <xsl:with-param> element at the source call. 2. Allow intermediate templates to process nodes normally without declaring the parameter. 3. Set tunnel="yes" on the <xsl:param> element in the destination template to receive the value.

Implementation Example

Consider an XML document where a document-level setting must reach deeply nested child elements:

<catalog title="Spring Collection">
  <category name="Apparel">
    <item id="101">T-Shirt</item>
  </category>
</catalog>

The corresponding XSLT stylesheet passes the catalog title directly to the <item> template across intermediate <category> processing:

<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <!-- Root template: Initiates the tunnel parameter -->
  <xsl:template match="/catalog">
    <output>
      <xsl:apply-templates select="category">
        <xsl:with-param name="catalogTitle" select="@title" tunnel="yes" />
      </xsl:apply-templates>
    </output>
  </xsl:template>

  <!-- Intermediate template: No parameter declaration needed -->
  <xsl:template match="category">
    <section name="{@name}">
      <xsl:apply-templates select="item" />
    </section>
  </xsl:template>

  <!-- Target template: Receives the tunnel parameter -->
  <xsl:template match="item">
    <xsl:param name="catalogTitle" tunnel="yes" />
    <product catalog="{$catalogTitle}">
      <xsl:value-of select="." />
    </product>
  </xsl:template>

</xsl:stylesheet>

Overriding Tunnel Parameters

Tunnel parameters remain active down the execution tree until explicitly overridden. If an intermediate template needs to modify or replace the tunneled value for subsequent descendants, it can issue a new <xsl:with-param name="..." select="..." tunnel="yes"/> in its own <xsl:apply-templates> call. Descendants below that point will receive the newly defined value, while sibling branches retain the original value.