Sharing tailwind config elsewhere for variable access (theme, spacing, and others)

Recently I came across a requirement for my side project to be able to access tailwind theme (such as colors and spacing) through javascript. Tailwind has a javascript file that exports an object to define custom attributes such attributes can be then used by the application - Imagine that you want to set a custom background color besides the one that comes already with tailwind, this is a custom attribute. Therefore, accessing this file from a reactjs application is not straight forward as it seems.

This blog post is an attempt to share the problems I faced trying to expose the tailwind config, for the reader that want to jump straight into the code, the commit I used to achieve that is available on github.

Context

In the end, the goal was to use the same code used to define custom attributes in the tailwind file across reactjs components. So basically besides using class names for style, will be possible to use them in javascript.

Setting up the project

The project that I am using as an example is the json-tool which used the Create React App (CRA) to scaffold the project. Using CRA is an important bit as it won’t allow us to set custom webpack plugins to be used, later we will dive into this in more detail. The first heck point to keep following this post is to have:

  1. Reactjs project created with CRA (without ejecting)
  2. Set tailwind following their tutorial

Once both setups are done, the project structure should be something like the following files:

├── craco.config.js                  <----------- required by tailwind
├── LICENSE.md
├── package.json
├── package-lock.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── main.js
│   ├── manifest.json
│   └── robots.txt
├── README.md
├── src
│   ├── App.test.tsx
│   ├── App.tsx
│   ├── core
│   ├── index.css
│   ├── index.tsx
│   ├── react-app-env.d.ts
│   ├── reportWebVitals.ts
│   └── setupTests.ts
├── tailwind.config.js               <---------
└── tsconfig.json

This is a standard CRA application created with typescript support (which is optional). The spotlight goes to the tailwind.config.js file, this is the file mentioned before which holds tailwind’s custom styles. For example, you can write the following code without problems in such setup:

// App.tsx
function App() {
  return (
    <div className="bg-black m-5">
      <p>Hello there</p>
    </div>
  );
}

The code above would render a div with a black background color and a margin of 5, those classes are part of tailwind, not much to add here. Adding those classes through the markup is tailwind foundation.

Unveiling the problem

Now, if using classes is the basics, there are some specific cases that it is not possible, for example, what if a third party library does not expose the class to be used for external consumers, and instead offers only the style?

In this case, one of the possible solutions if to use the javascript notation to fetch the tailwind attributes and apply them. Tailwind official documentation explores this idea and offers a straight forward solution. Let’s take the previous example and build on top of that:

import resolveConfig from 'tailwindcss/resolveConfig';
import tailwindConfig from '../tailwind.config.js';

// App.tsx
function App() {
  return (
    <div className="bg-black m-5">
      <p>Hello there</p>
    </div>
  );
}

Side note: if you are using typescript as the example, you might face the following error message when importing the resolveConfig: Could not find a declaration file for module ‘tailwindcss/resolveConfig’. ‘node_modules/tailwindcss/resolveConfig.js’ implicitly has an ‘any’ type. Try npm i --save-dev @types/tailwindcss__postcss7-compat if it exists or add a new declaration (.d.ts) file containing declare module 'tailwindcss/resolveConfig'; TS7016

The easiest fix I could find is to create the file tailwind.d.ts and add the line declare module 'tailwindcss/resolveConfig';

At this point, the project should transpile without any problem except a few warnings around unused variables. To avoid such warnings, the next step is to use both imports to “resolve” the tailwind configuration (resolve means to merge the default tailwind configuration and the configuration from tailwind.config.js).

import resolveConfig from 'tailwindcss/resolveConfig';
import tailwindConfig from '../tailwind.config.js';

const fullConfig = resolveConfig(tailwindConfig);   // <--- merge configs

// App.tsx
function App() {
  return (
    <div className="bg-black m-5">
      <p>Hello there</p>
    </div>
  );
}

Trying to do that will result in the first error related to the CRA setup:

Failed to compile.

./src/App.tsx
Module not found: You attempted to import ../tailwind.config.js which falls outside of the project src/ directory. Relative imports outside of src/ are not supported.

CRA does not allow import from outside the src folder. It seems that this is a restriction related to CRA. To overwrite this limitation, would be needed to eject from CRA, which isa blocker for me. Then, the next try would be to move the tailwind.config.js inside the src folder.

