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:
- Use webpack to run PostCSS.
- Use PostCSS to generate our Tailwind-friendly CSS.
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"))
- Note that
postcss-loader
seems to not work on versions >= 5.0.0. - The referenced solution also depends on
kotlin-extensions
, but these seem to already be available (potentially fromkotlin-react
, which I’m also using).
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!