Using tailwind at build-time with lit-element
Tags: tailwindcss, typescript, javascript, webdev
Published: 2020-12-29 21:14:41

## Outdated solution!

There's a much easier way to do this now:

{% link 43081j/using-tailwind-v3-with-lit-elements-39mk %}

**Read on if the new solution doesn't work, or you just prefer this one...**

---

A few days ago, I wrote about using [tailwind](https://github.com/tailwindlabs/tailwindcss) with web components at run-time:

{% link 43081j/using-tailwind-at-run-time-with-web-components-47c %}

At the time, I was actually trying to figure out how to do this at build-time but was struggling to find an existing solution. Good news: I found one!

Keep in mind, this example is **specific to lit-element**.

## My Setup

As in my previous article, the same setup was used:

* A single web component (lit-element in this case)
* esbuild
* TypeScript

Using a [lit-element](https://github.com/polymer/lit-element) component:

```ts
class MyElement extends LitElement {
  static styles = css`
    /*
     * Somehow we want tailwind's CSS to ultimately
     * exist here
     */
  `;

  render() {
    // We want these tailwind CSS classes to exist
    return html`<div class="text-xl text-black">
      I am a test.
    </div>`;
  }
}
```

## The problem

As discussed in my [last post](/43081j/using-tailwind-at-run-time-with-web-components-47c), tailwind doesn't seem to support shadow DOM or web components in general out of the box.

I previously solved this by using [twind](https://github.com/tw-in-js/twind), a great little library which behaves as a 'tailwind runtime' and produces the correct stylesheets at run-time.

However, not everyone wants a run-time solution, some have static enough CSS they would rather build it once and forget.

So, as you saw in the example above, our aim is to inject tailwind's CSS into the component's stylesheet.

## The investigation

Getting to the solution below took quite some time over the past day or so, involved finding a few bugs and discovering new tools.

First of all, I did some googling and found:

### postcss-js

This is a postcss plugin for dealing with "CSS in JS". Sounds promising!

But no, this is a plugin for converting between CSS objects (actual JS representations of CSS) and CSS strings. We don't want this, we want to transform CSS strings in-place.

### babel plugin

The babel plugin extracted CSS from template literals, passed them through postcss and replaced the original. Exactly what we need!

But... it is a babel plugin and we don't want to use babel. So this one was a no, too.

### rollup plugin

A rollup plugin or two exist which do the same as "postcss-js": they transform to and from CSS objects.

Again, not what we want.

### Custom rollup plugin

I then made my own rollup plugin, which extracted template literals the same as the babel plugin did and processed them with postcss.

This did work, but seemed like overkill and tied us into rollup. I didn't really want to have a solution which depends on another build tool being used.

Fun to make my own rollup plugin, though, so good experience.

### postcss-jsx (aka postcss-css-in-js)

Andrey (postcss maintainer) at this point recommended I use "postcss-jsx". I had seen this while googling previously but couldn't quite figure out from the docs how to get it working with my sources.

It sounded like the right way to go, though, so I tried again!

First try, I managed to get it processing the CSS from my element! Success. It resulted in a huge stylesheet (all of tailwind) but looked like it worked.

#### Bug 1

Not so fast, though. I tried this in a browser and was met with a good ol' syntax error. The _first_ bug: postcss-jsx doesn't escape backticks in the output CSS.

Tailwind's CSS contains comments with backticks, so we end up producing syntactically incorrect code like this:

```ts
const style = css`
  /** Tailwind broke `my code with these backticks` */
`;
```

At this point, I noticed postcss-jsx is unmaintained and the folks at stylelint have forked it. So I filed the first bug in my investigation:

https://github.com/stylelint/postcss-css-in-js/issues/89

#### Bug 2

I fixed postcss-css-in-js locally to escape backticks, so I now got some output. 

But this won't work for anyone else until the package is fixed, of course. So I figured we can get around it: use cssnano to strip comments entirely - making those backtick comments conveniently disappear.

Installed cssnano, added it to my postcss config, and used the "lite" preset as I only wanted empty rules and comments removing.

Turns out, cssnano-preset-lite doesn't work with postcss-cli. Another bug:

https://github.com/cssnano/cssnano/issues/976

#### Bug 3

I almost forgot, postcss-css-in-js also had a 3rd bug: it produces an AST like this:

```
Document {
  nodes: [
    Root { ... },
    Root { ... }
  ]
}
```

Turns out, postcss has trouble stringifying nested roots. Bug raised and even tried a PR this time:

https://github.com/postcss/postcss/issues/1494

**UPDATE: fixed in PostCSS 8.2.2!**

## Solution

After this excellent amount of fun finding bugs and researching solutions, I finally got to one which works.

### Source

To include tailwind's CSS, we do exactly as in their docs:

```ts
export class MyElement extends LitElement {
  public static styles = css`
    @tailwind base;
    @tailwind utilities;
    /* whatever other tailwind imports you want */
  `;
  // ...
}
```

These `@tailwind` directives will later be replaced with tailwind's actual CSS by postcss.

### Dependencies

As mentioned above, we needed the following:

```bash
$ npm i -D postcss @stylelint/postcss-css-in-js tailwindcss postcss-syntax postcss-discard-comments postcss-discard-empty
```

### Build script (`package.json`)

```json
{
  "scripts": {
    "build:js": "tsc && esbuild --bundle --format=esm --outfile=bundle.js src/index.ts",
    "build:css": "postcss -r bundle.js",
    "build": "npm run build:js && npm run build:css"
  }
}
```

Running `npm run build` will:

* Run typescript (with `noEmit: true`) just for type-checking
* Run esbuild to create a JS bundle
* Run postcss and replace the contents of the JS bundle in place

### `tailwind.config.js`

```json
module.exports = {
  purge: [
   './bundle.js'
  ]
};
```

Here, `bundle.js` is what we produced with esbuild earlier on. We want to purge unused styles from our bundle.

### `postcss.config.js`

```ts
module.exports = {
  syntax: require('@stylelint/postcss-css-in-js'),
  plugins: [
    require('tailwindcss')(),
    require('postcss-discard-comments')(),
    require('postcss-discard-empty')()
  ]
};
```

Here:

* `syntax` tells postcss how to read our JS file
* `tailwindcss` injects tailwind's CSS and then purges unused styles
* `postcss-discard-comments` discards comments (which prevents bug 1 above)
* `postcss-discard-empty` discards the empty rules tailwind left behind after purging

Note: *cssnano can be used instead of the last 2 plugins but we didn't in this case because of bug 2 above*

### Build it

Our build script from before should now work:

```bash
$ npm run build
```

If we want to strip all those unused styles and make use of the `purge` option in our config, we need to specify `NODE_ENV`:

```bash
$ NODE_ENV=production npm run build
```

Tailwind will pick this up and purge unused styles.

#### Enabling purging in both dev and prod

If you always want purging to happen, simply change your tailwind config to look like this:

```ts
module.exports = {
  purge: {
    enabled: true,
    content: [
      './bundle.js'
    ]
  }
};
```

This is described more [here](https://tailwindcss.com/docs/optimizing-for-production).

### Optimise it

We can do a bit better than this. Right now, we are producing a tailwind stylesheet for each component.

If we have multiple components, each one's stylesheet will have a copy of the tailwind CSS _the whole app_ used (as we're operating against the bundle, not individual files).

So we'd probably be better off having a single tailwind template many components share:

```ts
// styles.ts
export const styles = css`
  @tailwind base;
  @tailwind utilities;
`;

// my-element.ts
import {styles} from './styles';
export class MyElement extends LitElement {
  static styles = [styles];
  public render() {
    return html`<p class="p-4">One</p>`;
  }
}

// another-element
import {styles} from './styles';
export class AnotherElement extends LitElement {
  static styles = [styles];
  public render() {
    return html`<p class="p-6">Two</p>`;
  }
}
```

This means we will produce one monolithic tailwind stylesheet all of our components re-use.

In the example above, `.p-6` and `.p-4` (the classes used in the `render` methods) will both exist in the stylesheet with all other unused styles stripped.

Whether this is an optimisation or not does depend on your use case. Just remember the "purging" happens on the bundle, not the individual files.

## Useful links (packages we used)

- [postcss](https://github.com/postcss/postcss)
- [postcss-css-in-js](https://github.com/stylelint/postcss-css-in-js)
- [cssnano](https://github.com/cssnano/cssnano/)
- [postcss-syntax](https://github.com/gucong3000/postcss-syntax)
- [tailwindcss](https://github.com/tailwindlabs/tailwindcss)

## Wrap-up

As I said in my previous post, run-time vs build-time is a project-based preference I think. Some of you will be better off using the run-time twind solution, others will be better off using this build-time solution.

If your styles are very static (i.e. you don't really dynamically use any at run-time) or you already have a similar postcss build process, you should probably process Tailwind at the same time.

The cssnano inclusion is a hack in my case, to get around bug 2 mentioned above. Though you probably want to use it anyway to save some bytes in production.

Have fun!