Thymeleaf Fragments: Building Reusable Page Components Without Template Inheritance
Thymeleaf fragments let you build reusable, parameterized page components from plain HTML files. Learn th:replace vs th:insert, a worked card example, and the pitfalls.
19 Sept 2025, 03:46 UTC

Your Spring MVC app has fifteen pages, and the same header, footer, and alert banner are copy-pasted into all of them. Someone tweaks the navigation markup, and now you are diffing fifteen files. Thymeleaf's answer to this is not a layout inheritance system — it is fragments: small, named chunks of plain HTML that any page can pull in. This post shows how fragments work, how to parameterize them, and the one decision — th:replace versus th:insert — that quietly changes your rendered DOM.
The idea: valid HTML files that double as a component library
A fragment is just an element marked with th:fragment. Because Thymeleaf templates are natural templates (valid HTML that still opens sensibly in a browser), a fragment file is both a component library and a previewable document. A designer can open fragments/components.html directly and see styled markup; the server sees reusable pieces.
In a Spring Boot app with spring-boot-starter-thymeleaf on the classpath, templates resolve from classpath:/templates with an .html suffix. So the reference ~{fragments/components :: card} maps predictably to templates/fragments/components.html, fragment card. The ~{...} wrapper is a fragment expression — it tells Thymeleaf "resolve this as a template fragment, not a URL."
th:replace vs th:insert: the decision that changes your DOM
Both attributes include a fragment, but they treat the host element differently:
th:replaceremoves the host tag entirely and substitutes the fragment in its place.th:insertkeeps the host tag and places the fragment inside it as a child.
This is not a cosmetic distinction. If your CSS targets .card > .card-body or your JavaScript hooks a specific element id, choosing the wrong one silently breaks selectors after a refactor. Rule of thumb: use th:replace when the fragment is a complete, self-contained component (the usual case); use th:insert when the host element carries meaning you want to keep, such as a wrapper with layout classes.
A worked example: a parameterized card component
Fragments can declare parameters, which turns one file into a small library of reusable UI pieces. Here is a fragment file at templates/fragments/components.html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="card" th:fragment="card(title, body)">
<h3 class="card-title" th:text="${title}">Sample title</h3>
<p class="card-body" th:text="${body}">Sample body text.</p>
</div>
</body>
</html>And a page that uses it twice, at templates/dashboard.html:
<section class="grid">
<div th:replace="~{fragments/components :: card('Revenue', 'Up 12% this quarter')}"></div>
<div th:replace="~{fragments/components :: card('Errors', '3 open incidents')}"></div>
</section>The rendered output contains two <div class="card"> elements — the placeholder host <div> is gone because th:replace substitutes the fragment. Had you used th:insert, each card would be wrapped in an extra empty <div>, which can disturb grid or flex layouts.
To check the result yourself: run the app, load the page, and view source (or curl the endpoint). Confirm the host wrapper is absent and the parameter values appear in the h3 and p. The static text inside the fragment ("Sample title") is what a designer sees opening the file directly; it is replaced at render time.
Development ergonomics: turn off the cache
Spring Boot enables Thymeleaf template caching by default, which is right for production but maddening while iterating on markup — you edit a fragment and nothing changes. In application.properties for your dev profile:
spring.thymeleaf.cache=falseVerify it took effect by editing the fragment file and reloading the page without restarting; the change should appear immediately. Just make sure the property is not set (or is true) in production, since per-request template parsing costs real CPU under load.
Trade-offs and limitations
Fragments are not free of friction. Three things bite in practice:
- Version sensitivity. Fragment expression syntax and parameter passing differ between Thymeleaf 2 and 3. Tutorials from the Thymeleaf 2 era produce confusing parse errors on 3.x. Check your version — for example with
./mvnw dependency:tree | grep thymeleaf(run from the project root; no special permissions needed) — and read the matching documentation. - Nesting depth. Fragments including fragments including fragments makes server-side rendering errors hard to trace; the exception points at the outer template, not the nested culprit. Keep composition to one or two levels.
- Refactoring risk. Moving markup into a fragment with
th:replacechanges the DOM shape. Existing CSS selectors and JS hooks can break without any error — only a visual regression. After refactoring, spot-check the rendered HTML and your browser's dev tools.
Where to start
Pick the most-duplicated markup in your app — usually the header or an alert banner — and extract it into templates/fragments/ with one parameter. Reference it with th:replace, disable the cache in dev, and view source to confirm the DOM is what your CSS expects. Once that round-trip feels boring, you have a component library growing out of plain HTML, with no inheritance hierarchy to fight.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.