Alexander Reelsen

Backend developer, productivity fan, likes distributed systems & the new serverless era

Create a custom Tailwind CSS build with Gradle in your Java project
Feb 15, 2021
12 minutes read

TLDR; This post will show how to create a custom Tailwind CSS build in a Gradle based Java project. This example uses Javalin, but as the final result will be a CSS file that does not really matter. For the Tailwind build we will be using Snowpack to create the final CSS artifact.

Note: I am a backend developer by trade, so if you find anything horrible wrong in here, feel free to ping me - I am more than happy to correct.


Bridging the gap between node driven front-end development and Java driven backend development is surprisingly hard, at least for me. This blog post is merely a reminder to myself how to set something like that up and stay sane while doing so. The ever changing technology landscape in the Javascript world makes it rather hard to keep up at times for folks outside of that ecosystem.

Let’s untangle the technology zoo involved today

  • Gradle as our build system
  • A super small Javalin Web Server
  • The node gradle plugin to bridge the Gradle and npm world
  • Snowpack to build the frontend classes
  • postcss (called via Snowpack) to find needed CSS classes and post-process those
  • Tailwind CSS as our CSS toolkit, which seems to be the preferred way of building halfway-ok looking prototypes even if you don’t have any measurable UI skills like I do
  • Last but not least, the Tailwind configuration uses purge css, a tool to remove unneeded CSS

That’s quite the list of things to keep in mind, only to end up with a small CSS file, that is optimized for your use-case.

We will start with nothing, create the Gradle and Java project, and once that is up and running, we will start building out the customized CSS in this blog post.

If you want to skip reading my descriptions and dive into the code, please go ahead and check out the GitHub sample repository I created.

Creating a new Gradle project

gradle init --type basic --dsl groovy \
  --project-name javalin-custom-tailwindcss-example

Let’s create a custom build.gradle file

