Why Thymeleaf + TypeScript + CSS Changes Don't Appear on Screen, and How to Fix It
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:
-
Did you build?
- Check if
npm run buildornpm run watchis running - Check for build errors (look at the terminal)
- Check if
-
Did you modify the correct file?
- Did you modify the source file (
app.ts), or accidentally modify the built file (app.js)?
- Did you modify the source file (
-
Is Thymeleaf cache disabled?
- Check the
spring.thymeleaf.cache=falsesetting
- Check the
-
Is browser cache remaining?
- Hard refresh (Ctrl+Shift+R or Ctrl+F5)
- Developer tools → Network tab → Check “Disable cache”
- Add
?v=timestampquery parameter to HTML for cache busting
-
Is Spring Boot cache disabled?
- Set
spring.web.resources.cache.period=0
- Set
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:
- Frontend assets require building (unlike HTML)
- Use watch mode for automatic builds during development
- When changes don’t appear, check build → then hard refresh
- 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.