Stencil + Tailwind

Tailwind CSS has taken the CSS world by storm, and with good reason. Tailwind is a CSS framework that, at its core, supplies utility classes in an effort to make styling much easier. Tailwind offers a lot of additional features as well and has become especially popular in the world of design systems.

While there isn’t one “correct” way to integrate Tailwind into a Stencil project, many members of the community have come up with their own solutions that are suited for their individual use cases. In this post, we’d like to highlight some of those community solutions and provide a holistic view of different ways you can use Tailwind with Stencil.

Using a Stencil Tailwind plugin

The first way you can integrate Tailwind into your Stencil project is with the stencil-tailwind-plugin created by @Poimen. Using this plugin requires very little setup and can get you up and running with Tailwind fairly quickly. In order to use this plugin, we’ll first have to install some dependencies:

npm i tailwindcss stencil-tailwind-plugin typescript --save-dev

Next, we’ll need to configure the plugin in our stencil.config.ts file by importing tailwind and tailwindHMR from stencil-tailwind-plugin and adding them to our plugins array.

import { Config } from '@stencil/core';
import tailwind, { tailwindHMR } from 'stencil-tailwind-plugin';

export const config: Config = {
  namespace: ‘my-project’,
  plugins: [
    tailwind(),
    tailwindHMR(),
  ],
  // …
}

NOTE: These plugins accept a lot of different configuration options for additional customization. You can read more about these options in the plugin’s readme

And those two steps are all we need! With the plugin installed and configured, we can now start using Tailwind utility classes in our Stencil components. Let’s take a look at an example component:

import { Component, h } from '@stencil/core';

@Component({
  tag: 'tailwind-component',
  shadow: true,
})
export class TailwindComponent {
  render() {
    return (
      <div class="bg-indigo-500 p-6 rounded-md flex justify-center">
        <h1 class="text-white font-sans">This is a Stencil component using Tailwind</h1>
      </div>
    );
  }
}

And here is the result:

You may notice that this plugin allows you to use Tailwind with shadow DOM components right out of the box, which would otherwise require some additional work. This setup is just meant to get you started. There’s so much more you can do with this plugin. I encourage you to check out the stencil-tailwind-plugin readme and see everything the plugin has to offer. For a more developed example, checkout @Poimen’s example repo that makes use of the plugin.

Using Global Styles and the Tailwind CLI

The Stencil Tailwind plugin is great, but some folks may not want to rely on a third party plugin for their Tailwind integration. If that sounds like you, then you may be more interested in this next solution from @snaptopixel. The general idea behind this approach is to point Stencil’s globalStyle to Tailwind’s compiled output.

Before we get started with this implementation, we’ll need to install Tailwind as a dev dependency:

npm i tailwindcss --save-dev

For this solution, we’re going to start in the stencil.config.ts file. This solution leverages the www output target, so we’ll need to ensure that it’s included in the outputTargets array. We’ll also need to specify a global stylesheet for the project by giving globalStyle a value of 'www/tailwind.css'. This tailwind.css file doesn’t exist yet, but will be generated shortly. All in all, our stencil.config.ts file should look something like this:

export const config: Config = {
  namespace: 'my-project',
  globalStyle: 'www/tailwind.css',
  outputTargets: [
    {
      type: 'www',
      serviceWorker: null,
    },
    // …
  ],
};

Now that our Stencil project is configured, let’s set up our Tailwind configuration. We can create a tailwind.config.js file by running:

npx tailwindcss init

There are A LOT of things we can do in this file, but for our basic setup we just need to specify the files we want Tailwind to scan via the “content” field. In our case, we’ll scan all files ending in “ts”, “tsx” and “html”:

module.exports = {
  content: ['./src/**/*.{ts,tsx,html}'],
  // …
}

NOTE: You can learn more about all the ways you can configure Tailwind on the Tailwind configuration docs page.

The next step in this process is creating a CSS file that contains all of the Tailwind directives. These directives inject Tailwind’s core styles into our project. Under the src folder of your Stencil project, create a file called directives.css (or any name of your choosing) and add the Tailwind directives like so:

@tailwind base;
@tailwind components;
@tailwind utilities;

Now comes the crux of this solution. We’re going to generate that tailwind.css file that we previously defined for our global styles. We do that with the Tailwind CLI:

npx tailwindcss -i ./src/directives.css -o ./www/tailwind.css --watch

This command scans our project for Tailwind classes and builds a CSS file called tailwind.css in the www directory. We use the --watch flag to watch for any changes and rebuild when changes occur. We can also include the --minify flag to minify this file for production.

With our global styles generated, now we need to include a link to that stylesheet in our index.html file.

<link rel="stylesheet" href="build/stencil-tailwind.css" />

The name of your stylesheet will be the same as the name of your Stencil project.

Now, in a separate terminal window, we can start running our Stencil project with npm start.

NOTE: You can bake all of this into one command by using npm-run-all. It is beyond the scope of this post, but feel free to check out @snaptopixel’s example repo to see how you can set this up.

Again, we can create that component from the first solution to see Tailwind in action.

import { Component, h } from '@stencil/core';

@Component({
  tag: 'tailwind-component',
})
export class TailwindComponent {
  render() {
    return (
      <div class="bg-indigo-500 p-6 rounded-md flex justify-center">
        <h1 class="text-white font-sans">This is a Stencil component using Tailwind</h1>
      </div>
    );
  }
}