plugins {
  id 'java'
  id 'application'

repositories {

group = 'de.spinscale.javalin'
version = '0.1.0-SNAPSHOT'

dependencies {
  compile 'io.javalin:javalin:3.13.0'
  // logging
  compile 'org.slf4j:slf4j-simple:1.8.0-beta4'

  // testing intentionally left blank, not part of this example

Next, add new directories

mkdir -p src/main/{java,resources/html}

Adding a Javalin endpoint

Before we can add any HTML, let’s add a server in src/main/java/

import io.javalin.Javalin;
import io.javalin.http.staticfiles.Location;

public class Server {

  public static void main(String[] args) {
    Javalin app = Javalin.create(config -> {
        "build/resources/main/html", Location.EXTERNAL);

Let’s add an src/main/resources/html/index.html that uses the default Tailwind CSS build distributed via common CDNs. I pretty much used the login sample

<!doctype html>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <link rel="stylesheet" href="" integrity="sha512-wl80ucxCRpLkfaCnbM88y4AxnutbGk327762eM9E/rRTvY/ZGAHWMZrYUq66VQBYMIYDFpDdJAOGSLyIPHZ2IQ==" crossorigin="anonymous" />

<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
    <div class="max-w-md w-full space-y-8">
            <h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
                Sign in
        <form class="mt-8 space-y-6" action="#" method="POST">
            <input type="hidden" name="remember" value="true">
            <div class="rounded-md shadow-sm -space-y-px">
                    <label for="email-address" class="sr-only">Email address</label>
                    <input id="email-address" name="email" type="email" autocomplete="email" required class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" placeholder="Email address">
                    <label for="password" class="sr-only">Password</label>
                    <input id="password" name="password" type="password" autocomplete="current-password" required class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" placeholder="Password">

                <button type="submit" class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
          <span class="absolute left-0 inset-y-0 flex items-center pl-3">
            <!-- Heroicon name: solid/lock-closed -->
            <svg class="h-5 w-5 text-indigo-500 group-hover:text-indigo-400" xmlns="" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
              <path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd" />
                    Sign in

Let’s test this by running ./gradlew run - which requires one last bit of configuration in the build.gradle file, by appending

application {
  mainClassName = 'Server'

When you open http://localhost:7000 you will see that the tailwind.min.css file is a whopping 2.7 megabytes in size, despite us using only a small part of this.

Let’s create a custom build!

Adding the css sub project

First, let’s add a sub project, that is doing all this fancy CSS creation to keep the main Java project clean. Change the settings.gradle file to the following to refer to a sub project = 'javalin-custom-tailwindcss-example'

include 'css'

Now run mkdir css and create a css/build.gradle file

plugins {
  id "com.github.node-gradle.node" version "3.0.1"

node {
  // current LTS release
  version = "14.15.5"
  download = true

// build css and put it into resources directory
task build(type: NpxTask) {
  dependsOn npmInstall
  command = './node_modules/.bin/snowpack'
  args = ['build']
  onlyIf {
    File destFile   = file("$projectDir/build/src/project.css")
    File sourceFile = file("$projectDir/src/project.css")

    if (!destFile.exists()) {
      return true

    return sourceFile.lastModified() > destFile.lastModified()

The build task tries to be a little bit smart and only builds the CSS file, if there have been changes. This does not work for all cases, but was usually good enough for me. It might make sense to have buildForce task, that does not contain the onlyIf part so you can always build your CSS file.

Now comes the fun part - wiring all the Javascript magic together (magic for me, not for any average web developer, who knows what happens there).

Setting up the Tailwind build using Snowpack

The Gradle sub project configures the Gradle node plugin, which will take care of downloading nodejs, and adds a npx task running Snowpack to build our CSS file. Before being able to run this, we need to configure a couple of more files for nodejs. First and foremost, the css/package.json file defining dependencies.

  "name": "css",
  "description": "tooling to create a custom Tailwind build",
  "scripts": {
    "build": "./node_modules/.bin/snowpack build"
  "devDependencies": {
    "@snowpack/plugin-postcss": "1.1.0",
    "@tailwindcss/typography": "0.4.0",
    "autoprefixer": "10.2.4",
    "cssnano": "4.1.10",
    "postcss": "8.2.6",
    "postcss-cli": "8.3.1",
    "postcss-fail-on-warn": "0.1.0",
    "postcss-import": "14.0.0",
    "snowpack": "3.0.11",
    "tailwindcss": "2.0.3"

If you look in the css/build.gradle file, the build depends on the npmInstall task, so we are safe, that those dependencies will be installed. The next step is the configuration of Snowpack. In case you never heard of it like me, it’s tagline is Snowpack is a modern, lightweight tool chain for web application development. - and seems to be faster than other tools. It also allows for dev environments with auto reload in the browser, however I did not choose that option, but just wanted my CSS file to be created. Let’s create a css/snowpack.config.js file

module.exports = {
  plugins: [
  mount: {
    "src": "/src"

Snowpack uses the postcss plugin, which is doing all the hard CSS work and will use the src directory as its input.

What is postcss you may ask. What an excellent question! postcss is a tool for transforming CSS with JavaScript. Let’s add a css/postcss.config.js file

module.exports = {
  plugins: [

So, let’s take a closer look at this configuration and what this does. First, the tailwindcss plugin is the part that will create the custom build. The postcss-fail-on-warn plugin will do what its name says to prevent leniency. In order to support @import rules in CSS file, we need to use the postcss-import plugin. We will use this feature in the main CSS file later. The autoprefixer plugin creates vendor specific CSS prefixes as far as I have understood - something I never care for when just writing prototypes, but is handy when going to production. Finally cssnano minifies your CSS. You might want to remove that one for testing.

We are not done yet however! The final configuration file that needs to be created is for the custom Tailwind built. It’s aptly named css/tailwind.config.js

module.exports = {
  purge: [
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  variants: {
    extend: {},
  plugins: [

Now this configuration is important, as this contains one of the few custom configurations. You can see the purge parameter list in the configuration file, that refers to the HTML directory of the root project from Javalin and would need to be changed if you switch to a different templating engine or path. This array resembles the list of directories and files to process and search for CSS classes being used, that are collected to create the minimal possible CSS build.

Also, if you are creating classes somehow programmatically, so that information cannot be extracted from the templates, you can specify those in a file in the css/src/ directory and thus end up with those classes as well.

On top of that, Tailwind has a modular system as well. In the above example I am using the tailwind typography plugin. If you ever want to render markdown, you can use prose classes from that typography plugin to get sane default markdown styles.

The moment you use Tailwind plugins and you stick to the default built, you would need to add more external style sheets to your project. Another advantage of this building approach.

Don’t fret, we’re almost there now!

The last step is to write a custom CSS file with your own custom classes in as css/src/project.css - don’t forget to run mkdir css/src first.

@tailwind base;
@tailwind components;
@tailwind utilities;

.my-title {
  @apply mt-6 text-center text-3xl font-extrabold text-gray-900

This way you can now use my-title as a class for the <h2> being used in our HTML, making your own HTML much more readable.

OK, let’s give this a spin now and see if we get a fancy CSS file as a result!

Run this in the main directory of your application

./gradlew :css:build

Running this should show output similar to this

> Task :css:npmInstall

> esbuild@0.8.46 postinstall /private/tmp/x/css/node_modules/esbuild
> node install.js

npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN css@ No repository field.
npm WARN css@ No license field.

added 365 packages from 199 contributors and audited 365 packages in 7.13s

49 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

> Task :css:build
[snowpack] ! building source files...
[snowpack] ✔ build complete [4.99s]
[snowpack] ! verifying build...
[snowpack] ✔ verification complete [0.00s]
[snowpack] ! writing build to disk...
[snowpack] watching for changes...

3 actionable tasks: 3 executed

You should end up with a file in css/build/src/project.css - and that should be rather small, in my case it was 7.6kb (uncompressed). Which is a nice reduction from 2.7 megabytes.

Allow Javalin app to use the CSS file

Finally, let’s do two changes to our src/main/resources/html/index.html file. First, let’s use the my-title class.

<h2 class="my-title">
    Sign in

Second, we still need to put the CSS class into a proper directory, so it gets loaded by Javalin. Running

find . -name "project.css"

You will see, that the CSS file is only in the css/ sub directory, but not in the resources directory of the root project. There are several (probably cleaner) ways to fix this, I went with the following in the build.gradle file

task copyCssFile(type: Copy) {
  dependsOn ':css:build'
  from 'css/build/src/project.css'
  into 'build/resources/css'
compileJava.dependsOn 'copyCssFile'

I know that compileJava is not the best dependency stage here, one could also put this into the assemble part, but this ensures everything is there when ./gradlew run is called.

After running ./gradlew copyCssFile you should end up with the file in build/resources/css/project.css. Again, let’s go simple here for the sake of example, usually you would make this more configurable. Now src/main/java/ looks like this

import io.javalin.Javalin;
import io.javalin.http.staticfiles.Location;

public class Server {

  public static void main(String[] args) {
    Javalin app = Javalin.create(config -> {
      config.addStaticFiles("/", "build/resources/main/html",

      config.addStaticFiles("/css", "build/resources/css",

The final bit would be to replace the style sheet link in src/main/resources/html/index.html:

  <link rel="stylesheet" href="/css/project.css" />

Rerunning ./gradlew run will now deliver a nice and tiny customized build of Tailwind CSS to your customers!


Let’s follow the call stack of ./gradlew :css:build one more time

  • Gradle starts
  • Gradle node plugin checks if node is installed
  • Gradle node plugin runs npm install
  • Gradle node plugin runs Snowpack, if the CSS file in the CSS directory is newer than the CSS file in the build directory. Note that this may not catch changes in the templates
  • Snowpack runs postcss
  • postcss creates custom Tailwind build, that only includes those CSS classes that are found in the files configured
  • postcss minifies CSS, autoprefixes CSS (for different browsers), postcss also exits early on errors instead of you wondering why the color you configured is not valid.

So this is the graph I came up with to visualize this to myself

As you can see, there is a lot of passing down responsibilities until we hit the final step to strip all unneeded CSS classes.

One more thing: I have done Snowpack very wrong in this example. Snowpack is a much more powerful solution than just a caller to postcss. It’s a really fast JS bundler, that can be combined with livereload to instantly update when you change your source files. If you ever write single page applications, taking a look at Snowpack might be well spent time. That said, in this example it is unfortunately only a CSS creation tool.

Also, Snowpack has proxying options via its routing functionality. Unfortunately I was not able to get this up and running with my java application, so that I took the less developer friendly route of static CSS generation. If you want to dig deeper into this, I am happy to retrieve a PR for the GitHub repository.

Do I like all of this? Hell no, it feels overly complex to me! Do I see any other option? Not really… so for now, this post reminds me of the different steps to jump through, to have a custom built.

As I have not found a ton of resources for creating your custom CSS builds within a Java project I keep myself asking if I am doing something terribly wrong, or if everyone is doing things differently and thus does not want to talk about it. If you know more, I am more than happy to be enlightened about what is the best approach to solve this issue.


Final remarks

If you made it down here, wooow! Thanks for sticking with me. You can follow or ping me on twitter, GitHub or reach me via Email (just to tell me, you read this whole thing :-).

If there is anything to correct, drop me a note, and I am happy to do so and append to this post!

Same applies for questions. If you have question, go ahead and ask!

If you want me to speak about this, drop me an email!

Back to posts