Tag Files and Tag Libraries

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Jakarta Faces 4.1 (API jakarta.faces-api 4.1.2, part of Jakarta EE 11), Eclipse Mojarra 4.1.17 and Apache MyFaces Core 4.1.4, with Java 17 or later (the Java classes were compiled with --release 21). Faces 4.0 behaves the same for everything below, except for the schema version noted in What Changed per Version. The listings are files, or excerpts of files, of a scratch Bookshelf WAR that was deployed on Apache Tomcat 11.0.26 with Weld 6.0.4.Final, once with Mojarra and once with MyFaces, and requested with curl: the output and the error messages quoted come from those runs (checked on 2026-10-08). Where the two implementations disagree, the text says so. Faces 5.0 is still a milestone release and is not described as current behaviour.

Facelets pages use tags such as h:inputText and ui:insert. Each tag belongs to a tag library, and the library is found through the XML namespace the page declares. The standard libraries (jakarta.faces.html, jakarta.faces.core, jakarta.faces.facelets, jakarta.faces.composite) are the libraries every implementation ships; the specification describes the same mechanism for tag libraries written by the application author or by a third party. This page shows the four things an application can put into such a library, in increasing order of power:

  • a tag file, an XHTML file that becomes a tag, with no Java at all

  • a tag that already exists in Faces, such as a converter, a validator or a component, under a name of your choice

  • a tag handler, a Java class that runs while the view is being built

  • an EL function, a static method that expressions can call

The page ends with a decision guide for choosing between these, a composite component and a custom component.

The Tag Library Descriptor

A library is described by an XML file whose name ends in .taglib.xml and whose root element is facelet-taglib. The Bookshelf library uses one namespace, urn:org.example.bookshelf:tags, and any string is allowed there: it is only a key. The file starts like this, and the entries that follow are shown one by one in the rest of the page:

<facelet-taglib xmlns="https://jakarta.ee/xml/ns/jakartaee"
                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-facelettaglibrary_4_1.xsd"
                version="4.1">
    <namespace>urn:org.example.bookshelf:tags</namespace>
    <short-name>bs</short-name>
    ...
</facelet-taglib>

The elements that can appear, as defined by the 4.1 schema:

Element Meaning

namespace

The namespace pages use to select the library. Required unless the library is given as a library-class.

short-name

An advisory short name for tags of the library, "advisory" being the schema’s word. In both runtimes the prefix is whatever the page declares with xmlns:.

tag

One tag, with a tag-name (an XML NCName) and exactly one of source, handler-class, component, converter, validator or behavior, followed by optional attribute declarations. Sections Tag Files to Custom Tag Handlers show each of them.

function

An EL function (function-name, function-class, function-signature), shown in EL Functions.

composite-library-name

Makes every composite component of one resource library (Choosing: Tag File, Composite, Component, Handler or Function) available under this namespace.

library-class

An alternative to listing tags: the fully qualified name of a Java class that provides the library. The schema gives no further definition, so this is implementation specific; it was not used for this page.

Registering a Library

The specification requires two ways to make a library known: a context parameter, and automatic discovery of descriptors packaged in JARs.

  • jakarta.faces.FACELETS_LIBRARIES. A semicolon separated list of paths, each starting with a slash and relative to the web application root. This is how the Jakarta EE Tutorial wires its bookstore.taglib.xml.

  • Auto-discovery. A file whose name ends in .taglib.xml, placed in the META-INF directory of a JAR on the web application’s class path, is found without any configuration.

The Bookshelf registers its library in web.xml (a second, private descriptor used for the failing examples of this page is listed the same way and is left out):

<context-param>
    <param-name>jakarta.faces.FACELETS_LIBRARIES</param-name>
    <param-value>/WEB-INF/bookshelf.taglib.xml</param-value>
</context-param>

What the runs showed about discovery, identical on Mojarra and MyFaces:

  • A descriptor named META-INF/bookshelf-ext.taglib.xml in a JAR under WEB-INF/lib was found. So was META-INF/cls.taglib.xml placed in WEB-INF/classes, which is where Maven puts src/main/resources/META-INF.

  • A descriptor in WEB-INF/auto.taglib.xml that was not listed in the context parameter was not found. In the development stage the page then shows a warning and the tag is copied to the output as plain text. Mojarra says the page "calls for XML namespace …​ declared with prefix web but no taglibrary exists for that namespace". MyFaces says the page "declares namespace …​ and uses the tag web:flag , but no TagLibrary associated to namespace".

  • A path in the context parameter that does not exist is only logged (Mojarra as a warning that mentions jakarta.faces.CONFIG_FILES, MyFaces as SEVERE: Error loading library). The application starts, and the first page using one of the library’s tags or functions fails: Function [bs:formatIsbn] not found.

  • The old Facelets 1.x parameter facelets.LIBRARIES and the Faces 2.x name javax.faces.FACELETS_LIBRARIES are not read by either runtime in 4.1: the Bookshelf pages failed with the same "not found" error when registered under those names.

The specification also says that a descriptor that cannot be parsed must make the application fail to deploy.

Tag Files

A tag file is the simplest thing a library can contain: an XHTML file, written like any other Facelets file, which a tag entry turns into a tag. The entry’s source element names the file:

<tag>
    <tag-name>bookCard</tag-name>
    <source>tags/bookCard.xhtml</source>
    <attribute>
        <name>title</name>
        <required>true</required>
    </attribute>
</tag>

