Tech Blog

Why Thymeleaf + TypeScript + CSS Changes Don't Appear on Screen, and How to Fix It

by Tech Writer
Thymeleaf TypeScript CSS Spring Boot

Introduction

During development with Spring Boot + Thymeleaf + TypeScript + TailwindCSS, I had this experience:

  • Modified TypeScript code
  • Refreshed the browser
  • No change

Suspecting a “cache?”, I tried a hard refresh (Ctrl+Shift+R).
Still no change.

After puzzling over it for a while, I finally figured out the cause. I hadn’t built it.

This article might seem like “isn’t that obvious?” — but in the mixed Spring Boot + frontend assets setup, this is a surprisingly common point of confusion.


Structure Explanation

The structure of this project:

src/
  main/
    resources/
      templates/          ← Thymeleaf templates (.html)
      static/
        css/
          tailwind.css    ← TailwindCSS source
        js/
          app.ts          ← TypeScript source
        dist/
          tailwind.min.css ← Built CSS (this is served)
          app.js           ← Built JS (this is served)

What gets served to the browser is the built files under dist/.
Even if you modify source files (app.ts, tailwind.css) directly, dist/ won’t be updated unless you build.


Why It’s Easy to Mistake

Thymeleaf Templates Reflect Immediately

Thymeleaf .html files, with spring.thymeleaf.cache=false set, reflect the latest content on every browser refresh without building.

# application.yml (development)
spring:
  thymeleaf:
    cache: false

Getting used to this, you end up editing TypeScript and CSS with the same expectation.

The Save → Browser Refresh Routine Breaks Down

For Thymeleaf changes: save → browser refresh is sufficient.
For frontend assets: save → build → browser refresh is required. You forget this “build” step.


Build Commands

// package.json
{
  "scripts": {
    "build:css": "npx tailwindcss -i ./src/main/resources/static/css/tailwind.css -o ./src/main/resources/static/dist/tailwind.min.css --minify",
    "build:ts": "npx tsc",
    "build": "npm run build:css && npm run build:ts",
    "watch": "npm run build:css -- --watch & npx tsc --watch"
  }
}

With npm run watch in watch mode, automatic builds run when files are saved.


Watch Mode Setup for Development

# Terminal 1: Spring Boot
./mvnw spring-boot:run

# Terminal 2: Frontend assets watch build
npm run watch

Run two processes in parallel.
In VS Code, using a separate terminal tab or defining as a task is convenient.

Define as a VS Code Task

// .vscode/tasks.json
{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "watch-frontend",
      "type": "shell",
      "command": "npm run watch",
      "isBackground": true,
      "problemMatcher": []
    }
  ]
}

Checklist When Changes Don’t Appear

When changes still don’t appear, check in this order:

  1. Did you build?

    • Check if npm run build or npm run watch is running
    • Check for build errors (look at the terminal)
  2. Did you modify the correct file?

    • Did you modify the source file (app.ts), or accidentally modify the built file (app.js)?
  3. Is Thymeleaf cache disabled?

    • Check the spring.thymeleaf.cache=false setting
  4. Is browser cache remaining?

    • Hard refresh (Ctrl+Shift+R or Ctrl+F5)
    • Developer tools → Network tab → Check “Disable cache”
    • Add ?v=timestamp query parameter to HTML for cache busting
  5. Is Spring Boot cache disabled?

    • Set spring.web.resources.cache.period=0

Cache Busting in Production Builds

In production environments, browsers are often configured to long-cache CSS and JS.
Adding a hash to file names invalidates the cache.

<!-- Thymeleaf template -->
<!-- Simple version (change on each deployment) -->
<link rel="stylesheet" th:href="@{/dist/tailwind.min.css(v=${buildVersion})}">
<script th:src="@{/dist/app.js(v=${buildVersion})}"></script>
// Pass build version in controller
@ModelAttribute("buildVersion")
public String buildVersion() {
    return System.getenv("BUILD_VERSION"); // Set as environment variable in CI
}

Summary

The golden rule for mixed Thymeleaf + frontend assets setup:

  1. Frontend assets require building (unlike HTML)
  2. Use watch mode for automatic builds during development
  3. When changes don’t appear, check build → then hard refresh
  4. Use cache busting in production (version query or file name hash)

“Why does Thymeleaf file reflect immediately but not this?” — the answer is “because build-free files and build-required files are mixed together.”
Understanding this from the start can dramatically reduce wasted debugging time.

Feel free to send a message

Please send a message if you have any technical questions, feedback, or inquiries.