All Articles

Using Tailwind CSS with Jetpack Compose for Web

With Jetpack Compose for Web, you have the option of defining your own styles using the provided Kotlin DSL, but I wanted to use Tailwind CSS for my project so this post outlines how did the setup required.

Background

The Kotlin IR compiler takes Kotlin code as input and outputs Javascript code. For this Javascript code to be usable on the browser, the Kotlin gradle plugin uses Webpack. Webpack bundles the Javascript code into a single one. It can also bundle CSS files that are imported in Javascript files.

To get this set up to work, I needed to customise the webpack configuration to include the required tools for Tailwind CSS.

NPM dependencies

Following the official Tailwind CSS guide, we should add the following npm dependencies

implementation(npm("tailwindcss", "3.0.23")) // Official tailwindcss package
implementation(npm("postcss", "8.4.8")) // CSS processor called postcss
implementation(npm("autoprefixer", "10.4.2")) //parse CSS and add browser specific prefixes to CSS rules

And since KotlinJS projects use Webpack by default, we will need to add the following webpack loaders to handle CSS files and integrate with postcss

implementation(npm("style-loader", "2.0.0"))
implementation(npm("css-loader", "5.2.7")) // required to handle @import/url() rules in CSS files
implementation(npm("postcss-loader", "4.3.0")) // required to invoke postcss during bundling

Configuring Tailwind CSS

Per the official documentation we need to have a postcss.config.js and a tailwind.config.js. I’ve created them and put them in the root directory of my project.

// postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  }
}
// tailwind.config.js
module.exports = {
  content: ["**/*.{html,js}"], // This is important
  theme: {
    extend: {},
  },
  plugins: [],
}

Customising Webpack

By default, the Kotlin gradle plugin will generate its own webpack config file but we can customise it by adding .js file into webpack.config.d directory. This directory should be in the root of the project.

In order to add support for handling css files using style-loader, css-loader and postcss-loader, I’ve created postcss.js inside my webpack.config.d

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

Updating the build process

By default, webpack is invoked after the Kotlin code has been compiled. This means that the working directory for webpack is somewhere inside the build/ directory and for our css configuration to work, we need to move postcss.config.js and tailwind.config.js to the same directory before webpack is invoked.

So First, let’s create a gradle task to copy the files

tasks.register<Copy>("copyTailwindConfig") {
    dependsOn("kotlinNpmInstall") // ensures that all required dependencies are installed
    val jsFolder = project
        .buildDir
        .resolve("js")
        .resolve("packages")
        .resolve(rootProject.name) // This is where the JS is and where webpack works  
    delete(jsFolder.resolve("tailwind.config.js")) // deleting any existing tailwind.config.js file
    // Copying tailwind.config.js from root directory to js folder
    from(projectDir.resolve("tailwind.config.js"))
    into(jsFolder)
    delete(jsFolder.resolve("postcss.config.js"))
    // Copying tailwind.config.js from root directory to js folder
    from(projectDir.resolve("postcss.config.js"))
    into(jsFolder)
}

Then we can hook into the build process by making jsBrowserDevelopmentRun & jsBrowserProductionRun depend on our task

tasks.getByName("jsBrowserDevelopmentRun").dependsOn("copyTailwindConfig")
tasks.getByName("jsBrowserProductionRun").dependsOn("copyTailwindConfig")

Importing Tailwind CSS

After setting up the build system to handle CSS files, we can include our default CSS file. This file is copied from Tailwind’s official guide and stored in src/jsMain/resources/ directory.

/* index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

If you compile and check where this CSS file located, you will notice it’s on the same directory level as the JS bundle.

Build Directory Structure

This is important as we need to reference that CSS file in our Javascript bundle, via Kotlin, for Webpack to know about it.

In Javascript, we usually can do import "./index.css" and the equivalent in Kotlin is using @JsModule annotation.

@JsModule("./input.css")
external val cssFile: dynamic

Wrapping up

With the above configuration, we made sure that first we moved Tailwind CSS and PostCSS configuration files to the correct location before webpack starts using them. We also customised the webpack configuration to support bundling CSS files and process them with PostCSS.

PostCSS will invoke Tailwind to insect the generated JS bundle for usages for Tailwind CSS related CSS classes and include the required styles in our bundle.

Finally, we are ready to use Tailwind’s classes in our composables

fun main() {
    renderComposable(rootElementId = "root") {
        Div({ classes("container", "mx-auto", "bg-cyan-700") }) {
            // Content
        }
    }
}