The file below is the tag. It is a ui:composition, so everything outside that element is ignored by Facelets (the ui:composition VDL page says so), exactly as in a template client (see templates and template clients). Each attribute the page writes on the tag becomes a variable of the same name inside the file, and the body of the tag is inserted where the file has an unnamed ui:insert:

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:h="jakarta.faces.html"
                xmlns:ui="jakarta.faces.facelets">
    <div class="book-card">
        <h3>#{title}</h3>
        <p>by #{author}<ui:fragment rendered="#{not empty year}">, #{year}</ui:fragment></p>
        <ui:insert/>
    </div>
</ui:composition>

A page binds the namespace and uses the tag like any other. The tag is used here inside ui:repeat with the EL functions of EL Functions:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:bs="urn:org.example.bookshelf:tags">
<h:head><title>Cards</title></h:head>
<h:body>
    <ui:repeat value="#{catalogueBean.books}" var="b">
        <bs:bookCard title="#{b.title}" author="#{b.author}" year="#{b.year}">
            <p>ISBN #{bs:formatIsbn(b.isbn)} (#{bs:initials(b.author)})</p>
        </bs:bookCard>
    </ui:repeat>
    <bs:bookCard title="Literal title" author="Someone"/>
    <bs:panel heading="Panel one">Body text<ui:define name="footer">custom footer</ui:define></bs:panel>
    <bs:panel heading="Panel two">Body two</bs:panel>
</h:body>
</html>

The response, identical on both implementations apart from the development-stage message div that MyFaces appends, was one card per book. The first was:

<div class="book-card">
    <h3>Domain-Driven Design</h3>
    <p>by Eric Evans, 2003</p>
        <p>ISBN 978-0-32112-521-7 (EE)</p>
</div>

A tag file is resolved when the view is built (see Where a Handler Runs for what that means in the lifecycle). Nothing in it runs in the later phases, but the components it creates do, so an h:inputText in a tag file converts, validates and updates the model like any other input.

Where the source Path Is Resolved

The source path is resolved relative to the descriptor file, not to the web application root. With the descriptor at /WEB-INF/bookshelf.taglib.xml, the entry tags/bookCard.xhtml is /WEB-INF/tags/bookCard.xhtml. An entry that starts with a slash is not treated as an absolute path: <source>/WEB-INF/tags/panel.xhtml</source> failed on both implementations with "Facelet //WEB-INF/tags/panel.xhtml not found at: file:/WEB-INF/tags/panel.xhtml". A path with ../ did work. For a library packaged in a JAR the same rule means a file next to the descriptor, so the JAR in Packaging a Library in a JAR keeps its tag files in META-INF/tags/.

Attributes of a Tag File

The attributes a tag file receives are not declared anywhere: any attribute the page writes becomes a variable, and any variable the file uses but the page does not pass is looked up as an ordinary EL name. A tag may declare its attributes in the descriptor, with required, type and method-signature as optional children (type defaults to java.lang.Object). The Bookshelf declares the one mandatory attribute of bs:bookCard (the entry above) and nothing else. What happened with a missing mandatory attribute and with an undeclared one:

Page Mojarra 4.1.17 MyFaces 4.1.4

<bs:bookCard author="Someone"/> (the declared title is missing)

HTTP 500, Attribute 'title' is required

HTTP 200, the card is rendered with an empty title

<bs:bookCard title="T" colour="red"/> (colour is not declared)

HTTP 200, the attribute is ignored

HTTP 200, the attribute is ignored

So required="true" on a tag-file tag is enforced by Mojarra and ignored by MyFaces. Do not rely on it for correctness; if the file needs a value, guard against its absence inside the file (for example with rendered="#{not empty title}"), or move to a composite component, whose cc:attribute declarations are part of its interface.

The Body and Named Slots

An unnamed <ui:insert/> receives the body of the tag. A tag file can also declare named slots, which the page fills with ui:define exactly as in a template client; a slot that the page leaves out shows its default content:

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:ui="jakarta.faces.facelets">
    <section class="panel">
        <header>#{heading}</header>
        <div class="body"><ui:insert/></div>
        <footer><ui:insert name="footer">default footer</ui:insert></footer>
    </section>
</ui:composition>

used as:

<bs:panel heading="Panel one">Body text<ui:define name="footer">custom footer</ui:define></bs:panel>
<bs:panel heading="Panel two">Body two</bs:panel>

which rendered, on both implementations, a section whose footer was custom footer in the first case and default footer in the second.

The same file works through ui:decorate and ui:include, which makes the difference between the three visible. With <ui:decorate template="/WEB-INF/tags/bookCard.xhtml"> and ui:param children the body was inserted. With <ui:include src="/WEB-INF/tags/bookCard.xhtml"> the parameters were passed but the body of the ui:include was silently dropped, on both implementations. A tag file is therefore a template that has been given a name, a namespace and a body.

Expressions Passed to a Tag File

An attribute value that is an EL expression is passed on as an expression, not as its value. Three consequences follow, all observed on both runtimes. The tag files:

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:ui="jakarta.faces.facelets">
    <p>first: #{title} / second: #{title}</p>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:ui="jakarta.faces.facelets">
    <p>catalogueBean: #{empty catalogueBean ? 'not set' : 'a CDI bean'}, name: [#{name}]</p>
</ui:composition>

and the page that uses them with the CatalogueBean of the scenario, whose expensiveTitle() increments a counter:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:bs="urn:org.example.bookshelf:tags">
<h:head><title>Expressions</title></h:head>
<h:body>
    <bs:twice title="#{catalogueBean.expensiveTitle()}"/>
    <bs:leak/>
</h:body>
</html>

