Spring BootThymeleaf Integration
Spring Boot

Thymeleaf Integration

Thymeleaf is the recommended server-side template engine for Spring Boot. It processes HTML templates on the server, binding model data to the markup using th:* attributes. Templates are valid HTML files that open correctly in a browser without a running server — a property Thymeleaf calls Natural Templating. Spring Boot auto-configures everything with a single starter dependency.

Setup and Auto-Configuration

Adding spring-boot-starter-thymeleaf to the classpath triggers ThymeleafAutoConfiguration. It registers a ThymeleafViewResolver, a SpringTemplateEngine, and a ClassLoaderTemplateResolver — all with sensible defaults. No Java configuration is required.
XML
<!-- pom.xml: -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

# ── application.yml — default values (only override when needed): ─────
spring:
  thymeleaf:
    prefix: classpath:/templates/   # template root
    suffix: .html                   # file extension
    mode: HTML                      # template mode: HTML, XML, TEXT, JAVASCRIPT, CSS
    encoding: UTF-8
    cache: false                    # disable in dev; set true in prod
    check-template-location: true   # fail fast if templates/ directory missing
    check-template: true            # fail fast if a specific template is missing
    servlet:
      content-type: text/html       # response Content-Type

# ── Template file layout: ─────────────────────────────────────────────
# src/main/resources/
# └── templates/
#     ā”œā”€ā”€ index.html
#     ā”œā”€ā”€ layout/
#     │   └── base.html            ← shared layout fragment
#     ā”œā”€ā”€ users/
#     │   ā”œā”€ā”€ list.html            ← resolved by "users/list"
#     │   ā”œā”€ā”€ detail.html          ← resolved by "users/detail"
#     │   └── form.html            ← resolved by "users/form"
#     └── error/
#         ā”œā”€ā”€ 404.html
#         └── 500.html

Core Thymeleaf Expressions

Thymeleaf uses five expression syntaxes inside th:* attributes. Understanding them is the foundation for all template work.
html
<!-- ── Variable expression ${...} — access model attributes: ───────── -->
<p th:text="${user.name}">Placeholder name</p>
<p th:text="${user.role.name()}">ROLE</p>

<!-- ── Selection expression *{...} — access fields on th:object: ────── -->
<form th:object="${userForm}">
    <input th:field="*{name}" />       <!-- = th:field="${userForm.name}" -->
    <input th:field="*{email}" />
</form>

<!-- ── Message expression #{...} — internationalisation keys: ────────── -->
<h1 th:text="#{page.users.title}">Users</h1>
<p th:text="#{validation.required}">This field is required</p>

<!-- ── Link expression @{...} — URL building: ────────────────────────── -->
<a th:href="@{/users}">All users</a>
<a th:href="@{/users/{id}(id=${user.id})}">View user</a>
<a th:href="@{/users(page=${page},size=20)}">Page</a>  <!-- query params -->
<img th:src="@{/static/images/logo.png}" />

<!-- ── Fragment expression ~{...} — include fragments: ──────────────── -->
<div th:insert="~{layout/base :: header}"></div>
<div th:replace="~{layout/base :: footer}"></div>

<!-- ── Literals and operations inside expressions: ───────────────────── -->
<p th:text="'Hello, ' + ${user.name} + '!'"></p>
<p th:text="|Hello, ${user.name}!|"></p>          <!-- literal substitution -->
<p th:text="${user.age >= 18 ? 'Adult' : 'Minor'}"></p>   <!-- ternary -->
<p th:text="${user.bio} ?: 'No bio provided'"></p>        <!-- Elvis -->
<p th:text="${user.name?.toUpperCase()}"></p>            <!-- safe navigation -->

Iteration, Conditionals, and Attributes

th:each iterates over collections, th:if and th:unless control conditional rendering, and th:attr sets arbitrary HTML attributes. These three cover the vast majority of dynamic template logic.
html
<!-- ── th:each — iterate over a collection: ─────────────────────────── -->
<tr th:each="user : ${users}">
    <td th:text="${user.name}"></td>
    <td th:text="${user.email}"></td>