├── LICENSE.md
├── main.spec.ts
├── package.json
├── package-lock.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── main.js
│   ├── manifest.json
│   └── robots.txt
├── README.md
├── src
│   ├── App.test.tsx
│   ├── App.tsx
│   ├── core
│   ├── index.css
│   ├── index.tsx
│   ├── react-app-env.d.ts
│   ├── reportWebVitals.ts
│   ├── setupTests.ts
│   ├── tailwind.config.js          <----------------
│   └── tailwind.d.ts
└── tsconfig.json

Lastly, there is a change in the craco.config.js as well, tailwind assumes that the file tailwind.config.js will be always in the root of the project. As we changed it, we need to tell craco about the file again. Otherwise the following message will be displayed in the terminal:

warn - Tailwind is not purging unused styles because no template paths have been provided.
warn - If you have manually configured PurgeCSS outside of Tailwind or are deliberately not removing unused styles, set `purge: false` in your Tailwind config file to silence this warning.
warn - https://tailwindcss.com/docs/controlling-file-size/#removing-unused-css

The fix is to update the craco config file with the following content:

module.exports = {
  style: {
    postcss: {
      plugins: [
        require('tailwindcss')('./src/tailwind.config.js'),
        require('autoprefixer'),
      ],
    },
  },
}

Moving tailwind to under src fixes the location issue and attempts to run the following code will work as expected in development mode:

import resolveConfig from 'tailwindcss/resolveConfig';
import tailwindConfig from './tailwind.config.js';

const fullConfig = resolveConfig(tailwindConfig);
const style = { backgroundColor: fullConfig.theme.colors.black };

function App() {
  return (
    <div style={style}>
    </div>
  );
}

It’s worth mentioning that the replace from className by style is on purpose, as tailwind exposes the raw style, we can use it in style tags as well.

Therefore attempting to create the production bundle (npm run build), will fail and result in the following error:

Creating an optimized production build...
Failed to compile.

./src/App.tsx
Attempted import error: './tailwind.config.js' does not contain a default export (imported as 'tailwindConfig').

To avoid this issue I tried different strategies (as the error message points to an issue regarding tailwind configuration location), and the one that fixed the problem is in the next section.

The workaround

As this is such a dilemma to get to work, I started to search on google and found different issues related to this problem (tailwind, webpack and storybook). The one that caught my attention was the discussion #1853 that relates the same problem as I was having. Even though the issue is from 2020, there is no “official” answer for that, anyways sara-jegorova shared a workaround that worked. Before jumping into the fix, let’s do a quick check point:

  1. The file tailwind.config.js should be under the root folder.
  2. Make sure that the error that you see in the console is: Uncaught TypeError Cannot assign to read only property ‘exports’ of object<#Object>.

The workaround uses the plugin babel-plugin-preval to evaluate the source code before using it, as the error shows that, it can’t handle the module.exports that is nodejs related, preval will do the job. The first step is to install the preval plugin:

npm install --save-dev babel-plugin-preval

Now, the craco.config.js comes into play as it allow us to hook into webpack configuration without ejecting, the configuration should be something like the following:

module.exports = {
  style: {
    postcss: {
      plugins: [
        require('tailwindcss'),
        require('autoprefixer'),
      ],
    },
  },
  babel: {
    plugins: ['preval']     <----------- enables the preval plugin
  }
};

Next, we will need to create a javascript file under the src folder to hold the tailwind “wrapper”. I named my file tailwindWrapper.js and added the following content to it:

// @preval
const resolveConfig = require('tailwindcss/resolveConfig');
const tailwindConfig = require('../tailwind.config');
module.exports = resolveConfig(tailwindConfig);

In the end, the project structure should be something similar to the following:

├── craco.config.js                   <---------------------
├── icons
│   └── 512x512.png
├── LICENSE.md
├── package.json
├── package-lock.json
├── public
│   ├── electron.js
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
├── README.md
├── src
│   ├── App.test.tsx
│   ├── App.tsx
│   ├── components
│   ├── core
│   ├── index.scss
│   ├── index.tsx
│   ├── react-app-env.d.ts
│   ├── reportWebVitals.ts
│   ├── setupTests.ts
│   ├── __snapshots__
│   └── tailwindResolver.js           <---------------------
├── tailwind.config.js                <---------------------
└── tsconfig.json

With everything in place, the fix should be ready to be tested, in order to do that, I would suggest the following actions:

  1. npm run start - to check that in development mode everything works as expected.
  2. npm run build and serve -s build - to check that the production build work as expected.

If the application runs as expected in both cases, the fix is done. I couldn’t spot other issues while writing this blog post, but they can exists and if so a comment is always welcome.