The output was first: Computed title 1 / second: Computed title 2 and catalogueBean: a CDI bean, name: []:

  • Every use re-evaluates. #{title} used twice called expensiveTitle() twice. An expression with a cost or a side effect (a method call that queries a database, say) should not be passed to a tag file that uses it more than once; pass a cheap, cached bean property instead.

  • An attribute the page did not pass is a plain EL name. #{catalogueBean} was never an attribute of bs:leak, so the expression found the CDI bean of that name. A page that forgets an optional attribute named like a bean or an implicit object therefore silently reads that. Choose attribute names that no bean uses (bookTitle rather than book), or test for empty before using the value. A missing attribute whose name nothing owns evaluates to nothing (name: []).

  • The expression can be written to. The next file passes value straight to an input. Because value is the caller’s expression, the input reads and updates the caller’s property, which makes tag files a good way to build a labelled field.

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:h="jakarta.faces.html"
                xmlns:ui="jakarta.faces.facelets">
    <div class="field">
        <h:outputLabel for="#{name}" value="#{label}"/>
        <h:inputText id="#{name}" value="#{value}"/>
        <h:message for="#{name}"/>
    </div>
</ui:composition>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:bs="urn:org.example.bookshelf:tags">
<h:head><title>Ids</title></h:head>
<h:body>
    <h:form id="f">
        <bs:fieldId name="title" label="Title" value="#{formBean.title}"/>
        <bs:fieldId name="author" label="Author" value="#{formBean.author}"/>
        <h:commandButton id="save" value="Save" action="#{formBean.save}"/>
    </h:form>
    <p id="saved">#{formBean.saved}</p>
</h:body>
</html>

Posting f:title=Dune and f:author=Herbert updated formBean.title and formBean.author on both runtimes (the page printed Saved: Dune / Herbert / null).

Ids: a Tag File Is Not a Naming Container

A tag file adds components to its parent and nothing else; it does not create a naming container. If the file gives its input a fixed id and the page uses the tag twice in one form, the two inputs collide. This variant of the field, with a fixed id, reproduced it:

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:h="jakarta.faces.html"
                xmlns:ui="jakarta.faces.facelets">
    <div class="field">
        <h:outputLabel for="input" value="#{label}"/>
        <h:inputText id="input" value="#{value}" required="#{required}"/>
        <h:message for="input"/>
    </div>
</ui:composition>

Using <bs:field label="Title" .../> and <bs:field label="Author" .../> in the same form failed with HTTP 500 on both implementations. Mojarra: "Component ID f:input has already been found in the view." MyFaces: "Component with duplicate id "f:input" found. The first component is /WEB-INF/tags/field.xhtml at line 6 and column 74". The cure, already used by the field of Expressions Passed to a Tag File, is to make the id an attribute (id="#{name}", with for="#{name}" on the label and the message), so each use supplies its own. A composite component does not have the problem, because it is a naming container (Choosing: Tag File, Composite, Component, Handler or Function).

Method Expressions Do Not Pass Through

A tag file can forward a value expression, as the field does with value. It cannot forward a method expression. This button wrapper is registered with a method-signature declaration, in the hope that it would help:

<tag>
    <tag-name>saveButton</tag-name>
    <source>tags/saveButton.xhtml</source>
    <attribute>
        <name>action</name>
        <method-signature>java.lang.String action()</method-signature>
    </attribute>
</tag>

and implemented by this tag file

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:h="jakarta.faces.html"
                xmlns:ui="jakarta.faces.facelets">
    <h:commandButton id="#{id}" value="#{label}" action="#{action}"/>
</ui:composition>

used as <bs:saveButton id="save" label="Save" action="#{formBean.save}"/>, failed on submit with HTTP 500 on both implementations. Mojarra: action="#{action}": ... action="#{formBean.save}": Property [save] not found on type [org.example.bookshelf.web.FormBean]. MyFaces: the same message wrapped in ContextAwarePropertyNotFoundException. The variable action holds the caller’s expression and the EL evaluates it as a property read of formBean.save, not as a method call. Writing the call with parentheses, action="#{formBean.save()}", did not help either: both runtimes reported "Identity [action] was null and was unable to invoke". The method-signature declaration in the descriptor made no difference.

This is the first reason to leave tag files: anything that must pass an action, an action listener or a validator method down to a component needs a composite component, whose cc:attribute method-signature was designed for it. The equivalent of the button above worked on both implementations:

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:h="jakarta.faces.html"
                xmlns:ui="jakarta.faces.facelets"
                xmlns:cc="jakarta.faces.composite">
    <cc:interface>
        <cc:attribute name="label" required="true"/>
        <cc:attribute name="action" method-signature="java.lang.String action()" required="true"/>
    </cc:interface>
    <cc:implementation>
        <h:commandButton id="button" value="#{cc.attrs.label}" action="#{cc.attrs.action}"/>
    </cc:implementation>
</ui:composition>

Posting the form with the button f:save:button printed Saved: Dune / null / null on Mojarra and MyFaces. The composite component page covers cc:interface in full.

Tags for Converters, Validators, Components and Composites

A tag entry does not have to point at a file or a Java handler. It can also publish something that Faces already knows under a name and namespace of the application’s choice. The children of the entry decide what:

Child of tag What it publishes

source

A tag file (Tag Files).

handler-class

A tag handler written in Java (Custom Tag Handlers).

converter

A Converter by its converter-id. An optional handler-class replaces the tag handler.

validator

A Validator by its validator-id, with an optional handler-class.

behavior

A client behavior by its behavior-id, with an optional handler-class. The client behaviors page uses it.

component

A UIComponent by component-type, optionally with a renderer-type and a handler-class, or a composite component by resource-id (a resource identifier such as bookshelf/field.xhtml).

