Setup white-labelled apps with Nx.dev and Tailwind

Cover Image for Setup white-labelled apps with Nx.dev and Tailwind

Setting up white-labeled apps with Nx.dev and Tailwind involves a few steps. Here's a brief overview of the process:

White labelling can be handeled with multiple approaches, Nx is a monorepo solution and lets you have shared libraries and code in one workspace, it is just amazing how it all works. For this tutorial we will be creating multiple builds for our app that will use tailwind configurations for theming, we do this because we want to utilise tailwind's build time prowess to build our styles.

We'll build a small app where we can demonstrate the possibilities of what we can achieve.

We start with the basics.

Install NX, React app and setup Tailwind:

I've already covered this in the article here, you can also use the boilerplate from github or start from scratch using the other link.

If you clone the repo don't forget to run npm install

Add shared tailwind library

We use nx's generate command to create a new shared library that will hold all our tailwind related configs, we do this:

 nx generate @nrwl/js:library tailwind-config

This will generate a Typescript library for us under the folder libs. If you also notice your tsconfig.base.json, it should now have this path.

"paths": {
      "@with-nx-react-tailwind/tailwind-config": [

Our lib can now be imported like any npm package like:

import {Something} from '@with-nx-react-tailwind/tailwind-config';

Although we won't be doing much importing, for the scope of our exercise.

Convert our lib/tailwind-config project from TS to JS

We need to do this in order for tailwind to work, tailwind does not understand .ts files as config hence if you notice our tailwind.config file has extension .js and not .ts

We can get rid of the function that was auto generated in our tailwind-config lib and rename index.ts to .js

We also need to touch our tsconfig.base.json file to allowJS

  "compilerOptions": {
    "allowJs": true,

// also ensure webapp/tsconfig.json and tailwind-config/tsconfig.json 
// don't override allowJs flag

Once all this is out of way we can start putting in some configs.

Let's bring in a default config.

// tailwind-config/src/lib/default.config.js

module.exports = {
  theme: {
    extend: {
      colors: {
        'ceruleanBlue': {
          '50': '#f4f6fd',
          '100': '#e9eefa',
          '200': '#c7d4f3',
          '300': '#a6baec',
          '400': '#6387dd',
          '500': '#2053cf',
          '600': '#1d4bba',
          '700': '#183e9b',
          '800': '#13327c',
          '900': '#102965'

We create 2 more configs specific to 2 different enterprises.

// tailwind-config/src/lib/enterprise1.config.js

module.exports = {
  theme: {
    extend: {
      colors: {
        brand: {
          '50': '#f9f3fd',
          '100': '#f3e7fc',
          '200': '#e1c3f7',
          '300': '#cf9ff3',
          '400': '#ac57e9',
          '500': '#880fe0',
          '600': '#7a0eca',
          '700': '#660ba8',
          '800': '#520986',
          '900': '#43076e'

// tailwind-config/src/lib/enterprise2.config.js

module.exports = {
  theme: {
    extend: {
      colors: {
        brand: {
          '50': '#f7fbf5',
          '100': '#eef7eb',
          '200': '#d5ecce',
          '300': '#bbe1b0',
          '400': '#88ca74',
          '500': '#55b339',
          '600': '#4da133',
          '700': '#40862b',
          '800': '#336b22',
          '900': '#2a581c'

Now comes the actual logic we need to put in place. We move some of the config from generated tailwind.config.js here. It needs a value dirname here because of the way NX handles file paths, we need to be giving tailwind the right paths to ensure it compiles our css and understands the classes.

const defaultPreset = require('./lib/default.config');
const {join} = require("path");
let clientPreset = [];

console.log(`\n ====>>> client = ${process.env.CLIENT} <<<==== \n`);

if (process.env.CLIENT !== undefined) {
  clientPreset = [require(`./lib/${process.env.CLIENT}.config`)];

module.exports = (dirname) => {
  return {
    content: [
    presets: [

We use tailwind's presets to use our default config and then the client preset for our clients. process.env.CLIENT will give us the active client or the client that we're building for and it would then pickup relevant config for tailwind.

One last change, we need to tell tailwind.config.js in our webapp to use the one from lib

let defaultTailwindPreset = require('../../libs/tailwind-config/src');

/** @type {import('tailwindcss').Config} */
module.exports = {
  presets: [defaultTailwindPreset(__dirname)],

// we can't use import from @our-lib because this is .js file and it 
// doesn't understand our tsconfig path that we setup initially.

To see this in action make changes to the webapp component.

// webapp/src/app/app.tsx

<div className='h-screen bg-brand-500 flex flex-col justify-center items-center'>
    <h1 className='font-bold text-5xl bg-brand-200'>This works!</h1>
    <pre className="text-sm">font-bold text-5xl bg-brand-200</pre>
    <pre className="text-sm text-cerulean-blue-200">I'm there in all configs</pre>
    <div className="absolute top-0 left-0">
        <p className="text-sm">bg-brand-500 flex flex-col justify-center items-center</p>

To run this:

nx serve webapp
// this runs our default variant of our tailwind config

CLIENT=enterprise1 nx serve webapp
CLIENT=enterprise2 nx serve webapp

This is how it looks like once it's run.


You can see we've run 3 different flavours of the same code. The limit is endless this example is the absolute naive way of handling things there's a whole lot more to it.

In conclusion there's plenty more to Nx its a great way to help us build complex projects and yet manage all the dependencies in an efficient way.

In the next part we'll cover how we can build this and export as different artifacts.

You can clone the whole thing from this github repo 🚀