Spring Boot and Tailwind CSS

Structuring Tailwind CSS with Spring Boot, gradle, and (optionally) thymeleaf templates

See the simple hosted example here


View the code

Gradle Structure

Structured as a flat multi-module gradle project. Java is the spring boot application, web builds tailwind with gulp and copies the thymeleaf templates (for live-reloading during development)

    - /docker
        - build.gradle
    - /java
        - build.gradle
    - /web
        - build.gradle
- settings.gradle

Java gradle build

Goal: Copy the resources and start the webapp

In addition to the template generated from https://start.spring.io/, our java build.gradle module should have a dependency on the web module so a ./gradlew bootRun will completely build and start our application without running any additional commands.

We accomplish this by first creating a tailwind configuration

configurations {

then declare a dependency on the web module

// there's a hack here that i'll explain later so we don't have to
// declare the configuration
dependencies {
    tailwind project(':web')

finally create a task to copy the css files from the web configuration to our spring boot resource output directory

task copyCss(type: Copy) {
    from configurations.tailwind
    into "$sourceSets.main.output.resourcesDir/static"

// depend on assemble
// https://docs.gradle.org/current/userguide/img/javaPluginTasks.png
assemble.dependsOn copyCss

Web gradle build

Goal: Watch-recompile tailwind/thymeleaf cycle

We use the node gradle plugin but it's not strictly necessary if you want to exec commands directly. Most of the code here is related to the gulp watch task and passing paths around. If you want you can just hardcode all the paths without too much concern (not publishing). The only magic we add is to declare our compiled tailwind styles as an artifact so we can depend on it in our java project.

// declare the default configuration
configurations {
    create 'default'

// depend on assemble so styles are compiled when we do a `build`
task assemble {
    dependsOn 'build'

artifacts {
// use 'default' configuration so we don't have to specify in :java
'default' file("$buildDir/styles")

Wrapping up

The only other logic is in the gulpfile which is fairly boilerplate besides passing some env variables from the gradle build to control the paths. We also set

cache: false

in the spring application.yaml so that spring picks up any changed thymeleaf templates instead of caching them in memory.

For the dev workflow we open one terminal to run ./gradlew bootRun then in another we run our gulp watch task (directly in the web dir or through gradle) with ./gradlew :web:watch