The two entries below turn the Bookshelf’s validator and converter into tags. The classes are ordinary @FacesValidator and @FacesConverter classes with a property each (see validators and converters for how to write them):

<tag>
    <tag-name>minWords</tag-name>
    <validator>
        <validator-id>bookshelf.minWords</validator-id>
    </validator>
</tag>
<tag>
    <tag-name>upper</tag-name>
    <converter>
        <converter-id>bookshelf.upper</converter-id>
    </converter>
</tag>
package org.example.bookshelf.tags;

import jakarta.faces.application.FacesMessage;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
import jakarta.faces.validator.FacesValidator;
import jakarta.faces.validator.Validator;
import jakarta.faces.validator.ValidatorException;

@FacesValidator("bookshelf.minWords")
public class MinWordsValidator implements Validator<String> {

    private int min = 2;

    public void setMin(int min) { this.min = min; }
    public int getMin() { return min; }

    @Override
    public void validate(FacesContext context, UIComponent component, String value) {
        if (value == null || value.isBlank()) {
            return;
        }
        int words = value.trim().split("\\s+").length;
        if (words < min) {
            throw new ValidatorException(new FacesMessage(FacesMessage.SEVERITY_ERROR,
                    "Please write at least " + min + " words.", null));
        }
    }
}
package org.example.bookshelf.tags;

import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
import jakarta.faces.convert.Converter;
import jakarta.faces.convert.FacesConverter;
import java.util.Locale;

@FacesConverter("bookshelf.upper")
public class UpperConverter implements Converter<String> {

    private boolean trim;

    public void setTrim(boolean trim) { this.trim = trim; }

    @Override
    public String getAsObject(FacesContext context, UIComponent component, String value) {
        if (value == null) {
            return null;
        }
        String v = trim ? value.trim() : value;
        return v.toUpperCase(Locale.ROOT);
    }

    @Override
    public String getAsString(FacesContext context, UIComponent component, String value) {
        return value == null ? "" : value;
    }
}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:bs="urn:org.example.bookshelf:tags">
<h:head><title>VC</title></h:head>
<h:body>
    <h:form id="f">
        <h:inputText id="notes" value="#{formBean.notes}">
            <bs:minWords min="3"/>
        </h:inputText>
        <h:inputText id="title" value="#{formBean.title}">
            <bs:upper trim="true"/>
        </h:inputText>
        <h:commandButton id="save" value="Save" action="#{formBean.save}"/>
        <h:messages/>
    </h:form>
    <p id="saved">#{formBean.saved}</p>
</h:body>
</html>

Posting f:notes=two words and f:title= dune +` answered on both runtimes with the message "Please write at least 3 words.", and the title converted to `+DUNE. Two things to know about such tags:

  • Attributes are wired to bean properties by name. min="3" called setMin(3) and trim="true" called setTrim(true), without any attribute declaration. An attribute that matches no property is silently ignored: with minimum="3" the validator kept its default and answered "Please write at least 2 words.".

  • The f:validator and f:converter tags work without a library, but they have no attributes that set properties of the instance (binding points at a bean instead). A tag is the way to give a validator or converter named, documented parameters.

A tag that publishes a component needs a component class, registered here with @FacesComponent. The smallest one writes its own markup and has the entry shown here; the custom components page extends the idea with renderers and a StateHelper:

package org.example.bookshelf.tags;

import jakarta.faces.component.FacesComponent;
import jakarta.faces.component.UIOutput;
import jakarta.faces.context.FacesContext;
import jakarta.faces.context.ResponseWriter;
import java.io.IOException;

/** A minimal component with its own tag declared in bookshelf.taglib.xml. */
@FacesComponent("bookshelf.Greeting")
public class GreetingComponent extends UIOutput {

    @Override
    public void encodeEnd(FacesContext context) throws IOException {
        ResponseWriter writer = context.getResponseWriter();
        writer.startElement("span", this);
        writer.writeAttribute("class", "greeting", null);
        writer.writeText("Hello, " + getValue(), null);
        writer.endElement("span");
    }
}
<tag>
    <tag-name>greeting</tag-name>
    <component>
        <component-type>bookshelf.Greeting</component-type>
    </component>
</tag>
<tag>
    <tag-name>compositeField</tag-name>
    <component>
        <resource-id>bookshelf/field.xhtml</resource-id>
    </component>
</tag>

Using <bs:greeting value="Ada"/> printed <span class="greeting">Hello, Ada</span>, and <bs:compositeField .../> rendered the composite component resources/bookshelf/field.xhtml under the library’s own namespace and tag name on both implementations. The resource-id form was introduced in Faces 2.2 (What Changed per Version).

Without any entry at all, a composite component is reachable through the namespace jakarta.faces.composite/<library>. The descriptor element composite-library-name gives the same library a namespace of its own, which is the form to choose when the namespace should not reveal the resource folder:

<facelet-taglib xmlns="https://jakarta.ee/xml/ns/jakartaee"
                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-facelettaglibrary_4_1.xsd"
                version="4.1">
    <namespace>urn:org.example.bookshelf:composites</namespace>
    <composite-library-name>bookshelf</composite-library-name>
</facelet-taglib>

With this file registered, xmlns:bsc="urn:org.example.bookshelf:composites" and <bsc:field .../> rendered the composite field.xhtml of resources/bookshelf on both implementations.

Custom Tag Handlers

The specification calls the role of the tag handler class in Facelets "greatly reduced and in most cases unnecessary". A handler is the right tool only when a tag has to act on the view as it is being built: set a variable for its body, decide whether the body exists at all, repeat it, or attach something to the parent. Everything that renders output or takes part in the lifecycle belongs in a component (a custom component), and everything that merely reuses markup belongs in a tag file.

