DEV Community

James Garbutt
James Garbutt

Posted on

Using tailwind at run-time with web components

I've seen Tailwind mentioned a fair amount lately but still not had the chance to use it so far. I figured I'd finally give it a go, using some of that Christmas "vacation time".

My setup

When I play around with a new tool or library, I generally throw up the same thing:

  • A single web component (vanilla or lit-element)
  • rollup or esbuild (if needed)
  • TypeScript

In this case, I threw this together:

class MyElement extends LitElement {
  static styles = css`
    :host { display: block; }
    /*
     * 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>`;
  }
}
Enter fullscreen mode Exit fullscreen mode

Where I want to get to is commented in the code. Essentially, the ability to use Tailwind classes.

The problem/gaps

Tailwind seems to have pretty poor or non-existent support for web components. It doesn't support most of the web component APIs/concepts out of the box.

By default, its styles will simply be added to the <head> of our document. This, of course, means that they will be unavailable to any web components which use shadow DOM (intentionally).

Remember, shadow DOM does not inherit styles from the document. It has its own scoped stylesheets.

So what we somehow need to do is detect which Tailwind classes we have used and populate the correct CSS for those classes into our component's stylesheet.

Possible solution #1 (build-time)

The first potential solution is a build-time process.

Tailwind offloads most of its work to PostCSS and follows this kind of process:

  • Detects any directives (@tailwind in CSS)
  • Replaces those with the actual Tailwind CSS

This only works when transforming a single CSS file via PostCSS out of the box. You cannot transform CSS this way from within a JS or HTML file (without extra plugins).

So the ideal build-time solution would be a way to allow PostCSS to transform inline CSS from within tagged templates, in-place.

Basically, this is the result I want:

class MyElement extends LitElement {
  // Build tool somehow figured out which classes we
  // referenced and added the associated CSS to a css
  // tag of our choosing.
  static styles = css`
    .text-x1 { /* ... */ }
    .text-black { /* ... */ }
  `;

  render() {
    return html`<div class="text-xl text-black">
      I am a test.
    </div>`;
  }
}
Enter fullscreen mode Exit fullscreen mode

I have yet to find an existing tool to solve this problem. If anyone knows a solution to this, please do let me know!

Possible solution #2 (run-time)

By pure coincidence, while trying to figure out the previous solution, the following package was tweeted and released:

https://github.com/tw-in-js/twind/

This library changes the whole thing to be a run-time process.

The idea is:

  • Anytime we want to use a tailwind class, we use a helper called tw
  • This tracks which classes we've used
  • All the classes we've used have their CSS generated and appended to a stylesheet we specify

Here's our example from before, but now using twind:

import { create, cssomSheet } from 'twind'

// This creates a twind "sheet" which is a wrapper
// around a CSS stylesheet
const sheet = cssomSheet({ target: new CSSStyleSheet() });

// This gets an instance of the tw helper for our
// stylesheet above. Meaning any time we use `tw`, it will
// append the right CSS to our custom stylesheet.
const { tw } = create({ sheet });

class MyElement extends LitElement {
  // We tell lit that we have already made a CSS
  // stylesheet and to use that directly.
  // At this point, it will be an empty sheet, but will
  // be updated after our first render by `tw`
  static styles = [sheet.target];

