Thymeleaf: Using External CSS and JavaScript Files

13 Mar 2021

Table of Contents


Introduction

Disclaimer: The examples shown here do not use Spring.

All of the code used below is available here on GitHub.

Thymeleaf supports inline expression processing for JavaScript and CSS. So, you can use code like this in your HTML page:

HTML
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Test</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <style th:inline="css">
            .myStyle {
                padding: 1em;
                display: inline-block;
                background-color: /*[(${backgroundColor})]*/ blueviolet;
            }
        </style>
    </head>

    <body>
        <div class="myStyle">
            <div>Hello world.</div>
        </div>

        <script th:inline="javascript">
            var dataList = /*[[${dataList}]]*/ [];
            processList( dataList );
        </script>

    </body>
</html>

In this example we have an inline CSS expression introduced by <style th:inline="css">:

CSS
1
background-color: /*[(${backgroundColor})]*/ blueviolet;

And we have a similar inline JavaScript expression, introduced by <script th:inline="javascript">:

JavaScript
1
var dataList = /*[[${dataList}]]*/ [];

In both cases, the Thymeleaf expression is surrounded by /*...*/ comment delimiters - and followed by a valid CSS and JavaScript value. This means that the CSS and JavaScript fragments are valid, despite the inclusion of (otherwise invalid) Thymeleaf expressions.

The fragments use default values, blueviolet and [], which are ignored when these lines are processed by Thymeleaf - but which are used normally outside of Thymeleaf processing. This is referred to as natural templating.

It is also worth noting that the ${dataList} expression in the JavaScript example can be a plain Java List - and Thymeleaf will automatically translate this Java list into an equivalent JavaScript list. The same is also true for other Java objects such as maps and arrays.

That is all very powerful and flexible.

But this assumes you have placed everything you need (HTML, CSS and JS) directly in your HTML tempalte file - and that every Thymeleaf expression is therefore in that HTML file.

This certainly works - but as you build more Thymeleaf templates, you may find yourself creating increasingly fragmented JavaScript and CSS snippets, placed throughout your templates

Using External CSS and JS Files

What if you want to use external CSS and JS files - and put your Thymeleaf expressions in those files, instead of in your HTML template?

What if your HTML <head> looks more like this:

HTML
1
2
3
4
5
6
7
<head>
    <title>Test</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="/js/my_script.js"></script>
    <link rel="stylesheet" type="text/css" href="/css/main.css"/>
</head>

How can you render your HTML template and, at the same time, also render one or more of these JavaScript and CSS external files?

Not Using Spring

There are plenty of tutorials and examples which show how to approach this task using Spring. One excellent (but maybe a bit dated) example is here:

Thymeleaf Template Modes Example

That’s all I’ll say about Spring, here. Instead I will focus on a Thymeleaf-without-Spring approach.

The Templates

For this walkthrough I will use Javalin as the web application framework. There are a couple of specific points which are relevant to Javalin, since it has built-in support for Thymeleaf and other templating solutions. But, more generally, this is a container-agnostic approach (Javalin uses embedded Jetty as its web container).

We start with a simple Thymeleaf HTML template:

The test.html file:

HTML
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Test</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <script src="/js/my_script.js"></script>
        <link rel="stylesheet" type="text/css" href="/css/main.css"/>
    </head>

    <body>
        <div class="myStyle">
            <div th:text="${hello}"></div>
            <br>
            <button type="button" onclick="getValue()">Show JS value</button>
            <br><br>
            <div id="test123"></div>
        </div>
    </body>
</html>

The my_script.js file:

JavaScript
1
2
3
4
function getValue() {
  var val = /*[[${jsTest}]]*/ '[no value]';
  document.getElementById('test123').innerHTML = val;
}

The main.css file:

CSS
1
2
3
4
5
6
7
8
.myStyle {
    font-family: Arial, sans-serif;
    padding: 1em;
    margin: 1em;
    display: inline-block;
    color: white;
    background-color: /*[[${backgroundColor}]]*/ pink;
}

The Java Route Handler

My Javalin set-up involves creating a public folder from which my static content is served:

Java
1
2
3
Javalin app = Javalin.create(config -> {
    config.addStaticFiles("/public");
}).start(7001);

This includes sub-folders for CSS and JS files.

In Javalin, you define route handlers as follows:

Java
1
2
3
app.routes(() -> {
    get("/test", TEST);
});

Here, TEST represents a functional interface (Handler), where we can place our handler logic:

Java
1
2
3
4
5
private static final Handler TEST = (ctx) -> {
    Map<String, Object> model = new HashMap<>();
    model.put("hello", "Hello, World.");
    ctx.render("test.html", model);
};

From the Javalin documentation:

Javalin looks for template files in src/resources, and uses the correct rendering engine based on the extension of the provided template.

I therefore create the resources folder and place my test.html there.

