Thymeleaf(タイムリーフ)とは、HTMLのタグに th:text や th:each などの属性を書き足すだけで、サーバー側のデータを埋め込んだHTMLを作れるJava向けのテンプレートエンジンです。 Spring Boot では spring-boot-starter-thymeleaf を追加し、src/main/resources/templates/ にHTMLを置けばすぐ使えます。属性を書いたHTMLはブラウザでそのまま開いても崩れないため、デザイナーとエンジニアが同じファイルを扱いやすいのが特長です。

この記事では、主要な属性約30個と5種類の式(${} *{} #{} @{} ~{})を「書き方 → 出力されるHTML」の対応表でまとめました。さらに、th:each のステータス変数8種類すべて、#strings #numbers #temporals #lists #dates などのユーティリティ一覧、fragment を使ったヘッダー・フッターの共通化(引数付き fragment を含む)、th:utext を使うときの注意と Spring Boot の設定まで、1ページで確認できるようにしています。

確認環境:2026年10月2日時点の最新版 Thymeleaf 3.1.5.RELEASE(2026年4月21日リリース) を、公式リポジトリのタグからソースでビルドし、OpenJDK 21.0.10 上で TemplateEngine 単体で実行しました(依存:attoparser 2.0.7、unbescape 1.1.6、OGNL 3.3.4)。記事中の「実行結果」はその出力をそのまま載せたものです。単体実行では式の評価に OGNL を使うため、Spring 連携が必要な th:field・#fields・CSRF トークンの出力は未実行で、公式ドキュメントの仕様に基づく例として示しています(Spring Boot では式の評価に SpEL が使われます)。

Thymeleaf属性と式の早見表

まず「何を書くと、どんなHTMLになるか」を一覧にします。例の前提となるデータは、表の下にまとめています。

やりたいこと 書き方 出力HTML(3.1.5 で実行。※は仕様に基づく例)
文字を表示する(エスケープあり) <p th:text="${user.name}">名前</p> <p>山田太郎</p>
HTMLとして表示する(エスケープなし) <p th:utext="${notice}">お知らせ</p> <p><b>本日休業</b></p>
繰り返す <li th:each="item : ${items}" th:text="${item.name}">商品</li> <li>ノート</li><li>ペン</li><li>付箋</li>
条件が true のときだけ表示 <p th:if="${stock > 0}">在庫あり</p> <p>在庫あり</p>(false なら要素ごと出力なし)
条件が false のときだけ表示 <p th:unless="${stock > 0}">在庫なし</p> 出力なし(stock=12 のとき)
複数の分岐 <div th:switch="${user.role}"><p th:case="'ADMIN'">管理者</p><p th:case="*">一般</p></div> <div><p>管理者</p></div>
リンクURLを作る <a th:href="@{/orders/{id}(id=${order.id})}">詳細</a> <a href="/orders/101">詳細</a>
クエリ付きURL <a th:href="@{/orders(page=${page})}">次へ</a> <a href="/orders?page=2">次へ</a>
input の value <input type="text" th:value="${user.name}"> <input type="text" value="山田太郎">
フォームの対象オブジェクト <form th:action="@{/members}" th:object="${memberForm}" method="post"> ※<form action="/members" method="post">(Spring では CSRF の hidden も付く)
フォーム項目とバインド(Spring) <input type="text" th:field="*{email}"> ※<input type="text" id="email" name="email" value="a@example.com">
class を置き換える <p class="msg" th:class="${error} ? 'msg-error' : 'msg'"> <p class="msg-error">(error=true)
class を追加する <tr class="row" th:classappend="${itemStat.odd} ? 'odd'"> <tr class="row odd">(奇数行)
任意の属性を設定 <a th:attr="data-id=${item.id},title=${item.name}"> <a data-id="2" title="ペン">
属性に追記 <button class="btn" th:attrappend="class=${' ' + style}"> <button class="btn warning">
ローカル変数を作る <div th:with="total=${price * qty}"><span th:text="${total}"></span></div> <div><span>360</span></div>
タグを出力しない入れ物 <th:block th:each="item : ${items}">…</th:block> 中身だけが繰り返し出力され、th:block 自体は消える
部品を中に入れる <div th:insert="~{common :: footer}"></div> <div><footer>…</footer></div>
部品で置き換える <div th:replace="~{common :: footer}"></div> <footer>…</footer>
JavaScript に値を埋め込む <script th:inline="javascript">const code = /*[[${code}]]*/ "仮";</script> const code = "ITEM-01";
本文中に式を埋め込む <p>こんにちは、[[${user.name}]]さん</p> <p>こんにちは、山田太郎さん</p>
確認用のダミー要素を消す <tr th:remove="all">ダミー行</tr> 出力なし
select の選択状態 <option value="B" th:selected="${selected} == 'B'"> <option value="B" selected="selected">
チェック状態 <input type="checkbox" th:checked="${user.active}"> <input type="checkbox" checked="checked">
無効化 <button th:disabled="${!editable}">保存</button> <button disabled="disabled">保存</button>
文字を連結する <p th:text="|合計 ${count} 件|"> <p>合計 3 件</p>
null のときの既定値 <p th:text="${user.nickname} ?: '未設定'"> <p>未設定</p>
選択オブジェクトの項目 <div th:object="${user}"><p th:text="*{name}"></p></div> <div><p>山田太郎</p></div>

例の前提データ:user.name="山田太郎"、user.role="ADMIN"、user.nickname=null、user.active=true、notice="<b>本日休業</b>"、items は「ノート・ペン・付箋」の3件(item はペン、id=2)、code="ITEM-01"、stock=12、order.id=101、page=2、price=120、qty=3、count=3、selected="B"。URLの例は、アプリのコンテキストパスが /(Spring Boot の既定)の場合です。コンテキストパスが /shop なら @{/orders} は /shop/orders になります。

5種類の式の早見表

式 名前 意味 例
${...} 変数式 Model やリクエストに入れた値を参照する(Spring では SpEL) ${user.name}
*{...} 選択変数式 th:object で選んだオブジェクトの項目を参照する *{email}
#{...} メッセージ式 messages.properties の文言を参照する(多言語対応。キーがないと ??キー_ja_JP?? と表示) #{order.title}、#{hello(${user.name})}
@{...} リンクURL式 コンテキストパスを補い、パス変数やクエリを組み立てる @{/orders/{id}(id=${o.id})}
~{...} フラグメント式 別テンプレートの部品を指定する ~{common :: header}

迷ったら「データは ${}、フォームの中は *{}、文言は #{}、URLは @{}、部品は ~{}」と覚えると選びやすくなります。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Thymeleafの使い方|th:each・th:ifなど属性一覧と出力例」

Spring BootでThymeleafを使う最小構成

依存関係を追加する

Spring Boot では spring-boot-starter-thymeleaf を追加するだけで、Thymeleaf とSpring連携(thymeleaf-spring6)が自動設定されます。 バージョンは Spring Boot の依存関係管理に任せるため、個別に書く必要はありません。

<!-- pom.xml(Spring Boot 3.x / 4.x) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

プロジェクトの作り方(Spring Initializr)やAPIの組み立ては、既存記事「Spring BootでREST APIを作る|入力チェック・エラー処理・テストまで」で詳しく扱っています。ここでは画面(HTML)を返す部分に絞ります。

Controller とテンプレートの対応

@Controller のメソッドが返す文字列が、templates/ 以下のファイル名(拡張子なし)になります。 @RestController にすると文字列がそのままレスポンス本文になり、テンプレートは使われないので注意します。

// src/main/java/com/example/shop/ItemController.java
package com.example.shop;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class ItemController {

    public record Item(long id, String name, BigDecimal price, int stock) {}

    @GetMapping("/items")
    public String list(Model model) {
        List<Item> items = List.of(
                new Item(1, "ノート", new BigDecimal("120"), 30),
                new Item(2, "ペン", new BigDecimal("80"), 0),
                new Item(3, "付箋", new BigDecimal("150"), 12));
        model.addAttribute("items", items);
        model.addAttribute("updatedAt", LocalDateTime.of(2026, 10, 2, 9, 30));
        return "items/list"; // → src/main/resources/templates/items/list.html
    }
}
<!-- src/main/resources/templates/items/list.html -->
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>商品一覧</title>
</head>
<body>
  <h1>商品一覧</h1>
  <p>更新:<span th:text="${#temporals.format(updatedAt, 'yyyy/MM/dd HH:mm')}">2000/01/01 00:00</span></p>
  <table>
    <tr><th>No.</th><th>商品名</th><th>価格</th><th>在庫</th></tr>
    <tr th:each="item, stat : ${items}" th:classappend="${stat.odd} ? 'odd' : 'even'">
      <td th:text="${stat.count}">1</td>
      <td th:text="${item.name}">サンプル商品</td>
      <td th:text="${#numbers.formatInteger(item.price, 1, 'COMMA')} + '円'">0円</td>
      <td>
        <span th:if="${item.stock > 0}" th:text="${item.stock}">10</span>
        <span th:unless="${item.stock > 0}">在庫なし</span>
      </td>
    </tr>
  </table>
</body>
</html>

このテンプレートを Thymeleaf 3.1.5 で処理した実行結果(<table> の前半2行分)は次のとおりです。

  <p>更新:<span>2026/10/02 09:30</span></p>
  <table>
    <tr><th>No.</th><th>商品名</th><th>価格</th><th>在庫</th></tr>
    <tr class="odd">
      <td>1</td>
      <td>ノート</td>
      <td>120円</td>
      <td>
        <span>30</span>

      </td>
    </tr>
    <tr class="even">
      <td>2</td>
      <td>ペン</td>
      <td>80円</td>
      <td>

        <span>在庫なし</span>
      </td>
    </tr>

th:if / th:unless で消えた要素の位置には空行が残ります(表示には影響しません)。

ポイントは、th: 属性のない部分(サンプル商品 などの仮の文字)がブラウザで直接開いたときの見た目用で、サーバーを通すと置き換わることです。xmlns:th は書かなくても動きますが、IDEの補完のために付けておくと便利です。

主要属性の使い方と出力例

th:text と th:utext(文字の表示)

th:text は値をHTMLエスケープしてから表示し、th:utext はエスケープせずにHTMLとして出力します。 普段は th:text を使い、th:utext は「信頼できるHTMLだけ」に限定します(理由は後半の「th:utextの注意」で説明します)。

実行結果では、値が <b>Hi</b> のとき、th:text と [[${msg}]] は <p>&lt;b&gt;Hi&lt;/b&gt;</p>(画面には文字として表示)、th:utext と [(${msg})] は <p><b>Hi</b></p>(太字で表示)になりました。

本文中に直接書く [[...]](エスケープあり)と [(...)](エスケープなし)は「インライン式」と呼ばれ、Thymeleaf 3 では既定で有効です。

th:each(繰り返し)

th:each="変数 : ${コレクション}" を付けた要素が、要素の数だけ繰り返し出力されます。 List・配列・Set・Map・Iterator などが使えます。

<!-- List -->
<ul>
  <li th:each="item : ${items}" th:text="${item.name}">商品名</li>
</ul>

<!-- Map:entry.key / entry.value -->
<dl>
  <th:block th:each="entry : ${stockMap}">
    <dt th:text="${entry.key}">商品</dt>
    <dd th:text="${entry.value}">0</dd>
  </th:block>
</dl>

実行結果(stockMap は {ノート=30, ペン=0} の LinkedHashMap)は次のとおりです。数値の範囲は ${#numbers.sequence(1, 5)} で繰り返せます。

<ul>
  <li>ノート</li>
  <li>ペン</li>
  <li>付箋</li>
</ul>
<dl>

    <dt>ノート</dt>
    <dd>30</dd>

    <dt>ペン</dt>
    <dd>0</dd>

</dl>

th:block 自体は出力されず、<dt> と <dd> をまとめて繰り返せます(位置に空行が残ります)。

th:if・th:unless・th:switch(条件分岐)

th:if は条件が true のとき、th:unless は false のときだけ要素を出力します。 条件が満たされない要素は、子要素も含めて丸ごと出力されません。

Thymeleaf の th:if は、値が boolean 以外でも次のルールで判定します。

値 th:if の判定
null false
boolean の true / false そのまま
数値 0 以外なら true
文字 "false" "off" "no" 以外なら true(空文字も true)
上記以外(List など) null でなければ true(空の List も true)

空のリストは true と判定されるため、「1件もないときに表示」したい場合は #lists.isEmpty() を使います。

<p th:if="${#lists.isEmpty(items)}">商品はまだありません。</p>
<table th:unless="${#lists.isEmpty(items)}">…</table>

<li th:each="item : ${items}" th:if="${item.stock > 0}" th:text="${item.name}">x</li>

<div th:switch="${order.status}">
  <span th:case="'NEW'">受付</span>
  <span th:case="'SHIPPED'">発送済み</span>
  <span th:case="*">その他</span>  <!-- どれにも当てはまらないとき -->
</div>

実行結果(空のリストを渡し、order.status が SHIPPED、items は通常の3件)では、1行目が <p>商品はまだありません。</p>、th:each と th:if を同じ要素に書いた行が <li>ノート</li> と <li>付箋</li>(在庫0のペンは出力なし)、th:switch が <span>発送済み</span> になりました。th:case="*" は既定(default)の分岐で、最後に書きます。

th:href・th:src と @{}(URLの組み立て)

@{} はコンテキストパスを自動で付け、パス変数とクエリパラメーターを組み立てます。

書き方 実行結果(コンテキストパス /shop、item.id=2、page=2)
@{/items} /shop/items
@{/items/{id}(id=${item.id})} /shop/items/2
@{/items(page=${page})} /shop/items?page=2
@{/items/{id}(id=${item.id}, tab='detail')} /shop/items/2?tab=detail(パスで使わなかった引数はクエリになる)
@{/items(page=${page},sort='name')} /shop/items?page=2&amp;sort=name(HTML上の &amp; はブラウザで & として扱われる)
@{https://example.com/help} https://example.com/help(絶対URLはそのまま)
@{~/static/a.css} /static/a.css(サーバー相対:コンテキストパスを付けない)

th:object・th:field・th:action(フォーム)

th:object でフォームの対象オブジェクトを指定し、その中の項目は *{} と th:field で書きます。 th:field は Spring 連携(thymeleaf-spring6)の機能で、id・name・value をまとめて出力します。

// フォーム用のクラス(Bean Validation は既存記事「Spring Bootバリデーション一覧」を参照)
public class MemberForm {
    private String name;
    private String email;
    private String plan = "BASIC";
    private boolean newsletter = true;
    // getter / setter は省略
}
<form th:action="@{/members}" th:object="${memberForm}" method="post">
  <input type="text" th:field="*{name}">
  <p th:if="${#fields.hasErrors('name')}" th:errors="*{name}">エラー</p>

  <input type="email" th:field="*{email}">

  <select th:field="*{plan}">
    <option value="BASIC">ベーシック</option>
    <option value="PRO">プロ</option>
  </select>

  <input type="checkbox" th:field="*{newsletter}"> お知らせを受け取る
  <button type="submit">登録</button>
</form>

th:field は Spring 連携が必要なため手元では未実行です。公式ドキュメントの仕様に基づく出力例(name=山田太郎、email=a@example.com の場合)は次のとおりです。

<form action="/members" method="post">
  <input type="text" id="name" name="name" value="山田太郎">

  <input type="email" id="email" name="email" value="a@example.com">

  <select id="plan" name="plan">
    <option value="BASIC" selected="selected">ベーシック</option>
    <option value="PRO">プロ</option>
  </select>

  <input type="checkbox" id="newsletter1" name="newsletter" value="true" checked="checked"><input type="hidden" name="_newsletter" value="on"> お知らせを受け取る
  <button type="submit">登録</button>
<input type="hidden" name="_csrf" value="(トークン)"></form>

th:field は id・name・value を自動で付け、チェックボックスには「チェックを外して送信した」ことを伝える hidden(_newsletter)を追加します。 Spring Security が有効なら th:action の POST フォームに _csrf の hidden も付きます。初期値はフォームオブジェクト側に入れておくのが確実です。

th:class・th:classappend・th:attr(属性の変更)

th:class は class を置き換え、th:classappend は既存の class の後ろに追加します。

<li class="item" th:classappend="${item.stock == 0} ? 'soldout'">…</li>
<div th:attr="data-id=${item.id}, data-name=${item.name}">…</div>

実行結果(item はペン、stock=0)は <li class="item soldout">、<div data-id="2" data-name="ペン"> です。th:data-id="${item.id}" のように th:属性名 でも任意の属性を書けます。条件の後ろの else を省略すると、false のときは何も追加されません。

th:inline(JavaScript・CSSへの埋め込み)

th:inline="javascript" を付けると、[[...]] の値がJavaScriptとして正しい形(文字列は引用符付き、配列やオブジェクトはJSON)で出力されます。

<script th:inline="javascript">
  const itemCode = /*[[${code}]]*/ "ITEM-00";
  const itemIds  = /*[[${itemIds}]]*/ [];
  const apiBase  = /*[[@{/api}]]*/ "/api";
</script>

実行結果(code = "ITEM-01"、itemIds = [1, 2, 3])は次のとおりです。

<script>
  const itemCode = "ITEM-01";
  const itemIds  = [1,2,3];
  const apiBase  = "\/api";
</script>

/ は \/ に、日本語などASCII以外の文字は「バックスラッシュ + u + 16進4桁」のUnicodeエスケープに変換されて出力されました。 どちらもJavaScriptとしては元の文字列("/api"、"山田太郎")と同じ値なので、そのまま使えます。HTMLソースを見て文字化けと勘違いしやすいポイントです。

/*[[...]]*/ "ITEM-00" の形なら、ブラウザで直接開いたときは仮の値、サーバーを通すと実際の値になります。CSS も th:inline="css" で同様です。

th:remove(確認用の要素を消す)

値は all(要素ごと削除)・body(子要素だけ削除)・tag(タグだけ削除し子要素は残す)・all-but-first(最初の子要素以外を削除)・none(何もしない)の5つです。

<tbody th:remove="all-but-first">
  <tr th:each="item : ${items}"><td th:text="${item.name}">ノート</td></tr>
  <tr><td>ダミー行(ブラウザで直接開いたときの見た目用)</td></tr>
  <tr><td>ダミー行</td></tr>
</tbody>

実行結果は <tr><td>ノート</td></tr>・<tr><td>ペン</td></tr>・<tr><td>付箋</td></tr> の3行だけになり、ダミー行は消えました。デザイン確認用のダミー行を置いたままにでき、サーバーを通すと実データだけが残ります。

属性が処理される順番

1つの要素に複数の th: 属性を書いた場合、書いた順ではなく決まった優先順位で処理されます。

順 機能 属性
1 fragment の挿入 th:insert th:replace
2 繰り返し th:each
3 条件 th:if th:unless th:switch th:case
4 ローカル変数 th:object th:with
5 汎用の属性変更 th:attr th:attrprepend th:attrappend
6 個別の属性変更 th:value th:href th:src など
7 本文 th:text th:utext
8 fragment の定義 th:fragment
9 fragment の削除 th:remove

th:each は th:if より先に処理されるため、<li th:each="item : ${items}" th:if="${item.stock > 0}"> は「在庫がある商品だけ表示」として動きます。 逆に「リスト全体が空でないときだけ繰り返す」なら、外側の要素に th:if を書きます。

th:eachのステータス変数(全8プロパティ)

th:each="item, stat : ${items}" のように2つ目の変数を書くと、繰り返しの状態(何番目か、最初か最後か)を参照できます。 省略した場合も「変数名 + Stat」(例:itemStat)という名前で自動的に使えます。

プロパティ 意味 3件のリストでの実行結果(1件目 / 2件目 / 3件目)
index 0から始まる番号 0 / 1 / 2
count 1から始まる番号 1 / 2 / 3
size 全体の件数 3 / 3 / 3
current 現在の要素(item と同じ) ノート / ペン / 付箋
even 偶数回目か(count 基準) false / true / false
odd 奇数回目か(count 基準) true / false / true
first 最初の要素か true / false / false
last 最後の要素か false / false / true

even と odd は count(1始まり)で判定されるため、1件目は odd になります。 index(0始まり)で考えると逆に感じるので注意します。

<table>
  <tr th:each="item : ${items}" th:classappend="${itemStat.odd} ? 'odd'">
    <td th:text="${itemStat.count} + '/' + ${itemStat.size}">1/1</td>
    <td th:text="${item.name}">商品</td>
    <td th:if="${itemStat.first}">新着</td>
  </tr>
</table>

実行結果は次のとおりです。

<table>
  <tr class="odd">
    <td>1/3</td>
    <td>ノート</td>
    <td>新着</td>
  </tr>
  <tr>
    <td>2/3</td>
    <td>ペン</td>

  </tr>
  <tr class="odd">
    <td>3/3</td>
    <td>付箋</td>

  </tr>
</table>

ユーティリティオブジェクト一覧(#strings・#numbers・#temporalsなど)

# で始まるユーティリティオブジェクトを使うと、文字・数値・日付の整形をテンプレート内で行えます。 Thymeleaf 3.1 からは #temporals(Java 8 の日付API用)が本体に統合され、追加の依存関係なしで使えるようになりました。

オブジェクトの一覧

オブジェクト 対象 主な用途
#strings String 空チェック、切り出し、置換、省略、結合
#numbers 数値 桁区切り、小数点以下の桁数、パーセント、連番
#temporals LocalDate・LocalDateTime など 日付の書式化、年月日の取り出し
#dates java.util.Date 日付の書式化(Date 型の場合)
#lists List 件数、空チェック、含むか、並べ替え
#sets / #maps / #arrays Set・Map・配列 件数、空チェック、含むか
#objects 任意のオブジェクト null のときの既定値
#aggregates 配列・コレクション 合計・平均
#ids id属性 繰り返しの中で重ならない id を作る
#fields フォーム(Spring) 入力チェックのエラー判定と取得

ほかに #calendars・#bools・#messages・#uris・#conversions・#execInfo があります。

よく使うメソッドと結果

書き方 実行結果(ロケール ja_JP)
${#strings.isEmpty(user.nickname)} true(null または空なら true)
${#strings.defaultString(user.nickname, '未設定')} 未設定
${#strings.length('ノート')} 3
${#strings.capitalize('order')} Order
${#strings.startsWith('ITEM-01', 'ITEM-')} true
${#strings.substring('2026-10-02', 0, 4)} 2026
${#strings.replace('a-b-c', '-', '/')} a/b/c
${#strings.abbreviate(title, 10)} Thymele...(title が「Thymeleafの属性一覧と出力例」のとき。先頭7文字 + ...)
${#strings.listJoin(names, ', ')} ノート, ペン, 付箋
${#numbers.formatInteger(1234567, 1, 'COMMA')} 1,234,567
${#numbers.formatInteger(7, 3)} 007(最低3桁にゼロ埋め)
${#numbers.formatDecimal(1234.5, 1, 'COMMA', 2, 'POINT')} 1,234.50
${#numbers.formatPercent(0.256, 1, 1)} 25.6%
${#numbers.formatCurrency(1200)} ¥1,200(ロケールに応じた通貨表記)
${#numbers.sequence(1, 5)} 1〜5 の連番の配列(th:each で使う。そのまま表示すると配列の内部表記になる)
${#temporals.format(updatedAt, 'yyyy/MM/dd HH:mm')} 2026/10/02 09:30
${#temporals.format(day, 'yyyy年M月d日(E)')} 2026年10月2日(金)
${#temporals.year(day)} / month / day 2026 / 10 / 2
${#dates.format(date, 'yyyy/MM/dd')} 2026/10/02(date は java.util.Date)
${#lists.size(items)} 3
${#lists.isEmpty(items)} false
${#lists.contains(names, 'ペン')} true
${#objects.nullSafe(user.nickname, '-')} -
${#aggregates.sum(prices)} 350(120・80・150 の BigDecimal)
${#ids.seq('opt')} を2回 opt1、opt2

日付は、LocalDate / LocalDateTime なら #temporals、java.util.Date なら #dates と、型で使い分けます。 #dates.format() に LocalDateTime を渡すと、次のエラーになりました。

java.lang.IllegalArgumentException: Unable to convert type java.time.LocalDateTime of 2026-10-02T09:30 to type of java.util.Date

fragmentで共通レイアウトを作る(ヘッダー・フッター)

部品を定義して読み込む

th:fragment で部品に名前を付け、別のテンプレートから th:replace="~{ファイル :: 部品名}" で読み込みます。 ファイル名は templates/ からの相対パス(拡張子なし)です。

<!-- templates/fragments/common.html -->
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<body>
  <header th:fragment="siteHeader">
    <a th:href="@{/}">備品管理</a>
    <nav>
      <a th:href="@{/items}">備品一覧</a>
      <a th:href="@{/members}">メンバー</a>
    </nav>
  </header>

  <footer th:fragment="siteFooter">
    <small>&copy; 2026 Sample App</small>
  </footer>
</body>
</html>
<!-- templates/items/list.html -->
<body>
  <div th:replace="~{fragments/common :: siteHeader}"></div>
  <main>…</main>
  <div th:replace="~{fragments/common :: siteFooter}"></div>
</body>

実行結果では、th:replace を書いた <div> が部品の <header>・<footer> に置き換わり、<div> 自体は残りませんでした。 部品の中の @{/items} なども href="/items" に変換されます。

部品名は header や footer のような HTML のタグ名と重ならない名前(siteHeader など)にします。 ~{...} の :: の後ろは「部品名」だけでなく「タグ名などのセレクター」としても解釈されるためです。実際に、同じファイルに th:fragment="header" の要素と別の <header> 要素(引数付き fragment の pageHeader)を置いて ~{fragments/common :: header} を読み込むと、次のように両方の <header> が出力されました(2つ目は引数が渡らないため、同名の別の変数が表示されています)。

<header>
    <a href="/">備品管理</a>
    …
  </header><header>
    <h1>Thymeleafの属性一覧と出力例</h1>
    …
  </header>

th:insert・th:replace・th:include の違い

属性 動作 実行結果(<div th:X="~{common :: siteFooter}"></div>) 3.1 での扱い
th:replace 書いた要素を部品で置き換える <footer>…</footer> 推奨
th:insert 書いた要素の中に部品を入れる <div><footer>…</footer></div> 推奨
th:include 書いた要素の中に部品の中身だけを入れる <div><small>…</small></div> 非推奨(実行すると WARN ログ)
th:substituteby th:replace の旧名 — 3.1 で削除

新しく書くときは th:replace か th:insert を使います。 また、3.1 からは th:replace="common :: siteFooter" のように ~{} を付けない書き方は非推奨です。どちらも出力自体は行われますが、3.1.5 では次の警告がログに出ました(テンプレート名は実行時のもの)。

WARN ... Deprecated attribute {th:include,data-th-include} found in template c26_include, line 3, col 6. Please use {th:insert,data-th-insert} instead, ...
WARN ... Deprecated unwrapped fragment expression "frag :: footer" found in template c26_include, line 4, col 6. Please use the complete syntax of fragment expressions instead ("~{frag :: footer}"). ...

部品の指定方法

書き方 意味
~{fragments/common :: siteHeader} 別ファイルの th:fragment="siteHeader"(同名のタグも対象になる)
~{fragments/common :: #site-footer} 別ファイルの id="site-footer" の要素(th:fragment なしでも可)
~{:: header} または ~{this :: header} 同じファイル内の部品
~{} 空の部品(何も出力しない)
${flag} ? ~{:: banner} : _ 条件付き。_(何もしない)なら元の要素がそのまま残る

引数付き fragment

th:fragment="名前(引数1, 引数2)" と書くと、関数のように値を渡せます。

<!-- templates/fragments/common.html -->
<header th:fragment="pageHeader(title, active)">
  <h1 th:text="${title}">タイトル</h1>
  <nav>
    <a th:href="@{/items}" th:classappend="${active == 'items'} ? 'active'">備品一覧</a>
    <a th:href="@{/members}" th:classappend="${active == 'members'} ? 'active'">メンバー</a>
  </nav>
</header>
<!-- 呼び出し側:順番で渡す -->
<div th:replace="~{fragments/common :: pageHeader('備品一覧', 'items')}"></div>

<!-- 名前で渡す(順番を気にしなくてよい) -->
<div th:replace="~{fragments/common :: pageHeader(active='members', title='メンバー')}"></div>

1つ目の実行結果は次のとおりです(インデントは整えています)。

<header>
  <h1>備品一覧</h1>
  <nav>
    <a href="/items" class="active">備品一覧</a>
    <a href="/members">メンバー</a>
  </nav>
</header>

ページごとに中身を差し替える共通レイアウト

ページの <title> や本文(<section>)を部品として共通レイアウトに渡すと、1つのレイアウトファイルで全ページの外枠を管理できます。 引数には文字だけでなく、~{::title} のようにマークアップそのものを渡せます。

<!-- templates/layout/base.html -->
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org"
      th:fragment="layout(title, content)">
<head>
  <meta charset="UTF-8">
  <title th:replace="${title}">共通タイトル</title>
  <link rel="stylesheet" th:href="@{/css/app.css}">
</head>
<body>
  <div th:replace="~{fragments/common :: siteHeader}"></div>
  <main th:replace="${content}">
    <p>ここに各ページの本文が入ります</p>
  </main>
  <div th:replace="~{fragments/common :: siteFooter}"></div>
</body>
</html>
<!-- templates/items/list.html -->
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org"
      th:replace="~{layout/base :: layout(~{::title}, ~{::section})}">
<head>
  <title>備品一覧</title>
</head>
<body>
  <section>
    <h2>備品一覧</h2>
    <ul>
      <li th:each="item : ${items}" th:text="${item.name}">備品</li>
    </ul>
  </section>
</body>
</html>

実行結果では、base.html の外枠(<meta charset>・CSS・共通ヘッダー・フッター)の中に、各ページの <title>備品一覧</title> と <section>…</section>(中の th:each も処理済み)が入りました。 各ページのファイルには、そのページ固有の部分だけを書けばよくなります。

継承のような書き方ができる Thymeleaf Layout Dialect(layout:decorate)という別ライブラリもありますが、まずは追加ライブラリ不要の「レイアウトに部品を渡す」方法で十分なことが多いです。

th:utextの注意(XSS対策)

th:utext と [(...)] は値をエスケープせずに出力するため、利用者が入力した文字を渡すと、その中の <script> などがそのまま動いてしまいます(XSS:クロスサイトスクリプティング)。

場面 書き方 判断
利用者の入力(名前・コメントなど) th:text / [[...]] エスケープされるので、そのまま使ってよい
管理者が登録した固定HTML th:utext 内容が信頼できる場合に限る
利用者が入力したHTML(記事本文など) サーバー側で許可するタグだけに絞ってから th:utext サニタイズ(無害化)ライブラリを通す
JavaScript 内に値を入れる th:inline="javascript" と [[...]] JSとして安全な形にエスケープされる
利用者が入力したURLをリンクにする th:href サーバー側で http / https だけに絞る(3.1.5 で javascript:alert(1) という値を th:href に渡すと、そのまま href に出力された)
<!-- 直す前:コメントにHTMLを書かれるとそのまま実行される -->
<p th:utext="${comment.body}">コメント</p>

<!-- 直した後:文字として表示される -->
<p th:text="${comment.body}">コメント</p>

Thymeleaf 3.1 では、th:on*(th:onclick など)・th:href・th:src・th:utext などで使える式に、安全のための制限が加わっています。 たとえば th:onclick="'show(' + ${item.name} + ')'" のように変数の文字を入れると、3.1.5 では次のエラーになりました(数値の ${item.id} なら onclick="show(2)" と出力されます)。

org.thymeleaf.exceptions.TemplateProcessingException: Only variable expressions returning numbers or booleans are allowed in this context, any other datatypes are not trusted in the context of this expression, including Strings or any other object that could be rendered as a text literal. A typical case is HTML attributes for event handlers (e.g. "onload"), in which textual data from variables should better be output to "data-*" attributes and then read from the event handler.

イベントに値を渡したいときは、エラー文のとおりth:data-id="${item.id}" のように data-* 属性に入れ、JavaScript から element.dataset.id で読み取る形にすると制限にかかりません。制限の詳細は公式ドキュメント(Tutorial: Using Thymeleaf 3.1 の Appendix D「Expression Restrictions」)で確認してください。

また 3.1 では、式の中から #request・#response・#session・#servletContext を直接参照できなくなりました。必要な値は Controller で Model に入れて渡します(${param.x} や ${session.x} の形は引き続き使えます)。

XSSの仕組みや、他のフレームワークでの書き方は、既存記事「XSS(クロスサイトスクリプティング)対策|フレームワーク別の注意点」で詳しく扱っています。

Spring BootのThymeleaf設定(application.properties)

spring.thymeleaf.* で置き場所やキャッシュを変更できます。(Spring Boot の公式リファレンスで確認。手元では未実行)

プロパティ 既定値 意味
spring.thymeleaf.cache true テンプレートのキャッシュ。開発中は false にすると修正がすぐ反映される
spring.thymeleaf.prefix classpath:/templates/ テンプレートの置き場所
spring.thymeleaf.suffix .html テンプレートの拡張子
spring.thymeleaf.mode HTML テンプレートモード(HTML、XML、TEXT、JAVASCRIPT、CSS など)
spring.thymeleaf.encoding UTF-8 テンプレートの文字コード
# application-dev.properties(開発用プロファイル)
spring.thymeleaf.cache=false

spring-boot-devtools を追加していれば、開発中は自動でキャッシュが無効になります。 本番は既定の true のままにします。プロパティ全般は既存記事「Spring Boot application.properties設定一覧」で扱っています。

よくあるエラーと直し方

症状・エラー文の例 主な原因 直し方
Error resolving template [items/list], template might not exist or might not be accessible ファイルの場所・名前が返り値と合っていない templates/items/list.html があるか、大文字小文字も含めて確認
Property or field 'nmae' cannot be found on object of type ... プロパティ名の誤り、または getter がない 名前と getter(record ならアクセサ)を確認
EL1007E: Property or field 'name' cannot be found on null 途中の値が null ${user?.name}(安全なナビゲーション)や th:if で null を避ける
Neither BindingResult nor plain target object for bean name 'memberForm' available th:object のオブジェクトを Model に入れていない GET のメソッドで model.addAttribute("memberForm", new MemberForm())
th:onclick に変数を入れるとエラー 3.1 の式の制限 data-* 属性に入れて JavaScript で読む

表の英語のエラー文は Spring Boot(SpEL)の場合の例で、手元の単体実行(OGNL)では未確認です。単体実行では、存在しないプロパティは ognl.NoSuchPropertyException: Harness$User.nmae、null に対するメソッド呼び出しは java.lang.NullPointerException: target is null for method length という原因で表示されました。${a?.b} の ?. は Spring(SpEL)の安全なナビゲーション演算子で、a が null のときはエラーにせず null を返します。表示に既定値を出すなら ${user?.name} ?: '-' と組み合わせます。

よくある質問

Q. th:each で何番目かを表示するにはどうしますか? A. ステータス変数の count(1始まり)か index(0始まり)を使います。 th:each="item, stat : ${items}" と書くか、省略して itemStat.count と書きます。

Q. Thymeleaf の最新バージョンはいくつですか? A. 2026年10月2日時点の最新版は Thymeleaf 3.1.5.RELEASE(2026年4月21日リリース)です。Spring Boot を使う場合は、Spring Boot が管理するバージョンが自動で使われるため、個別に指定する必要はありません。

まとめ

  • Thymeleaf は HTML に th: 属性を足すだけで、データを埋め込んだHTMLを作れるテンプレートエンジンです。
  • 式は「データ ${}・フォーム内 *{}・文言 #{}・URL @{}・部品 ~{}」で使い分けます。
  • th:each のステータス変数は index・count・size・current・even・odd・first・last の8つで、odd/even は count(1始まり)基準です。
  • 空のリストは th:if で true になるため、件数の判定は #lists.isEmpty() を使います。
  • 日付は LocalDateTime なら #temporals、Date なら #dates で整形します。
  • 共通レイアウトは th:fragment と th:replace="~{...}" で作り、引数付き fragment でページごとの違いを渡せます。
  • 利用者の入力は th:text で表示し、th:utext は信頼できるHTMLだけに使います。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Thymeleafの使い方|th:each・th:ifなど属性一覧と出力例」

株式会社RJCでは、Thymeleaf をはじめとするWeb開発の基本を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?