Thymeleaf Fragments: Choosing th:replace or th:insert for Shared Layouts
Learn when to use th:replace versus th:insert for Thymeleaf page layouts, plus how fragment parameters and template caching affect rendering.
06 Feb 2026, 12:21 UTC

The problem: copying the same header into every template
A Spring MVC application grows from three pages to thirty. Each Thymeleaf template repeats the same navigation bar, footer, and CSS links. When the marketing team changes a link, someone edits ten files and misses one. The result is inconsistent navigation and a layout bug that only appears in production.
The useful takeaway is to extract shared markup into Thymeleaf fragments and include them with th:replace or th:insert. The two attributes look similar, but they produce different HTML, and choosing the wrong one can break CSS selectors and layout expectations.
th:replace versus th:insert: what actually changes in the DOM
Thymeleaf fragments are reusable pieces of a template, marked with th:fragment. You include them from another template using th:replace or th:insert. Both attributes resolve the fragment through the configured Spring TemplateResolver, which by default looks under classpath:/templates/ and appends .html.
The difference is structural:
| Attribute | Rendered result | Best for |
|---|---|---|
th:replace | The host element is removed and replaced by the fragment markup. | Page shells, full layout wrappers. |
th:insert | The fragment markup is placed inside the host element. | Reusable blocks inside an existing container. |
For example, if _fragments/header.html defines:
<div th:fragment="header">
<nav><a href="/">Home</a></nav>
</div>
and a page template includes it like this:
<div th:replace="~{_fragments/header :: header}"></div>
the browser receives only the fragment's <div>. The empty host <div> disappears. With th:insert, the browser receives the host <div> containing the fragment's <div>, creating an extra wrapper. That extra layer can break CSS rules that expect a specific nesting depth.
Worked example: a parameterized layout fragment
Fragments can accept parameters, so one layout template can serve many pages with different titles and active menu items. This avoids duplicating the <head> and navigation shell.
Create src/main/resources/templates/_layouts/main.html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="layout(title, active)">
<title th:text="${title}">Default title</title>
<link rel="stylesheet" href="/css/main.css">
</head>
<body th:replace="~{_fragments/body :: body(active)}"></body>
</html>
Then src/main/resources/templates/_fragments/body.html:
<div th:fragment="body(active)">
<nav>
<a th:class="${active == 'home' ? 'active' : ''}" href="/">Home</a>
<a th:class="${active == 'about' ? 'active' : ''}" href="/about">About</a>
</nav>
<main th:insert="~{_fragments/content :: content}"></main>
</div>
A page template such as home.html uses the layout:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{_layouts/main :: layout('Home', 'home')}"></head>
<body>
<p th:text="${welcomeMessage}">Welcome</p>
</body>
</html>
Here th:replace removes the empty <head> host and inserts the fragment's <head> markup. Inside the layout, th:insert keeps the <main> container because the page content needs that wrapper for styling.
A minimal Spring MVC controller maps the page:
@Controller
public class HomeController {
@GetMapping("/")
public String home(Model model) {
model.addAttribute("welcomeMessage", "Welcome to the site");
return "home";
}
}
Run the application locally and request http://localhost:8080/. Use the browser's developer tools to inspect the <head> and navigation markup. Confirm that the title contains "Home" and that the Home link has the active class. If the title is empty, check the fragment parameter name and spelling against the th:fragment declaration.
Limitations and how to verify changes
Fragment problems usually appear at request time, not when the application starts. A mistyped fragment path or parameter name often renders an empty section instead of failing fast, so every page that uses a fragment needs to be loaded at least once during testing.
Thymeleaf caches compiled templates by default. In production this improves performance, but it also means editing _layouts/main.html does not update the running application until the cache is cleared or the application restarts. For local development, set:
spring.thymeleaf.cache=false
in application.properties. To verify a fragment edit in a non-production environment, change the template, clear the Thymeleaf cache if it is enabled, reload the page, and inspect the response HTML. In production, plan a restart or cache-clearing step as part of the deployment.
Quick diagnostic checklist
- Open the rendered page source and check whether the host element is missing (
th:replace) or still wrapping the fragment (th:insert). - If expected text is missing, confirm the parameter name in
th:paramor the fragment signature matches exactly. - If a recent template edit does not appear, check
spring.thymeleaf.cacheand restart or clear the cache.
Closing
Use th:replace when the host element is just a placeholder and should vanish from the final HTML. Use th:insert when the host element is a meaningful container. Parameterized fragments let a single layout drive many pages, but verify each rendered page because fragment errors surface at request time and template caching can hide recent edits.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.