Kotlin/JS and Tailwind CSS

I’ve been working on a project with my tech stack relying on Kotlin Multiplatform. It’s about time to add some styling, and I wanted to try out Tailwind CSS.

It was surprisingly hard to find resources that explained how to get everything working—particularly because I’m a novice when it comes to modern front end practices. I did eventually find everything I wanted; this post is for anyone else trying to get things working.

The TL;DR: follow these instructions. In this post I’ll give more context into the things I learned/discovered.

Requirements

The aim is to get Tailwind CSS into our Kotlin/JS app. We prefer to do everything in as standard/idiomatic of a way as possible.

From the Tailwind CSS Installation, we want to install Tailwind as a PostCSS plugin. PostCSS is a CSS preprocessor—it takes our raw CSS (which will use Tailwind) and transforms it into some final CSS.

We also need something that runs PostCSS. The default for Kotlin/JS projects is to use webpack.

So, we want to:

Exploration

To understand what’s going on, it helps to see what we’d need to do if we weren’t using Kotlin.

As webpack, PostCSS, and Tailwind CSS are popular frameworks in use today, we’d expect there to be plenty of information on how to get the three playing together. Sure enough, there are guides such as this one.

Webpack + PostCSS

Webpack is configured by a webpack.config.js file. In particular, we want it to contain something like this:

// webpack.config.js
module: {
  rules: [
    {
      test: /\.css$/,
      use: [
        'style-loader',
        'css-loader',
        'postcss-loader'
      ]
    }
  ]
}

This config specifies that webpack should look (“test”) for files ending in “.css”, then apply the specified loaders.

Loaders are webpack’s mechanism to preprocess files. The postcss-loader will eventually be responsible for configuring Tailwind CSS, but I wasn’t familiar with css-loader and style-loader. These are responsible for ensuring that the CSS we need makes it into our app.

We’ll typically have something that looks like:

<!-- index.html -->
<!DOCTYPE html>
  <head>
    <meta charset="UTF-8">
    <title>My App</title>
  </head>
  <body>
    <script src="/app.js"></script>
    <div id="root"></div>
  </body>
</html>

Our app relies on CSS to be styled correctly, but we’re not actually including any stylesheets here. That’s where css-loader and style-loader come in.

Inside of app.js, we need to specify that we depend on some kind of CSS (e.g. styles.css). css-loader understands these dependencies and finds (“loads”) all of that styling. Once css-loader is done, it outputs a string with all of the CSS.

style-loader then takes this string and adds it to our HTML, in a <style> block. Thus, style-loader.

The result of this preprocessing is that we get something like:

<!-- index.html -->
<!DOCTYPE html>
  <head>
    <meta charset="UTF-8">
    <title>My App</title>
    <style>
      // Styles we depend on.
      // This block is generated from our loaders.
    </style>
  </head>
  <body>
    <script src="/app.js"></script>
    <div id="root"></div>
  </body>
</html>

PostCSS + Tailwind CSS

This part is described in the Tailwind installation instructions. They state to add a postcss.config.js file:

// postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  }
}

Kotlin Issues

Webpack Config

The first issue we run into in our Kotlin/JS project is that we cannot specify a webpack.config.js file—this is automatically generated for us. We need a way to configure webpack.

As described here, we do have some ways to configure webpack. To do so, we create a directory webpack.config.d. Any configuration we need should be specified as a .js file in that directory.

To better understand this, the generated webpack.config.js looks like this:

// cat build/js/packages/web/webpack.config.js 
let config = {
  mode: 'development',
  resolve: {
    modules: [
      "node_modules"
    ]
  },
  plugins: [],
  module: {
    rules: []
  }
};

/* more config */

module.exports = config

We want to modify config.module.rules to use our desired loaders, as above. On paper, this looks like:

// webpack.config.d/css.config.js
config.module.rules.push({
  test: /\.css$/,
  use: [
    'style-loader',
    'css-loader',
    'postcss-loader'
  ]
});

I never quite got this to work, possibly because of…

CSS Support

Also from the Kotlin/JS Documentation:

The Kotlin/JS Gradle plugin also provides support for webpack’s CSS and style loaders.

This is done by setting the cssSupport.enabled flag:

// build.gradle.kts
browser {
    commonWebpackConfig {
        cssSupport.enabled = true
    }
    binaries.executable()
}

As far as I understand, this is equivalent to adding style-loader and css-loader, as previously discussed.

However, this adds a module rule that we still need to add the postcss-loader to. We can’t directly modify it, so we’ll need to indirectly do so.

Solution

This solution comes almost verbatim from here, and is reproduced just for completeness.

Dependencies

Add these dependencies to your build.gradle.kts. They belong in the dependencies section for a Kotlin/JS project, and the jsMain section for a Kotlin Multiplatform project.

implementation(npm("tailwindcss", "2.1.2"))
implementation(npm("postcss", "8.2.13"))
implementation(npm("postcss-loader", "4.2.0"))
implementation(npm("autoprefixer", "10.2.5"))

PostCSS Config

Add postcss.config.js at the root of your project, as specified here:

// postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  }
}

Tailwind CSS Config

The Tailwind docs imply it’s optional, but I still added the Tailwind config file.

The official instructions want to use npx, but things are configured differently with our Kotlin/Gradle setup.

The referenced solution handles this by running:

$ ./gradlew kotlinNpmInstall
$ ( cd build/js/ && npx tailwindcss init && mv tailwind.config.js ../../ )

As of this writing, this is equivalent to adding this tailwind.config.js at the root of your project:

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

PostCSS Loader

At this point, we still haven’t configured postcss-loader. As described before, this is done by editing the rule that was generated via cssSupport.

We just want to add the postcss-loader to the list of loaders to use:

// in webpack.config.d/postcss-loader.config.js
(() => {
  const cssRule = config.module.rules.find(r => "test.css".match(r.test));
  if (!cssRule) {
    throw new Error("Could not resolve webpack rule matching .css files.");
  }
  cssRule.use.push({
    loader: "postcss-loader",
    options: {}
  });
})();

CSS Dependency

Finally, we want to add our dependency to the CSS we need. This also allows us to confirm if things are working!

First, we want to make sure that our app uses Tailwind. Add a styled element to index.html, for example:

<p class="text-lg">Tailwind Test</p>

Via Stylesheet

The standard way to add Tailwind is to include it via your own stylesheet. Add one that looks like:

// commonMain/resources/styles.css
@tailwind base;
@tailwind components;
@tailwind utilities;

Then, in the main() function for our app:

fun main() {
    kotlinext.js.require("/styles.css")
    // ...
}

We should now be able to build and run our app and see our Tailwind-styled element!

Via Direct Dependency

If your JS framework allows importing CSS files (e.g. React), the Tailwind docs mention that we don’t need to create a stylesheet just to include Tailwind.

So, we can skip adding styles.css as mentioned above. Instead, we just change what we require:

fun main() {
    kotlinext.js.require("tailwindcss/tailwind.css")
    // ...
}

Happy coding!