  render() {
    // Any time we use a tailwind class, we use the `tw`
    // helper instead of using it directly. This allows
    // twind to track the classes and generate the correct
    // CSS.
    return html`<div class=${tw`text-xl text-black`}>
      I am a test.
    </div>`;
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the following happens:

  • We create a stylesheet and assign it to our element
  • When we render, we call tw which simply returns the string we passed it but meanwhile generates the associated CSS and appends it to the stylesheet

This is a cool little library since we never have to ship the tailwind CSS, all of what we need is generated at run-time. This actually means we don't ship much code to the client and we only generate the CSS we make use of rather than including Tailwind's whole stylesheet.

Wrap up

I don't think either of these options is a "winner". Different use cases need different solutions.

A build-time solution would be so nice as we could statically generate the CSS once and ship that to the client.

A run-time solution is super nice too as we can generate any tailwind CSS we need at any time. We don't need to package up the whole Tailwind CSS.

If anyone does know of an existing solution to the build-time option, I'd love to hear about it before I go off and write my own 😂

Meanwhile, do check out Twind as its a nice lightweight solution and fits in nicely with web components either way.

Top comments (7)

Collapse
 
westbrook profile image
Westbrook Johnson

James, does the runtime version create a fully unique stylesheet for each element you use?

I'd be very interested in hearing if you thought there was a middle ground solution that was able to build the used Tailwinds style as "parts" or "sets" that could then be consumed by LitElement or any system that leverages adopted stylesheets, a la:

static styles = [tailwinds.colors, tailwinds.headlines, ...etc.];
Enter fullscreen mode Exit fullscreen mode

Whereby the parts that could be leveraged across multiple components would be deduplicated and shared across the consuming components at constructed stylesheets.

Not sure where the line would be drawn and whether this sort of approach could actually overcome the full build or runtime approaches you've outlined here, but the if a suite of components all leverage similar parts of the Tailwinds system then it would seem like you could take some work out of the browser this way. Right?

Collapse
 
43081j profile image
James Garbutt

The run-time solution (twind) in this case would populate a single shared stylesheet, unless you choose to create one per module, then each component would have duplicate rules potentially but their own sheets.

The build-time solution (my other post) would populate a sheet per module, stripping styles based on what was used in that individual module.

i agree it'd be nice to have a middle ground whether you do it at run-time or build-time: a few shared stylesheets multiple components can depend on. but tailwind itself doesn't organise rules as well as your example afaik, so it'd be difficult i think

Collapse
 
quarkus profile image
Markus Hesper

Although it still feels a lot like an anti pattern (mixing shadow dom and runtime utility classes) we felt the need to do this as well.

Our solution to the runtime dep was to adopt a global (inline) stylesheets (i.e. tailwind) into a components own constructable stylesheets. Doing so, all global utility classes become available in the components shadow dom.
Component css files (imported via roll-up, build using postcss/tailwind) are adopted into the same constructable stylesheet.

Although being debatable this idea ended up in our component stack that we use in production for a couple of years now.
We did not perf test this feature very good and we'll have a look into this in the next weeks but the status quo is documented here.

github.com/webtides/element-js/blo...

And code:
github.com/webtides/element-js/blo...

Would be great to hear some thoughts on this topic in general.

Is this anti pattern or not? Which one is the way to go (build vs. runtime) or should we all rely more on css properties for "style sharing / interfacing" !?

Collapse
 
43081j profile image
James Garbutt

I think both paths are as valid as each other. I'd personally prefer a build-time solution as I have a very static set of styles I use, so if i know about them at build-time i may as well do the heavy lifting there. There are use cases where you want to dynamically generate/use these styles though, in which case a run time solution is nicer.

Your solution is interesting, pretty much is where i ended up when trying to throw together one using existing tooling. I also made a rollup plugin which replaces css templates inline with their processed equivalents and strips unused css at the same time. Both seem to be the way to go if wanting this at build time.

I think the ultimate build-time solution would be that CSS modules land in browsers, so we can just process a regular css file and import it natively into our modules.

Collapse
 
ivan_jrmc profile image
Ivan Jeremic

Does this runtime method still work? I tried it now a few times but the styles didn't change... any ideas?

Collapse
 
gorvgoyl profile image
Gourav

mentioned solution with Twind is outdated, here's the latest one with Tailwind v3 support in shadow DOM: gourav.io/blog/tailwind-in-shadow-dom

Collapse
 
jbruni profile image
J Bruni

I am using Twind at build time. BTW, this article headed me to Twind (thanks!), and I ended up with this: github.com/tailwindlabs/tailwindcs...