Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lit-dev-content): add lit-with-tailwind guide #1278

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/lit-dev-content/site/_data/authors.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,15 @@
"image": {
"url": "authors/elliott-marquez.webp"
}
},
"james-garbutt": {
"name": "James Garbutt",
"links": {
"twitter": "43081j",
"github": "43081j"
},
"image": {
"url": "authors/james-garbutt.jpg"
}
}
}
227 changes: 227 additions & 0 deletions packages/lit-dev-content/site/articles/article/lit-with-tailwind.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
---
title: Lit components with tailwind styles
publishDate: 2023-12-10
lastUpdated: 2023-12-10
summary: Using tailwind styles inside Lit components
thumbnail: /images/articles/lit_with_tailwind
tags:
- web-components
- tailwind
- css
eleventyNavigation:
parent: Articles
key: Lit with tailwind
order: 0
author:
- james-garbutt
---

Tailwind and many other CSS libraries were not designed with web components in
mind, and come with some non-obvious difficulties when trying to use them in
such a codebase.

This is a brief guide on how to use such a library with your Lit components.

## Why tailwind doesn't work out of the box

Out of the box, tailwind basically provides some global CSS classes and injects
the associated CSS at build time (via postcss).

This conflicts with how web components work, since each web component has its
own natively scoped stylesheet rather than inheriting any global styles. This
is why Tailwind will not be much use to us without further setup.

## Overview of solution

To solve the gaps tailwind comes with, we will:

