Text & Attribute Setting

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Thymeleaf 3.1.5.RELEASE (2026-04-21), Spring Boot 4.1.1 (Spring Framework 7.0.9) and Java 21, checked on 2026-10-08, and every example was compiled and run on that stack (a Spring MVC scratch app rendered through MockMvc, JDK 26 with --release 21). The Standard Dialect attributes described here are the same standalone, with Spring MVC and with Spring WebFlux; the only parts that need Spring are the request-aware behaviours called out in the text (the CSRF field that th:action adds, the #{...} message source and the locale). Add-on dialects (Layout Dialect 4.0.x, the data-attribute dialect) and the htmx libraries (htmx 2.0.x, htmx-spring-boot 5.x) are not required: htmx only appears here as an example of an attribute family that the default attribute processor sets.

Almost every Thymeleaf template is made of two operations: replace the body of an element with text and set, change or remove an attribute of an element. Both are done with th:* attributes that are processed when the template is rendered and removed from the output, so the rest of the markup stays valid HTML that a browser can open as a static prototype (a natural template). This page covers all of them: the text processors, th:attr, the specific attribute processors, the appending processors, the boolean attributes and the default attribute processor, and the one security rule that connects them: which positions are evaluated in restricted mode. The expressions placed inside the attribute values (${...}, #{...}, @{...} and the rest) are explained on the Standard Expression Syntax page; the URL expressions in th:href/th:src on the Link URLs page; [[...]] inlining (Inlining), text and form binding (th:field, Forms & Data Binding) on pages of their own.

The model of a processed attribute

An attribute processor is registered in a dialect under a prefix (here th) and a name (text, href, class…​). When the engine meets th:text="${book.title}", it evaluates the expression, applies the processor (replace the body of the element), and drops the th:text attribute from the output. The static content you wrote in the element is only the prototype for designers:

flowchart LR tpl["Template
p th:text=book.title: Prototype title"] --> parse["Parse
element + th attributes"] parse --> eval["Evaluate the expression
against the context"] eval --> proc["Attribute processor
text: set the body"] proc --> out["Output
p: Dune"] tpl -. "opened in a browser
without the engine" .-> proto["Prototype title"]

The processors in this page fall into five families, and picking the right one is most of the work:

Family Attributes What it does

Body text

th:text, th:utext

Replaces the element body with escaped (th:text) or unescaped (th:utext) text.

One attribute

th:attr, the specific th:* processors (th:href, th:value, th:id…​)

Sets one attribute; the processor is named after the attribute it sets.

Several at once

th:alt-title, th:lang-xmllang

Sets two attributes to the same value.

Add to an existing value

th:attrappend, th:attrprepend, th:classappend, th:styleappend

Suffixes or prefixes the existing attribute value instead of replacing it.

Conditional presence

th:checked, th:selected, th:disabled, th:required and the other fixed-value boolean attributes

Writes the attribute with its fixed value, or leaves it out.

th:text: replacing the body with escaped text

th:text evaluates its expression and replaces the body of the element with the result, after HTML-escaping it. The element itself and its other attributes are untouched. Because the escaping happens for you, th:text is the default choice for any value that could contain <, >, & or a quote, which means any data that came from a user or a database. The first example uses the Bookshelf’s messages.properties bundle (the #{...} expression is a message lookup, resolved with the request locale) and a value that deliberately contains markup:

books.title=Books
text.tagline=Find your next <b>favourite</b> book
text.greeting=Welcome, {0}!
@Controller
public class CardController {

    public record BookCard(Long id, String title, String blurb, boolean available, int copies, String level) { }

    @GetMapping("/text/texts")
    public String texts(Model model) {
        model.addAttribute("book", new BookCard(7L, "Dune",
                "A <em>desert</em> planet & a <script>alert('x')</script> prophecy", true, 3, "warning"));
        model.addAttribute("user", "Ada");
        return "text/texts";
    }
}
<h1 th:text="#{books.title}">Books</h1>
<p th:text="#{text.greeting(${user})}">Welcome, reader!</p>
<p th:text="${book.blurb}">Prototype blurb</p>
<p th:text="|${book.title} (${book.copies} copies)|">Prototype</p>
<p>Title: <span th:text="${book.title}">Prototype title</span></p>

The rendered HTML. The markup in the blurb is shown as text, not executed, which is the protection against cross-site scripting:

<h1>Books</h1>
<p>Welcome, Ada!</p>
<p>A &lt;em&gt;desert&lt;/em&gt; planet &amp; a &lt;script&gt;alert(&#39;x&#39;)&lt;/script&gt; prophecy</p>
<p>Dune (3 copies)</p>
<p>Title: <span>Dune</span></p>

A few points about the example:

  • th:text="#{text.greeting(${user})}" is a message expression with an argument; the message is looked up in the Spring MessageSource (a plain ResourceBundle standalone). The Internationalization page covers bundles, locales and message arguments in depth.

  • |...| is a literal substitution: it joins text and variables without + signs and quotes.

  • The text between the tags (Prototype title) is only what a browser shows when the file is opened without the engine. It is replaced by the processor, so changing it does not change the application.

  • The engine escapes <, >, &, " and ' (the single quote becomes &#39;), which is what makes the value safe both in element bodies and, with th:attr and the specific attribute processors, in quoted attribute values.

  • If the same text is meant to appear in the middle of other text, you can write [[${book.title}]] instead of a <span>; it is the inlined form of th:text and is covered on the Inlining page.

th:utext: unescaped text and the XSS decision

th:utext ("unescaped text") does the same replacement but writes the result without escaping it. Its legitimate use is a value that is meant to contain markup and that your own code controls, such as a message from a resource bundle written by the development team. In the Bookshelf the tagline message holds a <b> element:

<p th:utext="#{text.tagline}">Find your next favourite book</p>
<p th:text="#{text.tagline}">Find your next favourite book</p>
<p>Find your next <b>favourite</b> book</p>
<p>Find your next &lt;b&gt;favourite&lt;/b&gt; book</p>

The first line shows the bold text, the second the literal tags a visitor would see if the markup were escaped. Never use th:utext for data that originated outside your code (form input, request parameters, database rows filled by users, third-party API responses): the browser would run any script the value contains. When user-supplied rich text is a requirement, sanitise it with an HTML sanitiser on the way in (not in the template) and then store the cleaned result; escaping at output with th:text is the rule for everything else. Thymeleaf’s restrictions are only a defence in depth: Appendix D of the tutorial states that they are not a substitute for input validation and sanitising.

th:utext is evaluated in restricted expression mode (see the section on restricted mode below). A request parameter is forbidden in that position:

<p th:utext="${param.q}">x</p>
org.thymeleaf.exceptions.TemplateProcessingException: Instantiation of new objects and access to static classes or parameters is forbidden in this context (template: "text/r-utext" - line 4, col 6)

The same expression in th:text renders the escaped value (&lt;b&gt;x for ?q=<b>x). The restriction is on the expression, not on what the data is: it blocks ${param.*}, new expressions, static members (T(...) in SpEL) and a few utility objects, and it is not a validator of the data you put in the model.

th:attr: setting any attribute

th:attr takes a comma-separated list of name=expression assignments and sets each attribute on the element. Because XML rules forbid repeating an attribute on an element, the list is how several attributes are set from one processor:

<form action="subscribe.html" th:attr="action=@{/subscribe}" method="post">
  <input type="text" name="email">
  <input type="submit" value="Subscribe!" th:attr="value=#{text.subscribe}">
</form>
<img src="logo.png" th:attr="src=@{/images/logo.png},title=#{text.logo},alt=#{text.logo}">
<form action="/subscribe" method="post"><input type="hidden" name="_csrf" value="..."/>
  <input type="text" name="email">
  <input type="submit" value="Subscribe">
</form>
<img src="/images/logo.png" title="Bookshelf logo" alt="Bookshelf logo">

Two details deserve attention. First, the form gained a hidden _csrf field: that comes from Spring Security through the RequestDataValueProcessor integration described below, not from th:attr. Second, the tutorial itself says th:attr is scarcely used: it is ugly, and the specific processors in the next section do the same job with less markup. Use it for attributes that have no specific processor and that you do not want to write with the default attribute processor, or when you want to set several attributes in one place. When th:attr creates an attribute with an arbitrary name it is evaluated in restricted mode.

The specific attribute processors

For nearly every HTML attribute there is a th:<name> processor that sets exactly that attribute with the result of its expression. th:value="${book.title}" sets value, th:id sets id, th:href sets href (with the context path prepended for @{...} links) and so on. They are the everyday tools and the reason th:attr is seldom needed. The tutorial lists them all; the table groups the complete list by purpose (every name is written with the th: prefix: th:abbr, th:accept…​).

Purpose Attributes

URLs and resources

action, background, cite, codebase, data, formaction, href, icon, longdesc, manifest, poster, src, usemap

Form controls

accept, accept-charset, autocomplete, challenge, cols, enctype, for, form, formenctype, formmethod, formtarget, high, keytype, label, list, low, max, maxlength, method, min, name, optimum, pattern, placeholder, rows, size, step, type, value, valuetype, wrap

Tables and layout

abbr, align, axis, bgcolor, border, cellpadding, cellspacing, colspan, compact, frame, frameborder, headers, height, hspace, marginheight, marginwidth, rowspan, rules, scope, scrolling, span, summary, vspace, width

Identity, language and metadata

accesskey, alt, archive, charset, class, classid, codetype, content, contenteditable, contextmenu, datetime, dir, draggable, dropzone, hreflang, http-equiv, id, kind, lang, media, radiogroup, rel, rev, sandbox, scheme, sizes, spellcheck, srclang, standby, start, style, tabindex, target, title, xmlbase, xmllang, xmlspace

Media

audio, preload, code

Also in the tutorial’s list

fragment and inline: these names exist as processors with their own meaning (fragment definition and inlining mode), covered on the Fragments and Inlining pages of this section

The same list contains the whole family of event-handler attributes, th:on followed by the event name: onabort, onafterprint, onbeforeprint, onbeforeunload, onblur, oncanplay, oncanplaythrough, onchange, onclick, oncontextmenu, ondblclick, ondrag, ondragend, ondragenter, ondragleave, ondragover, ondragstart, ondrop, ondurationchange, onemptied, onended, onerror, onfocus, onformchange, onforminput, onhashchange, oninput, oninvalid, onkeydown, onkeypress, onkeyup, onload, onloadeddata, onloadedmetadata, onloadstart, onmessage, onmousedown, onmousemove, onmouseout, onmouseover, onmouseup, onmousewheel, onoffline, ononline, onpause, onplay, onplaying, onpopstate, onprogress, onratechange, onreadystatechange, onredo, onreset, onresize, onscroll, onseeked, onseeking, onselect, onshow, onstalled, onstorage, onsubmit, onsuspend, ontimeupdate, onundo, onunload, onvolumechange and onwaiting. They have a special restriction, explained below.

A realistic Bookshelf fragment uses several of them together. th:id builds a unique id with a string concatenation, th:value fills a form control, th:href builds a link with a path variable and th:title adds a tooltip:

<input id="book" th:id="'book-' + ${book.id}" th:value="${book.title}" value="Prototype" name="title">
<a href="book.html" th:href="@{/books/{id}(id=${book.id})}" th:title="${book.title}">Dune</a>
<input id="book-7" value="Dune" name="title">
<a href="/books/7" title="Dune">Dune</a>

Notes on how they behave:

  • The static attribute (id="book", value="Prototype") is the prototype value. The processor replaces it, and the th:* attribute is removed, so the output has one id.

  • An expression that evaluates to null removes the attribute. In the scratch app <p th:title="${null}" title="proto"> rendered as <p>. An empty string also removed th:title and th:alt (<img th:alt="''" alt="p"> rendered as <img>), while th:value="''" kept an empty value="", which is what a form control needs. If a rendered page is missing an attribute you expected, check for a null or empty value first.

  • th:class replaces the whole class attribute (<p th:class="${book.available} ? 'ok' : 'gone'" class="proto"> gave class="ok"); to add a class to an existing one use th:classappend (below).

  • th:action and the other form attributes take a link expression. In a Spring application a form with method="post" and a th:action receives an extra hidden _csrf input from Spring Security. Thymeleaf-Spring calls the RequestDataValueProcessor for th:action, th:value and related attributes, and Spring Security registers one that adds the CSRF token; the Spring integration is described in the Thymeleaf + Spring tutorial.

  • th:href and th:src are in the restricted-mode list for the base of the URL (not for the parameters); the Link URLs page of this section covers URLs.

Setting two attributes at once

Two special processors set a pair of attributes to the same value: th:alt-title sets alt and title, and th:lang-xmllang sets lang and xml:lang. They save repetition for the cases where the pair must stay in sync, as with an image description or an XHTML language declaration:

<img src="logo.png" th:src="@{/images/logo.png}" th:alt-title="#{text.logo}">
<html lang="en" th:lang-xmllang="es"></html>
<img src="/images/logo.png" alt="Bookshelf logo" title="Bookshelf logo">
<html lang="es" xml:lang="es"></html>

The first image is equivalent to th:src plus th:alt plus th:title with the same expression. HTML5 pages normally only need lang; xml:lang matters when the page is also served as XHTML.

Appending and prepending

th:attrappend and th:attrprepend add the result of their expression to the end or the start of the existing attribute value instead of replacing it. The value comes from a variable, so the template does not know the class in advance, and the space that separates two class names must be part of the added text:

<input type="button" value="Do it!" class="btn" th:attrappend="class=${' ' + cssStyle}">
<input type="button" value="Do it!" class="btn" th:attrprepend="class=${cssStyle + ' '}">
<input type="button" value="Do it!" class="btn warning">
<input type="button" value="Do it!" class="warning btn">

Because the pair is generic, the Standard Dialect adds two specialised versions for the two attributes that are most often built up, so you do not have to remember the separators: th:classappend adds a CSS class and th:styleappend adds a style fragment, both without overwriting what the markup already has. They are the way to switch a class on a condition. In the Bookshelf, an odd row of a table gets an extra class (s.odd comes from the iteration status variable, explained on the Iteration & Conditionals page):

<tr th:each="t, s : ${tags}" class="row" th:classappend="${s.odd} ? 'odd'"><td th:text="${t}">tag</td></tr>
<p style="color: red;" th:styleappend="${book.available} ? 'font-weight: bold'">Available</p>
<tr class="row odd"><td>sci-fi</td></tr>
<tr class="row"><td>classic</td></tr>
<p style="color: red; font-weight: bold">Available</p>

Note the inline condition ${cond} ? 'odd': with no : branch it evaluates to nothing when the condition is false, so no class is added (that is the "if-then" form of the conditional expression, which is covered on the Standard Expression Syntax page). On Thymeleaf 3.1.5 the two parts of a style are joined with a single space and no separator is added. If the static style does not end with a semicolon, the result is not valid CSS: <p style="color: red" th:styleappend="'font-weight: bold'"> rendered style="color: red font-weight: bold". Always finish the static style with ; or start the appended fragment with ;.

Fixed-value boolean attributes

HTML has boolean attributes: attributes with no meaningful value where the presence of the attribute means true (checked, disabled…​). XHTML writes the value as the attribute’s own name (checked="checked"). The Standard Dialect provides a processor for each of them, and each takes a condition: if it is true, the attribute is written with its fixed value; if it is false, the attribute is left out. The complete list, each with the th: prefix, is:

async, autofocus, autoplay, checked, controls, declare, default, defer, disabled, formnovalidate, hidden, ismap, loop, multiple, novalidate, nowrap, open, pubdate, readonly, required, reversed, scoped, seamless, selected.

The processors use the usual Thymeleaf truthiness rules (the full table is on the Iteration & Conditionals page of this section). These Bookshelf examples show the common conditions and what the engine does with unusual values:

<input type="checkbox" name="available" th:checked="${book.available}">
<input type="checkbox" name="gone" checked th:checked="${!book.available}">
<select><option value="a" th:selected="${book.copies > 2}">A</option></select>
<input type="text" name="isbn" th:required="${book.available}" th:disabled="${book.copies == 0}" th:readonly="true">
<details th:open="${book.available}"><summary>More</summary></details>
<input type="checkbox" th:checked="'on'">
<input type="checkbox" th:checked="'off'">
<input type="checkbox" th:checked="${book.copies}">
<input type="checkbox" th:checked="0">
<input type="checkbox" th:checked="null">
<input type="checkbox" th:checked="''">
<input type="checkbox" name="available" checked="checked">
<input type="checkbox" name="gone">
<select><option value="a" selected="selected">A</option></select>
<input type="text" name="isbn" readonly="readonly" required="required">
<details open="open"><summary>More</summary></details>
<input type="checkbox" checked="checked">
<input type="checkbox">
<input type="checkbox" checked="checked">
<input type="checkbox">
<input type="checkbox">
<input type="checkbox" checked="checked">

The static checked in the second line is the prototype: because the condition is false, the output has no checked at all. The last six lines show that 'on', a non-zero number and the empty string are true, while 'off', 0 and null are false. Do not set a boolean attribute from a string that holds "false" unless you know the engine reads it as false; pass a real boolean from the model. For a form bound to a model object, th:field (Thymeleaf + Spring) writes checked and selected for you, which is the Forms & Data Binding page.

The default attribute processor

Not every attribute has a processor. The default attribute processor fills that gap: any th: attribute whose name no dialect processor claims is turned into an attribute of the same name, set to the result of the expression. This is how data, ARIA and htmx attributes are written without th:attr. It is the processor that makes th:data-*, th:aria-* and th:hx-* work:

<span th:whatever="${book.title}">x</span>
<li th:data-isbn="${book.id}" th:data-title="${book.title}" th:aria-label="'Open ' + ${book.title}">Dune</li>
<button th:hx-get="@{/books/{id}(id=${book.id})}" hx-target="#detail" th:hx-vals="|{&quot;id&quot;: ${book.id}}|">Open</button>
<span whatever="Dune">x</span>
<li data-isbn="7" data-title="Dune" aria-label="Open Dune">Dune</li>
<button hx-get="/books/7" hx-target="#detail" hx-vals="{&quot;id&quot;: 7}">Open</button>

Three consequences:

  • Attributes that the markup does not know are still written. A misspelt name such as th:hrf produces a hrf attribute and no error, which is the typical reason for "the link has no URL" after a typo. A name with a processor always wins over the default.

  • htmx attributes that depend on data (hx-get with a path variable, hx-vals with JSON) are set with th:hx-*; the static ones (hx-target) need no th:. Prefer quoting JSON by building the value in Java and passing the text, so the template never concatenates user text into JSON.

  • The default attribute processor runs in restricted mode, the same as th:attr with an arbitrary name. th:data-q="${param.q}" failed with the same exception as the th:utext example, and th:attr="data-q=${param.q}" did too. Putting request-dependent values into the model in the controller (after validation) is the supported way.

HTML5-friendly names: data-th-* and th-block

The th: prefix is an XML namespace habit. A document that has to be valid strict HTML5 can use the data- form for every processor: data-{prefix}-{name}, which for the Standard Dialect gives data-th-*. The two forms are equivalent, and the namespaced one is not deprecated:

<ul><li data-th-each="t : ${tags}" data-th-text="${t}">tag</li></ul>
<button data-th-hx-get="@{/books}" data-th-text="${book.title}">Open</button>
<ul><li>sci-fi</li><li>classic</li></ul>
<button hx-get="/books">Dune</button>

The same section defines a syntax for custom elements, {prefix}-{name}, following the W3C Custom Elements convention. The only such element the Standard Dialect needs is the synthetic block that iterates or conditions without adding markup, written th:block or th-block:

<th-block th:each="t : ${tags}"><i th:text="${t}">t</i></th-block>
<th:block th:each="t : ${tags}"><b th:text="${t}">t</b></th:block>
<i>sci-fi</i><i>classic</i>
<b>sci-fi</b><b>classic</b>

The element is th-block, not a data-th-block attribute (an element name cannot be a data- attribute). Both th:block and th-block disappear from the output and keep their children; the Local Variables, Attribute Precedence, Comments & Blocks page covers it in full.

Restricted mode for th:utext and th:on* attributes

Appendix D of the tutorial defines a restricted expression evaluation mode that is applied automatically in specific positions. Of those, the ones that belong to this page are:

Position Why

th:utext and unescaped inlined expressions

The output is written without escaping.

All th:on* event-handler attributes whose value is a Thymeleaf Standard Expression

The value is JavaScript code. Variable expressions in them must evaluate to a number or a boolean.

th:attr that creates an attribute with an arbitrary name, and the default attribute processor

The name of the attribute is open-ended, so any value could end up in a sensitive attribute.

th:href, th:src and the base part of @{...} URLs

URL parameters in link expressions are not restricted; the base is.

In restricted mode these are forbidden: access to request parameters (${param.*}), instantiation with new, static classes and members (T(...) in SpEL, @identifier@ in OGNL), some context-level utility objects such as #execInfo, and, in Spring applications, @beanName bean references. Breaking the rules raises a TemplateProcessingException at render time. For th:on* there is an additional rule: a variable expression that produces anything other than a number or a boolean is rejected.

A number is accepted, so an id in a confirmation call works:

<button type="button" th:onclick="|confirmDelete(${book.id})|">Delete</button>
<button type="button" onclick="confirmDelete(7)">Delete</button>

A string is not. <button th:onclick="|confirm(${book.title})|"> threw:

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. (template: "text/r-onclick" - line 4, col 11)

The error message gives the solution: output the text to a data-* attribute (escaped by the normal attribute rules) and read it in the handler.

<button type="button" th:data-title="${book.title}" onclick="confirmTitle(this.dataset.title)">Title</button>
<button type="button" data-title="Dune" onclick="confirmTitle(this.dataset.title)">Title</button>

The more general advice is to avoid inline event handlers and attach listeners from a JavaScript file; if data must reach script from the template, see the Inlining page for JavaScript inlining and the safe way to pass server data. The restrictions are the same on Thymeleaf 3.1.5 (the minimum version this section teaches, since earlier 3.1 releases have published advisories on the expression sandbox) in a standalone engine and in Spring MVC or WebFlux.

A test for the rendered attributes

Since the processors only change markup, a rendering test is the cheapest regression net. With @SpringBootTest and MockMvc (the scratch app used the Boot 4 test starter spring-boot-starter-webmvc-test), assert on the HTML:

@SpringBootTest
@AutoConfigureMockMvc
@WithMockUser
class TextAttributeTest {

    @Autowired MockMvc mvc;

    @Test
    void textIsEscaped() throws Exception {
        String html = mvc.perform(get("/text/texts").header("Accept-Language", "en"))
                .andReturn().getResponse().getContentAsString();
        assertThat(html).contains("&lt;script&gt;").doesNotContain("<script>");
    }
}

AutoConfigureMockMvc lives in org.springframework.boot.webmvc.test.autoconfigure in Boot 4. If the bundle has a locale-specific file and the machine’s default locale is not the one you expect, set spring.messages.fallback-to-system-locale=false so tests render the base bundle.

Pitfalls

Symptom Cause Fix

Markup from the model shows as text

th:text escapes by design

Keep it; use th:utext only for trusted, code-controlled content

A <script> in user data runs

th:utext or [(...)] on untrusted data

Use th:text; sanitise rich text on input

Instantiation of new objects and access to static classes or parameters is forbidden

${param.*}, new, or static access in th:utext, th:on*, th:attr with an arbitrary name or the default processor

Compute and validate the value in the controller, put it in the model

Only variable expressions returning numbers or booleans are allowed

A string variable in a th:on* attribute

Write it to a data-* attribute and read it from the handler

An attribute disappears

The expression is null or an empty string (except th:value)

Check the value; provide a default with the Elvis operator

A th: attribute becomes a plain attribute with the typo

The default attribute processor accepts any name

Fix the spelling; compare the output with the table

class is replaced, not extended

th:class or th:attr="class=..." replaces

Use th:classappend

style="color: red font-weight: bold"

th:styleappend joins with a space, no semicolon

End the static style with ;

A checkbox stays checked

The condition evaluates true for 'on', a non-zero number or ''

Pass a real boolean, or negate the condition

The form has an unexpected _csrf input

Spring Security’s RequestDataValueProcessor with th:action and POST

Expected; keep CSRF protection on