Templating with Thymeleaf: The Basics (Part 1)

 ยท 12 min

Thymeleaf is a modern Java template engine for both web and standalone applications. Although widely used in the Spring ecosystem, it can be integrated into any JVM environment. If you need a dynamic, flexible, and extensible templating engine, Thymeleaf might be just what you need.

In this first part, we’ll explore Thymeleaf’s key features, set up a basic project, and dive into its fundamental syntax and capabilities.

This is the first article of “Templating with Thymeleaf”, a 3-part miniseries. We’ll cover the basics of setting up Thymeleaf, its fundamental syntax and features in this one. The next article will dive deeper into how to layout with fragments and reusability. And finally, we’ll take a look at custom dialects and more advanced features.


What is Thymeleaf?

At its core, Thymeleaf is a Java-based template engine with a strong focus on HTML/XML. However, it has dedicated modes for JavaScript, CSS, and plain text, making it usable for various content generation tasks.

What sets Thymeleaf apart from other engines is its natural templating philosophy. Unlike other template engines that use custom syntax, Thymeleaf uses normal HTML templates that can be viewed as static files without requiring any processing. This feature is particularly helpful when collaborating with front-end developers or designers who can work with pure HTML/XML prototypes.

This article guides you through using Thymeleaf in a framework-agnostic way. Besides the initial setup, the majority of the content and examples will apply no matter how you choose to use it.

Key Features

  • Natural templating
    Templates remain valid HTML/XML, as standard elements and attributes are used instead of a specialized template syntax.

  • Rich feature set
    Internationalization, forms, fragments, layout management, and more.

  • Extensibility
    Create custom dialects, which we’ll explore in the final article.

  • Platform-agnostic, but deep Spring integration
    Usable without any framework yet works seamlessly in Spring’s MVC architecture, including data binding.

Adding Thymeleaf Dependency

For Spring, the Thymeleaf Starter dependency will auto-configure most things.

In non-Spring environments, we must manually configure Thymeleaf, including the template engine and template resolution.

First, we add the dependency to our project.

For Maven, add the following to our pom.xml:

pom.xml
<dependency>
  <groupId>org.thymeleaf</groupId>
  <artifactId>thymeleaf</artifactId>
  <version>3.1.2.RELEASE</version>
</dependency>

For Gradle, add this to your build.gradle:

build.gradle
implementation 'org.thymeleaf:thymeleaf:3.1.2.RELEASE'

This adds the template engine core without any extras or Spring-specific support.

Make sure to check for the latest version on the Thymeleaf website or Maven Central.


Template Resolution and Setup

Before we can start using Thymeleaf, we need to set up the template engine and configure how it resolves templates. This involves two main steps:

  • Configuring one or more template resolvers
  • Setting up the template engine

Template Resolving

Resolving templates refers to how Thymeleaf locates and loads templates to process.

Template resolvers deal with the following:

  • Location: Where a template is stored
  • File format: .html, .xml, etc.
  • Processing mode: HTML, RAW, etc.
  • Caching: Should it be cached for performance

The template resolver is responsible for locating and reading template files. We don’t have to implement one from scratch, as Thymeleaf provides several implementations.

Here’s a selection:

  • ClassLoaderTemplateResolver: Looks for templates stored in the classpath
  • FileTemplateResolver: A filesystem-based resolver
  • UrlTemplateResolver: Loads templates from a URL, like an external resource
  • StringTemplateResolver: The template being resolved is the template source

For our purposes, we’ll use a ClassLoaderTemplateResolver:

Setting up a ClassLoaderTemplateResolver
ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
templateResolver.setPrefix("templates/");
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode(TemplateMode.HTML);
templateResolver.setCharacterEncoding("UTF-8");
templateResolver.setCacheable(false);
templateResolver.setOrder(1);

The exact options available depend on the type of resolver. The ITemplateResolver interface, which all resolvers are based on, has no configuration options; most of the mentioned methods are located in AbstractTemplateResolver.