</tr>

<!-- th:each provides an iteration status variable: -->
<tr th:each="user, stat : ${users}"
    th:classappend="${stat.odd} ? 'odd' : 'even'">
    <td th:text="${stat.index}"></td>    <!-- 0-based index -->
    <td th:text="${stat.count}"></td>    <!-- 1-based count -->
    <td th:text="${stat.size}"><td>     <!-- total size -->
    <td th:text="${stat.first}"><td>    <!-- true on first iteration -->
    <td th:text="${stat.last}"><td>     <!-- true on last iteration -->
    <td th:text="${user.name}"></td>
</tr>

<!-- ── th:if / th:unless — conditional rendering: ───────────────────── -->
<p th:if="${users.empty}">No users found.</p>
<p th:unless="${users.empty}" th:text="${users.size()} + ' users found'"></p>

<!-- th:if on a container removes both the element and its children: -->
<div th:if="${currentUser.admin}">
    <a href="/admin">Admin Panel</a>
</div>

<!-- ── th:switch / th:case: ──────────────────────────────────────────── -->
<span th:switch="${user.role}">
    <span th:case="'ADMIN'" class="badge-red">Admin</span>
    <span th:case="'USER'"  class="badge-blue">User</span>
    <span th:case="*"       class="badge-gray">Unknown</span>
</span>

<!-- ── th:classappend — add CSS class conditionally: ────────────────── -->
<tr th:classappend="${user.active} ? 'active' : 'inactive'">

<!-- ── th:attr — set any HTML attribute: ────────────────────────────── -->
<input th:attr="placeholder=#{form.name.placeholder},
                maxlength=${maxLength}" />

<!-- ── th:attrappend / th:attrprepend: ──────────────────────────────── -->
<input class="form-control" th:attrappend="class=${' is-invalid'}"/>

<!-- ── th:remove — remove elements from the final output: ───────────── -->
<tr th:remove="all-but-first">  <!-- keep only the first row in static preview -->
    <td>Sample data</tr>
</tr>

Fragments and Layouts

Thymeleaf fragments define reusable template sections. th:fragment marks a fragment; th:insert and th:replace include it. The Thymeleaf Layout Dialect (a separate library) extends this with a full inheritance-based layout system.
html
<!-- ── Define fragments in a shared file (templates/layout/base.html): ─ -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head th:fragment="head(title)">
    <meta charset="UTF-8" />
    <title th:text="${title} + ' | MyApp'">MyApp</title>
    <link rel="stylesheet" th:href="@{/static/css/main.css}" />
</head>

<nav th:fragment="navbar">
    <a th:href="@{/}">Home</a>
    <a th:href="@{/users}">Users</a>
</nav>

<footer th:fragment="footer">
    <p>Ā© 2024 MyApp</p>
</footer>

</html>

<!-- ── Include fragments in a page template: ────────────────────────── -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head th:replace="~{layout/base :: head('Users')}"></head>

<body>
    <!-- th:insert — keeps the container element, inserts fragment inside: -->
    <div th:insert="~{layout/base :: navbar}"></div>

    <!-- th:replace — replaces the container element with the fragment: -->
    <nav th:replace="~{layout/base :: navbar}"></nav>

    <main>
        <h1>Users</h1>
        <!-- page-specific content -->
    </main>

    <th:block th:replace="~{layout/base :: footer}"></th:block>
</body>
</html>

<!-- ── Fragment with parameters: ────────────────────────────────────── -->
<!-- Define: -->
<div th:fragment="userCard(user)">
    <h3 th:text="${user.name}"></h3>
    <p th:text="${user.email}"></p>
</div>

<!-- Use: -->
<div th:replace="~{fragments/user :: userCard(${selectedUser})}"></div>