Where a Handler Runs

Facelets compiles each XHTML file into a tree of FaceletHandler objects, one per element. Building the view means calling apply(FaceletContext, UIComponent) on the tree, depth first (the specification describes this in 10.2.1, and build time and render time describes the same two moments for c:forEach). A TagHandler has two moments:

  • its constructor runs when the file is compiled ("only called when the Facelets View is compiled", says the API), and receives a TagConfig;

  • its apply method runs every time the view is built, which is on the first request and again at Restore View of every postback (see the phases).

The Bookshelf handlers below increment two counters of a small bean, so a page can print how often each moment occurs:

package org.example.bookshelf.tags;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Named;
import java.util.concurrent.atomic.AtomicInteger;

@Named("tagStats")
@ApplicationScoped
public class TagStats {

    public static final AtomicInteger CONSTRUCTED = new AtomicInteger();
    public static final AtomicInteger APPLIED = new AtomicInteger();

    public int getConstructed() { return CONSTRUCTED.get(); }
    public int getApplied() { return APPLIED.get(); }
}

A page with one bs:times tag printed #{tagStats.constructed} and #{tagStats.applied}; the table shows them after each request:

Request Handler constructed apply calls

FACELETS_REFRESH_PERIOD=-1: first GET

1

1

same: second GET

1

2

same: postback of the form (the POST itself)

1

one more than before

default in the development stage: first, second, third GET

1, 2, 3

1, 2, 3

Mojarra and MyFaces gave the same numbers. In the development stage the refresh period is zero, so the file is compiled on every request and the handler is constructed every time. With -1 (the default in production) the constructor ran once and the object served every later request. The consequences are the rules of a servlet: a handler instance is shared by all requests and all users, so it must hold no per-request or per-user state in fields, and anything it needs from the request must be read in apply from the FaceletContext.

The API

The API lives in jakarta.faces.view.facelets. A handler extends TagHandler, whose constructor takes the TagConfig:

Type Use

TagHandler

The base class. The protected fields tag, tagId and nextHandler are set by the constructor; nextHandler is the handler of the tag’s body. getAttribute(name) returns a TagAttribute or null, getRequiredAttribute(name) throws a TagException if it is absent.

TagConfig

getTag(), getNextHandler() (never null) and getTagId(), a document-unique id.

TagAttribute

One attribute of the tag. getValue() is the literal text; with a FaceletContext: getValue(ctx), getInt(ctx), getBoolean(ctx), getObject(ctx), getObject(ctx, type), getValueExpression(ctx, type) and getMethodExpression(ctx, type, paramTypes). isLiteral() tells whether it is plain text rather than an expression. getLocalName(), getNamespace(), getQName() and getLocation() identify it.

FaceletContext

Extends the EL context: getFacesContext(), getExpressionFactory(), getVariableMapper() (inherited) and setVariableMapper, setFunctionMapper, setAttribute(name, value) and getAttribute(name) (backed by the variable mapper), generateUniqueId(base), includeFacelet(parent, path).

TagAttributeException, TagException, FaceletException

Report a problem with the attribute, the tag, or the Facelet. The runtime adds the file, line and column to the message.

Two Handlers

bs:today exposes today’s date to its body as a variable, the way c:set would, but scoped to the tag. The pattern is the one every handler that publishes a variable needs: save the previous mapping, set the new one, apply the body, restore the old one in a finally block.

package org.example.bookshelf.tags;

import jakarta.el.ValueExpression;
import jakarta.el.VariableMapper;
import jakarta.faces.component.UIComponent;
import jakarta.faces.view.facelets.FaceletContext;
import jakarta.faces.view.facelets.TagAttribute;
import jakarta.faces.view.facelets.TagConfig;
import jakarta.faces.view.facelets.TagHandler;
import java.io.IOException;
import java.time.LocalDate;

/** {@code <bs:today var="today">...</bs:today>} exposes the current date to the tag body. */
public class TodayHandler extends TagHandler {

    private final TagAttribute var;

    public TodayHandler(TagConfig config) {
        super(config);
        TagStats.CONSTRUCTED.incrementAndGet();
        this.var = getRequiredAttribute("var");
    }

    @Override
    public void apply(FaceletContext ctx, UIComponent parent) throws IOException {
        TagStats.APPLIED.incrementAndGet();
        String name = var.getValue(ctx);
        VariableMapper mapper = ctx.getVariableMapper();
        ValueExpression previous = mapper.resolveVariable(name);
        ValueExpression value = ctx.getExpressionFactory().createValueExpression(LocalDate.now(), LocalDate.class);
        mapper.setVariable(name, value);
        try {
            nextHandler.apply(ctx, parent);
        } finally {
            mapper.setVariable(name, previous);
        }
    }
}

bs:times applies its body a number of times, in the manner of c:forEach, and rejects a negative count with a TagAttributeException, which becomes an error that names the page, the line and the attribute:

package org.example.bookshelf.tags;

import jakarta.faces.component.UIComponent;
import jakarta.faces.view.facelets.FaceletContext;
import jakarta.faces.view.facelets.TagAttribute;
import jakarta.faces.view.facelets.TagAttributeException;
import jakarta.faces.view.facelets.TagConfig;
import jakarta.faces.view.facelets.TagHandler;
import java.io.IOException;

/** {@code <bs:times count="3">...</bs:times>} applies its body count times, when the view is built. */
public class TimesHandler extends TagHandler {