The setPrefix(String) and setSuffix(String) tell the template resolver where to find the templates and what file extension to look for. The resolver uses them to build the full path to locate the correct template.

  • Prefix
    Defines the directory or location where templates are stored based on the resolver’s default location. For example, if your templates are stored in src/main/resources/templates, you would set the prefix as templates/ for a ClassLoaderTemplateResolver.

  • Suffix
    Specifies the file extension of the templates.

When you process a template, Thymeleaf will combine its default location, the prefix, the template name, and the suffix to look for the file:

Processing a template
// Looks for src/main/resources/templates/home.html
templateEngine.process("home", context);

Next is setTemplateMode(TemplateMode), which sets the template mode, which tells the engine how to interpret a template’s content.

The available options are:

  • HTML,
  • XML
  • TEXT
  • JAVASCRIPT
  • CSS
  • RAW

Some template resolvers detect the template mode based on the file extensions and might ignore the explicitly set one. That’s why the ClassLoaderTemplaterResolver has setForceTemplateMode(boolean) to prevent that behavior if needed.

setCharacterEncoding(String) sets the encoding of the templates; there’s nothing special here.

The setCacheable option can significantly improve performance. However, during development, you’d want to turn it off to see any templates changes reflected immediately.

And finally, the setOrder(Integer) call sets the order of resolvers if more than one is configured.

With the resolver configured, we now create the template engine.

Setting Up the TemplateEngine

With our resolver configured, it’s time to create the TemplateEngine:

Create a TemplateEngine
TemplateEngine templateEngine = new TemplateEngine();
templateEngine.setTemplateResolver(templateResolver);

That was easy… if we have more than one resolver, we have multiple options at hand to add them to the engine:

Adding resolvers to the template engine
// MULTIPLE RESOLVERS
templateEngine.setTemplateResolvers(Set.of(templateResolver1, templateResolver2));

// ADDING RESOLVERS
templateEngine.addTemplateResolver(templateResolver1);
templateEngine.addTemplateResolver(templateResolver2);

Be aware that the set... methods remove all previously added/set resolvers on the engine.

More configuration options are available, but that’s out of the scope of this article.

Rendering a Thymeleaf Template

Now that we have the engine set up, we can start processing templates

Here’s a simple example stored as src/main/resources/templates/hello.html:

src/main/resources/templates/hello.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
      <title>Hello, Thymeleaf</title>
  </head>
  <body>
      <h1>Welcome, <span th:text="${name}">User</span>!</h1>
  </body>
</html>

Thanks to Thymeleaf’s natural templating approach, any browser renders the template as normal HTML, displaying “Welcome, User!”. The actual processing magic is done via the non-interfering th-prefixed attributed.

In this case, the th:text attribute replaces the content of the <span> element with the variable name from the Context object provided to the TemplateEngine for processing.

In Java code, it looks like this:

Simple processing
Context context = new Context();
context.setVariable("name", "Ben");

String renderedHtml = templateEngine.process("hello", context);

Simple enough, all we need is a configured template engine, a template, and a context to fill in the blanks.


Working with Context

The Context in Thymeleaf is a crucial concept. It serves as a container for all the data we want to make available in our template.

Creating and Populating a Context

We’ve already seen a basic example of creating a Context and adding variables to it.

It stores key-value pairs, where each key is a String (the variable name) and the associated value can be anything.

Creating a Context object is straightforward. You instantiate a new instance and then populate it with variables using the setVariable method.

Here’s how it works in practice:

Creating a Context
Context context = new Context();
context.setVariable("name", "Ben");
context.setVariable("age", 41);
context.setVariable("isLoggedIn", true);

There are multiple types of contexts available, like WebContext, which gives access to session and request parameters.

Let’s dive into the syntax to see how to use those variables!


Basic Syntax and Template Features

A wide range of features are available for manipulating text and attributes, resolving URLs, and implementing conditional logic. To avoid interfering with HTML/XML, all attributes and elements live in the th namespace, which has to be declared in each template:

Template preamble
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
...

Attributes are parsed by Thymeleaf and evaluated as expressions, which is why variable expansion is used for accessing data from Context object with the help of the ${ } syntax.

Thymeleaf’s standalone expression language is explicitly built for its templating needs. It includes features like variable expressions, message expressions, link expressions, and fragment expressions, etc. While it shares some syntactic similarities with other expression languages, it is fully independent and built specifically for Thymeleaf.

Text Manipulation

Dynamically inserting or replacing content in a template is the bread and butter of any templating engine. Of course, Thymeleaf has multiple ways to interact with text.

Replacing Text

The th:text attribute replaces the content of its element:

Text replacement
<h1>Welcome, <span th:text="${name}">User</span>!</h1>

Unescaped Text

Thymeleaf escapes any rendered text make the result safe to use. However, if you need HTML in the generated output, you can use th:utext to render unescaped text:

Unescaped text
<p th:utext="${htmlContent}">This is a paragraph.</p>

Attribute Processors

Thymeleaf provides various processors to manipulate attributes.

Setting Attributes

Setting specific attributes is done with th:attr:

Setting attributes
<a href="#" th:attr="href=@{~/profile/3}">Profile</a>

<!-- RESULT
<a href="/profile/23">Profile</a>
-->

The @{ } syntax resolves URLs dynamically, which we’ll talk about in the next section.

As working with the class attribute in HTML is such a common use-case, there’s a dedicated attribute:

Setting class attribute
<a href="#" th:class="${linkClass}">Profile</a>

<!-- EQUIVALENT -->
<a href="#" th:attr="class=${linkClass}">Profile</a>

Prepending/Appending Attributes

Where th:attr overrides any pre-existing value, its brethren th:attprepend and th:attrappend do what’s written on the package:

Setting attributes
<input type="text" value="foo" th:attrappend="value=' bar'" />

<!-- RESULT
<input type="text" value="foo bar" />
-->

As with th:attr, there’s a dedicated attribute for dealing with class:

Appending classes conditionals
<a class="btn" th:classappend="${isActive} ? 'active' : 'inactive'">...</a>

URL and Resource Resolution

Working with URLs can be a hassle, especially when dealing with relative URLs to other resources. That’s why using context-aware URLs is particularly useful when:

  • Your application is deployed with a context path
  • URLs are dynamically generated
  • You want to ensure consistency in URL generation

Thymeleaf’s @{ } syntax handles this by ensuring that URLs are correctly resolved and include the proper context path or dynamic parts as needed.

Basic URL Syntax

URLs are simply wrapped in @{ }:

Basic URL Syntax
<a th:href="@{/profile}">Profile</a>

If the application has a context path (e.g., /myawesomeapp), it will automatically be prepended to /profile.

This requires the context to be a WebContext

The big advantage of this approach is consistency, even if the context path changes. The resulting URL will always be correct without updating any templates.

Absolute URLs that include a scheme won’t be prepended:

Absolute URL
<a th:href="@{https://www.thymeleaf.org}">Thymeleaf</a>

Server-relative URLs

If we want to link to another resource on the server, ignoring a possible context path, we use server-relative URLs. If a URL starts with ~ (tilde), it won’t be prepended with the current context path:

Server-relative URLs
<a th:href="@{~/payment-app/profile}">Payment Profile</a>

Protocol-relative URLs

Equivalent to how HTML handles URLs, // (double-slash) can replace an absolute URL’s scheme to match the one on the current page:

Protocol-relative URLs
<a th:href="@{//cdn.example.com/img/cat.jpg}">Open Image</a>

Parameterized URLs

URLs don’t have to be static; they can use variables, either as literals or as expressions.

The data to be used for the URL is appended to it in normal parentheses. If the URL contains a { } placeholder, it will fill it with the variable; otherwise, it gets appended as a query parameter:

Parameterized URLs
<a th:href="@{/profile/{userId}/details(userId=3,action='show')}">Profile</a>

This will render to:

Result of parameterized URL
<a href="/myawesomeapp/profile/3/details?action=show">Profile</a>

Instead of using literals, variable interpolation to use values from the template’s Context is possible:

Parameterized URLs
<a th:href="@{/profile/{userId}/details(userId=${userId},action=${action})}">Profile</a>

Conditionals

Several options are available to express conditional logic in our templates. Thymeleaf supports if, unless, and for more complex decisions, switch.

Simple Conditions: th:if and th:unless

The th:if and th:unless attributes render their content their expressions. Unlike Java, there’s no explicit concept of else, so the two attributes often complement each other:

Conditional rendering
<p th:if="${isLoggedIn}">Welcome back, <span th:text="${username}">johndoe0815</span>!</p>

<p th:unless="${isLoggedIn}">Please log in to continue.</p>

There are also and/or operators available to improve the conditional’s expression:

Combining conditionals
<div th:if="${user.isAdmin()} and ${user.lastLogin != null}">
  <p>Welcome back, Admin!</p>
</div>

Switch Statements: th:switch and th:case

If more than just a simple true or false is required for making a rendering decision based on a value, there’s also a th:switch attribute, which is accompanied by th:case:

Complex Conditionals with switch
<div th:switch="${user.role}">
  <p th:case="'admin'">User is an administrator</p>
  <p th:case="'editor'">User is an editor</p>
  <p th:case="*">User is something else</p>
</div>

The * (asterisk) acts as the default cause.

Elvis Operator

For inline conditions, Thymeleaf also supports the Elvis operator ?: (question mark colon):

Elvis operator
<p th:text="${user.name} ?: 'Anonymous'">Unknown</p>

Iterating over Collections

One of the most powerful features of Thymeleaf is its ability to iterate over collections of data using the th:each attribute.

This allows you to dynamically generate repetitive content like lists or tables, based on your data.

Basic Usage

The element with the attribute is rendered for each loop, so we need to add additional attributes or inner elements to do the actual work:

Iterating with th:each
<ul>
  <li th:each="item : ${items}" th:text="${item.name}">Item</li>
</ul>

In this example, Thymeleaf loops over items from the template’s Context and stores the value in item. The th:text attribute then replaces the <li> element’s content with item.name:

xml
<ul>
  <li>John</li>
  <li>Jane</li>
  <li>Bob</li>
</ul>

Iteration Status Variable

There’s an optional status variable available that gives us additional information about the current iteration.

For example, if we need an index, we can access it via a special variable called iterState:

Using iterStat
<ul>
  <li th:each="item, iterStat : ${items}" th:text="${iterStat.index} + ' - ' + ${item.name}">
      Item
  </li>
</ul>

As expected, the index starts at 0 (zero).

There are other properties available as well:

  • count: Current iteration index, starting a 1 (one)
  • size: Total amount of elements
  • current: Current iteration variable
  • even/odd: Indicates if the current iteration is even or odd (useful for alternating renders, like striped tables)
  • first/last: Helps with rendering the first or last element differently

These properties cover many common use-cases encountered when trying to layout nice-looking HTML, as we often need to treat the edges of repeating elements differently.


Conclusion

In this first part of our Thymeleaf series, we’ve explored the basics of Thymeleaf, covering everything from template configuration, basic syntax, and features like text replacement, conditionals, iteration, and URL resolution.

Thymeleaf’s natural templating approach, combined with its versatile, easy-to-grasp features make it an excellent choice for smooth collaboration with non-developers.

Whether you’re building small components or complex systems, mastering these foundational features can help you create dynamic and maintainable templates.

In the next part of this series, we’ll dive deeper into Thymeleaf by exploring how to modularize templates with fragments and more!


A Functional Approach to Java Cover Image
Interested in using functional concepts and techniques in your Java code?
Check out my book!
Available in English, Polish, Korean, and soon, Chinese.

Resources