- Inject tailwind styles into regular CSS files
- Use [import attributes](https://github.com/tc39/proposal-import-attributes)
to import those CSS files
- Use [esbuild](https://github.com/evanw/esbuild) to pull those CSS files
into the bundle or same output directory
Comment on lines +39 to +42
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I'm not too familiar with features in this area and this is more for my understanding of what's going on.

Looking at the output from the example repo, esbuild produces this line in the bundle

import styles from "./hello-world-4K4DCJGM.css" assert { type: "css" };

esbuild is being used to bundle and move the css files into the outdir and is it then left up to the users to make this work on unsupported browsers? We probably need a loud caveat stating this only works in Chrome if that's the case. Can es-module-shims be used to make it work for other browser?

Might there be rollup plugins that work with the new correct import attribute syntax?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm... latest esbuild actually does support import attributes with the with keyword. https://github.com/evanw/esbuild/releases/tag/v0.19.7

evanw/esbuild#3384 (comment) Evan does say type: 'css' support will come to esbuild eventually but perhaps a plugin can be written for it in the mean time?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah pretty much, right now it just outputs the source as-is (i.e. your browser needs to support import assertions at least, ideally attributes too).

the esbuild blockage is because of no support for CSS. you're right though, we may be able to use a plugin, i'll have a dig around

it'd be nice to not have to use rollup, just because it consumes most of the build whereas esbuild focuses on only the bundling (as in we should also do the tailwind preprocessing in rollup, if we use rollup).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that makes sense. i don't disagree with the tool choice and i don't want to block or dictate too much as to what this article should say.

since this article being the first of its kind, i think it would be good to convey to the reader that this is meant as one way to solve this problem, with caveats like browser support, and mention alternatives (even without details) like using rollup plugins for all the steps. being published on lit.dev, people might consider this the only recommended way otherwise.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heya, seeing how with is shipped in chrome now + TS 5.4 supporting it, can we switch to with?


## Initial setup

To begin, we need the following dependencies for our build:

- `esbuild` for bundling our code and carry our CSS files across (via imports)
- `postcss` for injecting tailwind styles
- `tailwindcss` for tailwind itself (postcss plugin)

We can install these like so:

```sh
npm install -D esbuild postcss postcss-cli tailwindcss
```

In our case, we are going to use `postcss-cli` for processing our CSS files. If
you use rollup or another bundler already, you may be able to use a postcss
plugin instead.

## Using CSS imports

When creating components, we want to use
[import attributes](https://github.com/tc39/proposal-import-attributes) to
import our CSS files rather than embedding CSS in our sources.

**However, at the time of writing this article, the new `with` syntax is not
fully supported**. Temporarily, we still have to use the old `assert` syntax,
until esbuild implements full support for the new standard.

We can do this like so:

```ts
import myElementStyles from './my-element.css' assert {type: 'css'}

class MyElement extends LitElement {
static styles = myElementStyles;

render() {
return html`
<span class="italic">I am italic tailwind text</span>
`;
}
}
```

As you can see, we want our `my-element.css` file to contain tailwind
mixins, and we want to use the resulting classes in our element's `render`
method.

The CSS file (`my-element.css`) would look like this:

```css
@tailwind base;
@tailwind utilities;

/* other custom CSS here */
```

We will then use tailwind to replace those mixins with CSS, including only
the styles our `render` method referenced.

## Configuring tailwind and postcss

Before we run our build, we need to configure tailwind and postcss.

In our `tailwind.config.js`, we can write:

```ts
/** @type {import('tailwindcss').Config} */
export default {
content: [
'./src/**/*.ts'
],
theme: {
extend: {},
},
plugins: [],
}
```

Tailwind [configuration](https://tailwindcss.com/docs/configuration) can be
modified to fit your use case. The important part is for `content` to specify
where our component sources are so tailwind can detect which classes we have
used.

In our `postcss.config.js`, we simply want to tell postcss to use tailwind
as a plugin:

```ts
export default {
plugins: {
tailwindcss: {}
}
};
```

## Build scripts

We're going to do a few steps in our build:

1. Bundle our sources (TypeScript in this case, including our CSS imports)
2. Apply tailwind styles to the build output in-place

To do this, we can use an npm script:

```json
{
"scripts": {
"build:js": "esbuild --format=esm --bundle --loader:.css=copy --outfile=bundle.js src/main.ts",
"build:css": "postcss -r \"./*.css\"",
"build": "npm run build:js && npm run build:css"
}
}
```

You can see the magic here in our `build:js` script is esbuild's `--loader`
option, which we have set to `.css=copy`. This basically means any CSS files we
import via an ESM import statement will be copied across to the same directory
as the esbuild JS output (`bundle.js` in our case).

So if we import `my-element.css`, we will expect two files in our directory:

- `my-element.css` (probably under another name, using esbuild's chunk naming)
- `bundle.js`

Finally, the `build:css` script then _replaces_ (via the `-r` flag) those CSS
files with copies which now have the tailwind styles injected.

## Run it!

We now have a `build` script! You can run this via `npm run build` and should
see two new files appear as mentioned before.

## Expressions in styles (dynamic styles)

You may have noticed one thing we have lost by having our CSS in external
files: we can no longer template variables into our CSS like so:

```ts
static styles = css`
.foo {
color: ${dynamicColourDefinedInJs};
}
`;
```

There are many challenges in supporting this usage and still passing it through
tailwind. For this reason, **it is not recommended**.

Instead, in places we really need to do this, we should use CSS variables or
do the computation statically at build-time (replace values in our CSS at
build time).

To solve this with CSS variables, we can do the following:

`my-element.css`:

```css
.foo {
color: var(--foo-color);
}
```

`my-element.ts`:

```ts
static styles = [
css`
:host {
--foo-color: ${dynamicColour};
}
`,
myElementStyles
];
```

The initial `styles` entry in this case is only being used for setting the
values of some CSS variables, and never used for actual styling. We can then
reference those variables in our external CSS files.

## Example

An example repository very similar to this guide is available here:

https://github.com/43081j/tailwind-lit-example
Copy link
Collaborator

@e111077 e111077 May 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we do a link that's like:

[Source](https://github.com/43081j/tailwind-lit-example)
[Live demo](https://githubblitz.com/43081j/tailwind-lit-example)

All you would also need to do is have a start npm command that starts a server so that we can serve it like with WDS.

Here is an example with wds and a dev command:

https://stackblitz.com/edit/github-8tsp9c?file=src%2Fhello-world.ts

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.