You’ll notice one small difference in this version of the component and that is the fact that we are not using the shadow DOM here. In this implementation, the Tailwind styles are coming from an external global stylesheet and thus are not able to pass through the shadow boundary. If using the shadow DOM is crucial for your component, @snaptopixel has a clever way to accomplish that.

In order to add Tailwind styles, while still using the shadow DOM, we can create a utility wrapper component that looks something like this:

import { getAssetPath, h, Host } from '@stencil/core'

export const StyledHost: typeof Host = (attrs, children) => {
  return (
    <Host {...attrs}>
      <link rel='stylesheet' href={getAssetPath('your-tailwind-styles-file.css')} />
      {children}
    </Host>
  )
}

This component is meant to include a link to the generated Tailwind stylesheet right alongside the component we want to style, so they are both included within the shadow boundary. In practice, we can use this StyledHost component like so:

import { Component, h } from '@stencil/core'

import { StyledHost } from '../helpers' // this may vary depending on where your ‘StyledHost’ component lives

@Component({
  tag: 'styled-component',
  shadow: true,
})
export class StyledComponent {
  render() {
    return (
      <StyledHost>
        <div class="bg-indigo-500 p-6 rounded-md flex justify-center">
            <h1 class="text-white font-sans">This is a Stencil component using Tailwind</h1>
        </div>
      </StyledHost>
    )
  }
}

For a full example of how to use this technique, checkout @snaptopixel’s example repo on Github.

Using PostCSS

The last integration that we’ll discuss in this article comes from @retinafunk and relies on the Stencil PostCSS plugin. To get started, let’s install our dependencies.

npm install tailwindcss @stencil/postcss autoprefixer postcss-import --save-dev

Next, we’ll want to create our Tailwind config file with npx tailwindcss init and specify the content in tailwind.config.js like so:

module.exports = {
  content: ['./src/**/*.{ts,tsx,html}'],
  theme: {
    extend: {},
  },
  plugins: [],
}

With our tailwind.config.js file set up, we can now import and add our PostCSS plugins to our stencil.config.ts file:

import { Config } from '@stencil/core';
import {postcss} from '@stencil/postcss';
import autoprefixer from 'autoprefixer';

export const config: Config = {
  // …
  plugins : [
    postcss({
      plugins: [
        require("postcss-import"),
        require("tailwindcss")("./tailwind.config.js"),
        autoprefixer()
      ]
    })
  ],
}

At this point now, we can create a Stencil component that uses the Tailwind utility classes. We’ll use the same component we did for the previous examples.

import { Component, h } from '@stencil/core';

@Component({
  tag: 'tailwind-component',
  styleUrl: 'tailwind-component.css',
  shadow: true,
})
export class TailwindComponent  {
  render() {
    return (
      <div class="bg-indigo-500 p-6 rounded-md flex justify-center">
        <h1 class="text-white font-sans">This is a Stencil component using Tailwind</h1>
      </div>
    );
  }
}

You may notice that this component uses the shadow DOM which means we’ll have to include the Tailwind styles within the component. We can do that by adding the Tailwind directives to the tailwind-component.css file:

@tailwind base;
@tailwind components;
@tailwind utilities;

And with that we can now run npm start and see our Stencil Tailwind component in action.

This works well, and we could stop here, but one thing you may notice is that we’re including a lot of unused styles in our component. This isn’t a big deal on its own, but as we add more components to our app, each with their own bloated styles, the bundle size can become quite large.

Fortunately, we can reduce the bundle size quite a bit by using the PurgeCSS and CSSNANO plugins for PostCSS. Let’s install them as dev dependencies:

npm i @fullhuman/postcss-purgecss cssnano --save-dev

With these plugins installed, we can now go to our stencil.config.ts file and configure our PurgeCSS:

const purgecss = require("@fullhuman/postcss-purgecss")({
  content: ["./src/**/*.tsx", "./src/**/*.css", "./src/index.html"],
  defaultExtractor: content => content.match(/[A-Za-z0-9-_:/]+/g) || []
});

The “content” field specifies the files we want to analyze and the “defaultExtractor” field allows us to provide a regular expression that removes additional CSS that might not have otherwise been extracted.

Finally, we’ll add our purgecss and cssnano to our PostCSS plugins array in stencil.config.ts:

"plugins" : [
  postcss({
    plugins: [
      require("postcss-import"),
      require("tailwindcss")("./tailwind.config.js"),
      autoprefixer(),
      ...(process.env.NODE_ENV === "production"
          ? [purgecss, require("cssnano")]
          : [])
      ]
  })
],
// …

Note that we’re only minifying our CSS for production builds. Now our styles for each component will be much smaller.

To see all of this come together, checkout the full github repo demonstrating how @retinafunk configures a project.

Conclusion

Long as this post may be, it really only scratches the surface of strategies for integrating Stencil and Tailwind. If you’re looking for a way to use Tailwind in your Stencil projects, feel free to give all three of these strategies a try and see what works best for your use case. Don’t be afraid to combine some of these techniques and add on top of them to create a solution that works best for you.

Again, a huge thanks goes out to @Poimen, @snaptopixel, and @retinafunk for not only creating these solutions, but also for sharing them with the community. All three of these solutions came from discussions in the Stencil Slack community. If you’re interested in being a part of these conversations and many others around Stencil, please come join us on Slack. The community is full of brilliant Stencil developers eager to help and learn together.

Sign up for the Ionic Newsletter to get the latest news and updates!