<!-- ── Inline fragment (same file): ─────────────────────────────────── -->
<div th:replace="~{:: #my-fragment}"></div>
<div id="my-fragment">content</div>

Utility Objects

Thymeleaf exposes utility objects in all template expressions under the # prefix. These provide formatting, date handling, string manipulation, and collection operations without needing model attributes.
html
<!-- ── #dates / #temporals — date and time formatting: ──────────────── -->
<p th:text="${#temporals.format(user.createdAt, 'dd MMM yyyy')}"></p>
<p th:text="${#temporals.format(user.createdAt, 'dd/MM/yyyy HH:mm')}"></p>
<p th:text="${#dates.format(legacyDate, 'yyyy-MM-dd')}"></p>

<!-- ── #numbers — number formatting: ────────────────────────────────── -->
<p th:text="${#numbers.formatDecimal(price, 1, 2)}"></p>      <!-- 1,234.56 -->
<p th:text="${#numbers.formatCurrency(amount, 'en', 'GBP')}"></p>

<!-- ── #strings — string utilities: ─────────────────────────────────── -->
<p th:text="${#strings.toUpperCase(user.name)}"></p>
<p th:text="${#strings.abbreviate(user.bio, 100)}"></p>
<p th:if="${#strings.isEmpty(user.bio)}">No bio</p>
<p th:text="${#strings.replace(text, 'old', 'new')}"></p>

<!-- ── #lists / #sets — collection utilities: ───────────────────────── -->
<p th:text="${#lists.size(users)}"></p>
<p th:if="${#lists.isEmpty(users)}">Empty</p>

<!-- ── #fields — form validation (see Form Validation entry): ────────── -->
<span th:if="${#fields.hasErrors('email')}"
      th:errors="*{email}"></span>

<!-- ── #request / #session / #servletContext: ────────────────────────── -->
<p th:text="${#request.getParameter('q')}"></p>
<p th:text="${#session.getAttribute('userId')}"></p>

<!-- ── Inline expressions — use Thymeleaf inside text nodes: ─────────── -->
<p>Hello, [[${user.name}]]!</p>               <!-- escaped output -->
<script th:inline="javascript">
    const userId = [[${user.id}]];            <!-- JS-safe output -->
    const userName = [[${user.name}]];
</script>

Spring Security Integration

The Thymeleaf Extras Spring Security library adds sec:* attributes for showing or hiding content based on authentication state and roles — without writing controller logic for display conditions.
XML
<!-- pom.xml: -->
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>

<!-- Template — add the sec namespace: -->
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

<!-- ── Show only when authenticated: ────────────────────────────────── -->
<div sec:authorize="isAuthenticated()">
    <p>Welcome back!</p>
</div>

<!-- ── Show only when NOT authenticated: ────────────────────────────── -->
<div sec:authorize="!isAuthenticated()">
    <a th:href="@{/login}">Login</a>
    <a th:href="@{/register}">Register</a>
</div>

<!-- ── Show only for a specific role: ───────────────────────────────── -->
<div sec:authorize="hasRole('ADMIN')">
    <a th:href="@{/admin}">Admin Panel</a>
</div>

<!-- ── Show for any of multiple roles: ──────────────────────────────── -->
<div sec:authorize="hasAnyRole('ADMIN', 'MODERATOR')">
    <a th:href="@{/moderate}">Moderate</a>
</div>

<!-- ── Display the authenticated user's details: ─────────────────────── -->
<span sec:authentication="name">Username</span>
<span sec:authentication="principal.email">email</span>

<!-- ── CSRF token in forms (auto-included by Spring Security, but explicit): -->
<form method="post" th:action="@{/users}">
    <input type="hidden" th:name="${_csrf.parameterName}"
                         th:value="${_csrf.token}" />
    <!-- fields -->
</form>
<!-- Spring Security's Thymeleaf integration adds CSRF automatically
     to any th:action form — the explicit input above is not needed. -->