This means, apart from including the Thymeleaf library (in my pom.xml, in my case), there is nothing more I need to do to configure Thymeleaf in Javalin.

Thymeleaf HTML (no JS or CSS)

Using the above approach, when a user browses to

http://localhost:7001/test

there is not yet any Thymeleaf handling provided for our JS and CSS files - therefore, they are processed in the normal way by the browser. This is where the default values we provided in our JS and CSS files will be used - and the end result will be:

Custom Thymeleaf Configuration

In order to handle JS and CSS files, I will depart from Javalin’s out-of-the-box support for Thymeleaf (and other rendering libraries). Instead, I will provide a set of custom Thymeleaf template resolvers, and then integrate those into my Javalin route handlers.

The basic approach is this:

For page requests, we will continue to use Javalin’s standard approach for HTML rendering:

Java
1
ctx.render("my_template.html", my_model_data);

But for JavaScript and CSS, we will use Thymeleaf to render each of these files to a Java InputStream and then provide that stream as a response to the browser as part of each page request. Javalin’s role will be to intercept these requests, using standard routes, and pass the requests to Thymeleaf for processing.

We will use different Thymeleaf template modes for this:

  • TemplateMode.HTML
  • TemplateMode.JAVASCRIPT
  • TemplateMode.CSS

One more change we will make is to move our Thymeleaf templates to a new location. Instead of placing every HTML tempalte file in src/resources, we will create a new subfolder src/resources/thymeleaf, to keep our template files clearly separated from other (public) resources. We can also create additional subfolders as needed.

The Javalin-Thymeleaf Hook

Javalin can be given a custom Thymeleaf rendering engine:

Java
1
2
3
4
5
6
Javalin app = Javalin.create(config -> {
    config.addStaticFiles("/public");
    // two new lines for Thymeleaf custom configuration:
    JavalinRenderer.register(JavalinThymeleaf.INSTANCE);
    JavalinThymeleaf.configure(ThymeleafConfig.templateEngine());
}).start(7001);

The Configuration

The ThymeleafConfig class mentioned in the above code snippet is our custom class in which we provide all the necessary Thymeleaf configuration we will need.

Here is that class in full:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package org.northcoder.javalinthymeleaf;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
import org.thymeleaf.templateresolver.ITemplateResolver;

public class ThymeleafConfig {

    public static TemplateEngine templateEngine() {
        TemplateEngine templateEngine = new TemplateEngine();
        templateEngine.addDialect(new Java8TimeDialect());
        templateEngine.addTemplateResolver(htmlTemplateResolver());
        return templateEngine;
    }

    private static TemplateEngine jsTemplateEngine() {
        TemplateEngine templateEngine = new TemplateEngine();
        templateEngine.addTemplateResolver(jsTemplateResolver());
        return templateEngine;
    }

    private static TemplateEngine cssTemplateEngine() {
        TemplateEngine templateEngine = new TemplateEngine();
        templateEngine.addTemplateResolver(cssTemplateResolver());
        return templateEngine;
    }

    private static ITemplateResolver htmlTemplateResolver() {
        ClassLoaderTemplateResolver templateResolver
                = new ClassLoaderTemplateResolver(Thread
                        .currentThread().getContextClassLoader());
        templateResolver.setTemplateMode(TemplateMode.HTML);
        templateResolver.setPrefix("/thymeleaf/");
        templateResolver.setSuffix(".html");
        templateResolver.setCharacterEncoding("UTF-8");
        return templateResolver;
    }

    private static ITemplateResolver jsTemplateResolver() {
        ClassLoaderTemplateResolver templateResolver
                = new ClassLoaderTemplateResolver(Thread
                        .currentThread().getContextClassLoader());
        templateResolver.setTemplateMode(TemplateMode.JAVASCRIPT);
        templateResolver.setPrefix("/public/js/");
        templateResolver.setSuffix(".js");
        templateResolver.setCharacterEncoding("UTF-8");
        return templateResolver;
    }

    private static ITemplateResolver cssTemplateResolver() {
        ClassLoaderTemplateResolver templateResolver
                = new ClassLoaderTemplateResolver(Thread
                        .currentThread().getContextClassLoader());
        templateResolver.setTemplateMode(TemplateMode.CSS);
        templateResolver.setPrefix("/public/css/");
        templateResolver.setSuffix(".css");
        templateResolver.setCharacterEncoding("UTF-8");
        return templateResolver;
    }

    public static InputStream getJsTemplate(String templateName, Context ctx) {
        return new ByteArrayInputStream(jsTemplateEngine()
                .process(templateName, ctx)
                .getBytes(StandardCharsets.UTF_8));
    }

    public static InputStream getCssTemplate(String templateName, Context ctx) {
        return new ByteArrayInputStream(cssTemplateEngine()
                .process(templateName, ctx)
                .getBytes(StandardCharsets.UTF_8));
    }

}