    private final TagAttribute count;
    private final TagAttribute max;

    public TimesHandler(TagConfig config) {
        super(config);
        TagStats.CONSTRUCTED.incrementAndGet();
        this.count = getRequiredAttribute("count");
        this.max = getAttribute("max");
    }

    @Override
    public void apply(FaceletContext ctx, UIComponent parent) throws IOException {
        TagStats.APPLIED.incrementAndGet();
        int limit = max == null ? 10 : max.getInt(ctx);
        int requested = count.getInt(ctx);
        if (requested < 0) {
            throw new TagAttributeException(count, "count must not be negative");
        }
        int n = Math.min(requested, limit);
        for (int i = 0; i < n; i++) {
            nextHandler.apply(ctx, parent);
        }
    }
}

The TagStats lines only feed the counters of Where a Handler Runs. Both are declared with handler-class; the optional attribute children document the attributes (type and required):

<tag>
    <tag-name>today</tag-name>
    <handler-class>org.example.bookshelf.tags.TodayHandler</handler-class>
    <attribute>
        <name>var</name>
        <required>true</required>
        <type>java.lang.String</type>
    </attribute>
</tag>
<tag>
    <tag-name>times</tag-name>
    <handler-class>org.example.bookshelf.tags.TimesHandler</handler-class>
</tag>

