Author Avatar Image
Alexander Reelsen

Backend developer, productivity fan, likes the JVM, full text search, distributed databases & systems

Create a custom Tailwind CSS build with Gradle in your Java project (2023 edition)
Jul 13, 2023
3 minutes read

TLDR; This post will show how to create a custom Tailwind CSS build in a Gradle based Java project. This post is an update to the post made in 2021, as the steps outlined in that one are completely unneeded.

As of late December 2021 Tailwind features a CLI install, which is a node.js distribution packaged together with the tailwind code. All you need to do is to install that binary and no need to fiddle with any node or npm setup.

However that standalone Tailwind CLI build is platform specific and needs to be treated like that. As I develop under osx, but the application Docker image is built and run under Linux I need to take care of those two platforms. In order to do that I added code like this to my build.gradle file.

project.ext.tailwind = [
  version: '3.3.2',
]
if (OperatingSystem.current().isMacOsX()) {
  project.ext.tailwind.binary = 'tailwindcss-macos-arm64'
  project.ext.tailwind.checksum = '58afb95d8d887d2c2bedb1a66880e98e'
}
if (OperatingSystem.current().isLinux()) {
  project.ext.tailwind.binary = 'tailwindcss-linux-x64'
  project.ext.tailwind.checksum = 'bedccc4c35b489909878a715020ab6a2'
}
project.ext.tailwind.exec = "${project.ext.tailwind.binary}-${project.ext.tailwind.version}"
project.ext.tailwind.path = ".tailwindcss-binaries/${project.ext.tailwind.exec}"

The idea is to ensure downloading the correct build per platform as well as checking SHA sums, as you’re downloading arbitrary content from the internet.

The next step is define a task to download the CLI distribution

plugins {
  id "de.undercouch.download" version "5.4.0"
}

task tailwindCliDownload(type: Download) {
  src "https://github.com/tailwindlabs/tailwindcss/releases/download/v${project.ext.tailwind.version}/${project.ext.tailwind.binary}"
  dest project.ext.tailwind.path
  overwrite false
}

The above Gradle task requires the fantastic Gradle download task plugin which only downloads the plugin if it does not exist yet locally. Next up is the requirement to verify the SHA sum (that task is from the same download plugin):

task tailwindCliVerify(type: Verify, dependsOn: tailwindCliDownload) {
    src new File(project.ext.tailwind.path)
    algorithm 'MD5'
    checksum project.ext.tailwind.checksum
}

The last setup step is to ensure that the downloaded file is executable:

task tailwindCliExecutable(dependsOn: tailwindCliVerify) {
  def perms = [
    java.nio.file.attribute.PosixFilePermission.OWNER_READ,
    java.nio.file.attribute.PosixFilePermission.OWNER_WRITE,
    java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE
  ] as java.util.Set
  if (java.nio.file.Files.exists(java.nio.file.Paths.get(project.ext.tailwind.path))) {
    java.nio.file.Files.setPosixFilePermissions(java.nio.file.Paths.get(project.ext.tailwind.path), perms)
  }
}

This task makes the binary executable. Having finished the setup, you can now set up a tailwind.config.js to make sure to create a small custom build:

module.exports = {
  content: [ './src/main/jte/**/*.{jte,html}' ],
  plugins: [
    require('@tailwindcss/forms'),
    require('@tailwindcss/typography'),
  ]
}

The ‘only’ missing piece is building the CSS when needed, as well as having a watch task running. Let’s add tasks for this as well:

task watchCss(type: Exec, dependsOn: tailwindCliExecutable) {
  commandLine project.ext.tailwind.path, '--watch=always', '-i', 'src/main/resources/css/build.css', '-o', 'src/main/resources/css/workshop.css'
}

task buildCss(type: Exec, dependsOn: tailwindCliExecutable) {
  commandLine project.ext.tailwind.path, '-i', 'src/main/resources/css/build.css', '-o', 'src/main/resources/css/workshop.css'
}
assemble.dependsOn(buildCss)

You probably will have a different strategy than building your CSS file in the src/main/resources by adding a proper source set, instead of putting workshop.css file into the .gitignore file in order to not push it. I kept this for simplicity in my sample project.

The only obstacle I had was adding --watch=always to the watchCss task, as without the always the tailwind binary returns immediately instead of keeping running. I found this not in the docs, but in this GitHub issue.

Finally, you may want to add the --minify argument to your build in order to produce minified CSS.

That’s it for today. Happy tailwinding!


Back to posts