This class could use some refactoring - but for demonstration purposes (simplicity and clarity) I have left the class in this basic format.

Because I am using Javalin, all my Thymeleaf template resolvers use ClassLoaderTemplateResolver - but for different types of web application (for example, a traditional Tomcat webapp), different resolvers could be used - most notably, the ServletContextTemplateResolver.

My class has three template resolvers, one each for HTML, CSS, and JS files.

The CSS and JS resolvers also have methods which return their results as input streams.

Finally: Handling the CSS and JS Routes

To use these custom Thymeleaf resolvers, we add two new routes to our Javalin application:

Java
1
2
3
4
5
app.routes(() -> {
    get("/test", TEST);
    get("/js/:jsFile", JS);     // handles all JS requests
    get("/css/:cssFile", CSS);  // handles all CSS requests
});

These routes take advantag of the fact that all our JS and CSS resources are public - and are accessed by URLs from within the HTML file:

HTML
1
2
<script src="/js/my_script.js"></script>
<link rel="stylesheet" type="text/css" href="/css/main.css"/>

So now we are intercepting those requests.

The route handlers (including the HTML handler) need to use our custom Thymeleaf renderers:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
private static final Handler TEST = (ctx) -> {
    Map<String, Object> model = new HashMap<>();
    model.put("hello", "Hello, World.");
    ctx.render("test.html", model);
};

private static final Handler JS = (ctx) -> {
    String jsFileName = ctx.pathParam("jsFile");
    Context thymeleafCtx = new Context();
    thymeleafCtx.setVariable("jsTest", "This string is from a JS file");
    ctx.contentType("application/javascript");
    ctx.result(ThymeleafConfig.getJsTemplate(jsFileName, thymeleafCtx));
};

private static final Handler CSS = (ctx) -> {
    String cssFileName = ctx.pathParam("cssFile");
    Context thymeleafCtx = new Context();
    thymeleafCtx.setVariable("backgroundColor", "goldenrod");
    ctx.contentType("text/css");
    ctx.result(ThymeleafConfig.getCssTemplate(cssFileName, thymeleafCtx));
};

The TEST route will use the Thymeleaf HTML renderer automatically, without us needing to make any explicit changes. This is because ctx.render() simply delegates to a Javalin HTML handler.

But for the CSS and JS routes, we do not use that. Instead we use ctx.result() which simply returns the result (in our case, as an input stream) to the browser.

We do have to remember to set the result content types, as well. This ensures the correct MIME types are used by the browser when handling each response.

So, the processing flow is:

  1. A browser requests the resource at http://localhost:7001/test.
  2. That triggers the TEST route handler.
  3. The related HTML file contains its own requests for one CSS and one JS file.
  4. These requests are caught by the new CSS and JS handlers.

The end result is:

And now we see that the Thymeleaf expressions in our external CSS and JS files have been applied in the rendered HTML.

All of the code used above is available here on GitHub - with some refactoring applied - so some of the details will be different from the code shown in this post.

Error Handling

One significant downside to this approach is that the application has explicitly taken over the handling of your custom static JavaScript and CSS resources. This means you are also responsible for related error handling - for example:

  • missing files
  • Thymeleaf expression syntax errors

You want the browser to know when a “404” missing file has been thrown, and when a “500” internal server error has occurred.

How you handle this is somewhat dependent on your web framework and the container you are using.

The GitHub version of my code does include some basic error handling for 404 and 500 errors relating to these resources.

Postscript: Thymeleaf Escaping

In our JS and CSS Thymeleaf expressions, we used the double square bracket syntax:

[[...]]

This ensures that all values generated by Thymeleaf expressions are escaped, which is essential when handling user-provided (and therefore untrusted) input data - especially for use in HTML pages.

However, there can be some situations where this causes a problem.

One such situation is whentrying to render a hexadecimal color code into a CSS file:

In the CSS file:

CSS
1
background-color: /*[[${backgroundColor}]]*/ pink;

In our Java code to populate the model:

CSS
1
model.put("backgroundColor", "#bb9534");

Here we are using #bb9534 instead of the word goldenrod.

This will be rendered as follows by Thymeleaf:

CSS
1
background-color: "\#bb9534";

That extra backslash in front of the # is Thymeleaf’s escaping process in action - but here it causes the CSS to be invalid.

To solve this, you can use Thymeleaf’s unescaped syntax:

[(...)]

So, in the CSS file, the Thymeleaf expression becomes:

background-color: /*[(${backgroundColor})]*/ pink;

Use this with extreme caution. Make sure you only ever place expressions inside [( )] which are provided directly by your application and never by user-provided input, or by data from any external source.

Better yet, just use [[ ]] with color names, or with one of the other syntaxes for specifying a color in a CSS file.