and used from a page:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:bs="urn:org.example.bookshelf:tags">
<h:head><title>Handlers</title></h:head>
<h:body>
    <bs:today var="today"><p>Today is #{today}.</p></bs:today>
    <p>Outside the tag: [#{today}]</p>
    <bs:times count="3"><span>x</span></bs:times>
    <bs:times count="#{catalogueBean.books.size()}" max="2"><span>y</span></bs:times>
</h:body>
</html>

The page printed Today is 2026-10-08., an empty Outside the tag: [] (the variable was restored), three x and two y (the second count, 3, was capped by max="2") on both implementations. <bs:today/> without var failed with Attribute 'var' is required and <bs:times count="-1"> with count="-1" count must not be negative, each prefixed with the page, line and column (Mojarra /neg.xhtml @5,26, MyFaces /neg.xhtml at line 5 and column 26).

A Handler Decides When the View Is Built

A handler’s decision is taken when the view is built, before the action of a postback has run. bs:times and ui:repeat were given the same count, which lives in a session scoped bean that an action increments:

package org.example.bookshelf.web;

import jakarta.enterprise.context.SessionScoped;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.List;
import java.util.stream.IntStream;

@Named
@SessionScoped
public class CountBean implements Serializable {

    private int n = 2;

    public void more() {
        n++;
    }

    public int getN() {
        return n;
    }

    public List<Integer> getRange() {
        return IntStream.range(0, n).boxed().toList();
    }
}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:bs="urn:org.example.bookshelf:tags">
<h:head><title>Count</title></h:head>
<h:body>
    <h:form id="f">
        <p id="times">times: <bs:times count="#{countBean.n}"><span>t</span></bs:times></p>
        <p id="repeat">repeat: <ui:repeat value="#{countBean.range}" var="i"><span>r</span></ui:repeat></p>
        <p id="stats">applied #{tagStats.applied}</p>
        <h:commandButton id="more" value="More" action="#{countBean.more}"/>
    </h:form>
</h:body>
</html>

Both showed two items on the first request. After the postback of the button:

  • ui:repeat showed three on both implementations, because it reads its collection while rendering, after the action.

  • bs:times showed two on MyFaces, the stale value taken at Restore View, and three on the next request. Mojarra showed three straight away, because it applied the handler a second time in that response (the counter rose by two for that request, and by one when the count was the literal count="2").

Nothing in the specification says how many times apply runs for a request, so a handler must be written to be called repeatedly and must not depend on either behaviour. This is the same hazard as c:forEach and c:if (see build time and render time), and the same rule applies: use a component such as ui:repeat or rendered for anything that can change after the view is built, and a handler only for decisions that cannot.

Two further extension points exist and were not run for this page. MetaTagHandler and ComponentHandler (the base class a handler for a component, converter, validator or behavior tag must extend, according to its API) wire tag attributes to a Java object through MetaRuleset rules. TagDecorator rewrites a tag before its handler is chosen, and its implementations are listed in the context parameter jakarta.faces.FACELETS_DECORATORS.

EL Functions

A library can also publish static methods as EL functions, written #{prefix:name(arguments)} with the prefix the page bound to the library’s namespace. The function element names the function, the class and the Java signature:

<function>
    <function-name>formatIsbn</function-name>
    <function-class>org.example.bookshelf.tags.IsbnFunctions</function-class>
    <function-signature>java.lang.String formatIsbn(java.lang.String)</function-signature>
</function>
<function>
    <function-name>initials</function-name>
    <function-class>org.example.bookshelf.tags.IsbnFunctions</function-class>
    <function-signature>java.lang.String initials(java.lang.String)</function-signature>
</function>
package org.example.bookshelf.tags;

/** Static methods published as EL functions by bookshelf.taglib.xml. */
public final class IsbnFunctions {

    private IsbnFunctions() {
    }

    public static String formatIsbn(String isbn) {
        if (isbn == null || isbn.length() != 13) {
            return isbn;
        }
        return isbn.substring(0, 3) + "-" + isbn.substring(3, 4) + "-" + isbn.substring(4, 9) + "-"
                + isbn.substring(9, 12) + "-" + isbn.substring(12);
    }

    public static String initials(String name) {
        StringBuilder sb = new StringBuilder();
        for (String part : name.trim().split("\\s+")) {
            sb.append(Character.toUpperCase(part.charAt(0)));
        }
        return sb.toString();
    }
}

The signature has the form ReturnType name(ParameterType, ...), with fully qualified class names (the schema borrows the syntax of the JSP tag library descriptor). The cards page of Tag Files already used #{bs:formatIsbn(b.isbn)} and #{bs:initials(b.author)}. The edge cases:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:bs="urn:org.example.bookshelf:tags">
<h:head><title>Edge</title></h:head>
<h:body>
    <p>[#{bs:formatIsbn(null)}] [#{bs:formatIsbn('123')}] [#{bs:initials('ada king lovelace')}]</p>
</h:body>
</html>

printed [] [123] [AKL] on both runtimes: a null argument reaches the method as null, so the method must cope with it (here it returns null, which prints as nothing), and an input of the wrong length is returned unchanged.

How a wrong declaration fails differs between the runtimes, and a mistake in one function can take the whole library with it:

Mistake Mojarra 4.1.17 MyFaces 4.1.4

function-class names a class that does not exist

The application fails to deploy (FacesException: org.example.nope.Missing), every URL answers 404

The application starts; every page I requested that used the library answered 500 with ClassNotFoundException

function-signature names a method that the class does not have

Fails to deploy: "No Function Found on type …​ with signature …​"

Every such page answers 500 with the same message

the method exists but is not static

Deploys; the call fails: Cannot invoke "Object.getClass()" because "obj" is null

Same

a page calls bs:nosuch(...)

HTTP 500, Function [bs:nosuch] not found

Same

Deploy the library to a test environment before you ship it. An EL function is a good fit for a pure calculation with no state (formatting an ISBN, building an initial); a method on a bean can already be called from EL (method expressions) and needs no descriptor, so a function pays off only when the same helper is wanted in many views without a bean in scope. The standard function library jakarta.tags.functions (conventionally fn:) is listed in the specification among the standard Facelet tag libraries, and needs no registration.

Packaging a Library in a JAR

A library that several applications share is built as a JAR, and thanks to auto-discovery its users add a dependency and nothing else. The Bookshelf extension below has this layout, which is the standard Maven src/main/resources layout; the descriptor sits in META-INF and the tag file next to it, because source is relative to the descriptor (Where the source Path Is Resolved):

bookshelf-tags-ext.jar
  META-INF/bookshelf-ext.taglib.xml
  META-INF/tags/badge.xhtml
  org/example/bookshelf/...            (handlers, functions, components, validators)
<facelet-taglib xmlns="https://jakarta.ee/xml/ns/jakartaee"
                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-facelettaglibrary_4_1.xsd"
                version="4.1">
    <namespace>urn:org.example.bookshelf:ext</namespace>
    <tag>
        <tag-name>badge</tag-name>
        <source>tags/badge.xhtml</source>
    </tag>
</facelet-taglib>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:ui="jakarta.faces.facelets">
    <span class="badge">#{text}</span>
</ui:composition>

A page uses it with no entry in web.xml. The page below also uses a descriptor placed in WEB-INF/classes/META-INF and one in WEB-INF that is not registered, to show the difference:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="jakarta.faces.html"
      xmlns:ext="urn:org.example.bookshelf:ext" xmlns:cls="urn:org.example.bookshelf:cls" xmlns:web="urn:org.example.bookshelf:web">
<h:head><title>Auto</title></h:head>
<h:body>
    <ext:badge text="from the jar"/>
    <cls:tick text="from WEB-INF/classes/META-INF"/>
    <web:flag text="WEB-INF/auto.taglib.xml"/>
</h:body>
</html>

On Mojarra and MyFaces the first two tags rendered (<span class="badge">from the jar</span> and the one from WEB-INF/classes/META-INF), and the third was copied to the output as <web:flag text="WEB-INF/auto.taglib.xml"></web:flag> with the warning quoted in Registering a Library. Components, converters and validators that the JAR declares by annotation are subject to the packaging rules of the custom components page, which this page did not exercise.

Choosing: Tag File, Composite, Component, Handler or Function

All of them let a page say <bs:something/>. The runs showed concrete differences between them:

Option Needs Good for Cannot or does not

ui:include / ui:decorate (no tag)

XHTML. A path, ui:param children

A fragment used a few times; ui:decorate also takes a body

ui:include drops its body. No namespace, no name

Tag file

XHTML and a descriptor entry

A named, namespaced fragment with a body and named slots; reading and writing the caller’s value expressions

Attributes are undeclared variables (a missing one falls back to EL names). Not a naming container. Cannot forward method expressions

Composite component (cc:interface)

XHTML in a resource library

A declared interface (cc:interface): attributes with required and method-signature, a naming container (ids cannot collide) and cc.attrs; the composite components page covers the rest

Has no behaviour beyond what its XHTML and a backing component can express. Generated ids appear in client ids (f:j_idt5:input on Mojarra, f:j_id_7:input on MyFaces) unless the caller sets an id

Tag handler

Java class, descriptor entry

Decisions taken while the view is built: variables, repetition, conditional inclusion, adding components

Cannot render or take part in the lifecycle. Runs again on every build, in an implementation dependent number

EL function

Static Java method, descriptor entry

Pure calculations inside expressions

No access to the view or the request unless it reads them itself. A wrong declaration can stop the application

Custom component and renderer

Java classes (@FacesComponent, @FacesRenderer), a tag entry or createTag

State, decode of request parameters, events, markup that XHTML cannot express, several render kits

The most code, and state must be saved correctly

The same decision as a flow:

flowchart TD start["A reusable piece of view is needed"] --> state{"Does it hold state, decode request parameters or write its own markup?"} state -- yes --> comp["Custom component and renderer"] state -- no --> build{"Must it decide what the view contains while it is built?"} build -- yes --> handler["Tag handler"] build -- no --> calc{"Is it only a calculation inside an expression?"} calc -- yes --> fn["EL function"] calc -- no --> iface{"Needs required attributes, method expressions, validators or a naming container?"} iface -- yes --> cc["Composite component"] iface -- no --> tf["Tag file"]

The Jakarta EE Tutorial gives the same advice in other words: a component that merely aggregates existing components should be a composite component, data manipulation belongs in a managed bean, and conversion, validation and listeners are registered with the standard tags, so a custom component is needed only for behaviour or markup that none of them offers. In the Bookshelf the labelled field of this page is a tag file because it only forwards a value, while the star rating of the composite components page is a composite component.

What Changed per Version

Version Change

JSF 2.0 (Facelets 2.0)

Facelets joined the specification. Section 10.2 of the specification explains that the original project did not separate API and implementation, and that only the classes that had to be public were kept, in javax.faces.view.facelets. The tag library schema web-facelettaglibrary_2_0.xsd already contained composite-library-name; Rev a corrected its type (errata C027).

JSF 2.2

resource-id lets a tag point to a composite component (issue 1001); the method-signature element was added to the schema (issue 746); the schema moved to the namespace http://xmlns.jcp.org/xml/ns/javaee (issue 1169); the name of the decorators parameter was corrected to javax.faces.FACELETS_DECORATORS (issue 1014).

JSF 2.3

The cardinality rules of tag and component in the schema were revisited (issue 1362); FACELETS_REFRESH_PERIOD defaults to -1 in production (issue 936); f:importConstants was added (issue 1424).

Faces 3.0

javax.faces became jakarta.faces: the context parameters, the annotations and the API package jakarta.faces.view.facelets.

Faces 4.0

JSP support was removed entirely (issue 1546), so the JSP tag handlers and TLDs of earlier books have no replacement in the view layer: a tag library is a Facelets taglib.

Faces 4.1

The schema is web-facelettaglibrary_4_1.xsd in the namespace https://jakarta.ee/xml/ns/jakartaee, and its version attribute accepts only 4.1. The Javadoc no longer mentions backwards compatibility with the old facelets.* context parameters (issue 1828). In the tests, Mojarra 4.1.17 and MyFaces 4.1.4 nevertheless loaded descriptors that declared version="4.0", version="2.3" in the javaee namespace, and the Facelets 1.x DTD form.

Faces 5.0

Not described: the 5.0 milestone is not current behaviour.

Where the Books Are Outdated

  • Facelets 1.x libraries. Material written for the stand-alone Facelets 1.x project, which the book on Facelets covers, declares tag libraries with a facelet-taglib element validated by the DTD facelet-taglib_1_0.dtd, registers them with a project specific context parameter, and writes handlers that extend classes in com.sun.facelets. In Faces 4.1 the descriptor is validated by the web-facelettaglibrary_4_1.xsd schema, the registration parameter is jakarta.faces.FACELETS_LIBRARIES (the old names facelets.LIBRARIES and javax.faces.FACELETS_LIBRARIES were not read by either runtime), and the handler API is in jakarta.faces.view.facelets. A descriptor with the old DOCTYPE was still accepted by both runtimes in the test, but the specification no longer describes that form. The com.sun.facelets package names and the project’s custom ViewHandler come from the issue’s description of the book and were not checked against the book here.

  • JSP tag handlers and TLDs. The older Faces books (Jacobi and Fallows, Mann and Dudney and others) write a custom tag as a class that extends UIComponentELTag plus a .tld file. JSP support was removed in Faces 4.0; the replacement is a tag entry with a component child in a *.taglib.xml, or @FacesComponent(createTag = true) (custom components page).

  • "Composition components". Early Facelets material builds reusable components as ui:composition files included with ui:include. That still works, and a tag file is the same file given a tag name, but the composite component of Faces 2.0 replaced it where an interface, method expressions or unique ids are needed (Choosing: Tag File, Composite, Component, Handler or Function).

  • The Tutorial’s validator tag. The Jakarta EE Tutorial shows a validator entry with a validator-class child. The 4.1 schema defines validator-id and an optional handler-class only. Mojarra and MyFaces both loaded such a descriptor and ignored the extra element, so the snippet works by accident; write the entry as in Tags for Converters, Validators, Components and Composites.

Common Mistakes

  • Registering the library under the wrong parameter name. Only jakarta.faces.FACELETS_LIBRARIES (or a META-INF/*.taglib.xml in a JAR or in WEB-INF/classes) works. A missing library is only a warning, and the first tag or function that uses it fails.

  • A tag file path that starts with a slash. source is relative to the descriptor.

  • The same fixed id in a tag file used twice in one form. Make the id an attribute, or use a composite component.

  • Relying on required in the descriptor. Mojarra enforces it for tag files and MyFaces does not.

  • An optional attribute named like a bean. The expression resolves to the bean when the page omits the attribute.

  • Passing an action or listener to a tag file. It evaluates as a property read and fails. Use a composite component with method-signature.

  • State in a handler field. One instance serves every request and user.

  • Assuming how often apply runs. It runs per build, and Mojarra and MyFaces differ on a postback.

  • Decisions in a handler about data an action changes. The view is built before the action; use ui:repeat or rendered.

  • A faulty EL function declaration. It can stop the whole application at start-up (Mojarra) or fail every page of the library (MyFaces).

  • Forgetting the null case in an EL function. Expressions pass null through.

References