Nx, Angular, and Tailwind CSS logos

January 28, 2022

Originally published at blog.nrwl.ioExternal link icon

Set up Tailwind CSS with Angular in an Nx workspace

Tailwind CSSExternal link icon is a utility-first CSS framework packed with a lot of good functionality out of the box while providing a high level of customization. It has gained a lot of attention since it came out and it’s a good option when it comes to styling our applications.

In this blog post, we are going to see how we can use Tailwind CSS with AngularExternal link icon in an NxExternal link icon monorepo. We are going to be looking at different scenarios and how to approach them.

Let’s get started!

Please note that this blog post is not meant to be a Tailwind CSS or Angular tutorial. The purpose is to show how to use them together and how Nx can help to improve the developer experience and expand the native Angular capabilities when working with Tailwind CSS.

What are we going to build?

The final result of what we are going to be building can be found in this Github repository: https://github.com/leosvelperez/angular-tailwind-nxExternal link icon.

We are going to create 2 simple applications with the following layout:

Applications mockup

We’ll start by creating one application with the required markup and Tailwind CSS utility classes to achieve the above layout. Then, we’re going to leverage Nx’s library support and extract some common UI components into 2 different shared libraries:

  • a regular non-buildable library containing the header,
  • a buildable library containing the card elements.

At that point, we’ll create the second application using the components exposed by those shared libraries. Finally, we’ll extract the button elements to a publishable library and adjust both applications to use them.

The idea is to show how different applications can still use the same components and have them styled differently using Tailwind CSS. Both applications in this blog post will share the same layout, but the approach explained here would apply to applications with different layouts sharing the same UI components.

Discussing the different types of libraries and the motivation to use them goes beyond the scope of this blog post. We’ll use the three different types just to showcase how to use Tailwind CSS with each of them. To know more about them, please check https://nx.dev/structure/creating-librariesExternal link icon and https://nx.dev/structure/buildable-and-publishable-librariesExternal link icon.

Setting up the Nx workspace

First things first! We start by creating a new Nx workspace where our applications and libraries will be located. To do that, we can run:

❯ npx [email protected] angular-tailwind-nx --pm=yarn
✔ What to create in the new workspace · angular
✔ Application name                    · app1
✔ Default stylesheet format           · css
✔ Use Nx Cloud? (It's free and doesn't require registration.) · No

Passing the --packageManager (or --pm) flag allows us to change the package manager. If not passed, it defaults to npm.

The above command creates a workspace called angular-tailwind-nx and asks us a few questions to help us set up the workspace. We chose the angular preset, provided app1 for the initial Angular application name, chose css as the stylesheet to use, and this time chose not to use Nx CloudExternal link icon but feel free to opt-in to use the Nx Cloud free tier to benefit from distributing the computation caching of your projects.

Any of the stylesheet options can be used. Also, using Nx Cloud or not doesn’t affect setting up Tailwind CSS.

Now that we have a workspace with an Angular application ready to be used, let’s start adding some Tailwind CSS magic!

Adding Tailwind CSS

Angular added native support for building applications using Tailwind CSS a while ago. Still, we need to set it up in the workspace, and to do so, we can use the @nrwl/angular:setup-tailwind generator by simply running:

npx nx generate @nrwl/angular:setup-tailwind app1

The above command will do a few things for us:

  • It will check if tailwindcss is already installed and if not installed, it will install the necessary packages (tailwindcss, postcss and autoprefixer)
  • It will create a tailwind.config.js file in the project root with the default configuration to get started (specific to the installed version)
  • It will recognize the project type and for applications, it will update the application styles entry point file located at apps/app1/src/styles.css by including the Tailwind CSS base styles

The above generator supports Tailwind CSS v2 and v3.

Let’s take a look at the generated apps/app1/tailwind.config.js file:

const { createGlobPatternsForDependencies } = require('@nrwl/angular/tailwind');
const { join } = require('path');

module.exports = {
  content: [
    join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'),
    ...createGlobPatternsForDependencies(__dirname),
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};

We can see the content property is configured to scan for all the HTML and TypeScript files within our application and besides that, there’s also a call to a function called createGlobPatternsForDependencies. This is a pretty handy function that will identify the dependencies of the application and return the glob patterns for them. This ensures that Tailwind CSS utility classes that are used in the application’s dependencies are also taken into account and included in the final CSS of the application.

Instead of using the createGlobPatternsForDependencies, we could just add the libs/**/* glob pattern that captures all libraries, but that could lead to bundling more CSS than needed in monorepos with multiple applications. This would happen because all applications would be scanning all libraries, regardless of whether they are dependencies or not. Such a glob pattern could also lead to scaling issues in large monorepos due to the amount of libraries to scan.

We can also see that the generator updated the apps/app1/src/styles.css file with the Tailwind CSS bases styles:

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

And that’s all we need. We can now go ahead and add our custom theme and layout to achieve the desired design.

Adding a custom theme and the application markup

First, we are going to update the theme section of the generated apps/app1/tailwind.config.js. We are going to overwrite the Tailwind CSS default theme and provide the custom palette of colors and spacing of our theme to be used throughout the application:

...

module.exports = {
  ...
  theme: {
    colors: {
      primary: {
        light: '#5eead4',
        DEFAULT: '#14b8a6',
        dark: '#0f766e',
      },
      secondary: {
        light: '#bae6fd',
        DEFAULT: '#0ea5e9',
        dark: '#0369a1',
      },
      white: '#ffffff',
      black: '#000000',
    },
    spacing: {
      sm: '0.5rem',
      md: '1rem',
      lg: '1.5rem',
      xl: '2rem',
    },
  },
  ...
};

Next, we update the apps/app1/src/app/app.component.html file with the required markup and several Tailwind CSS utility classes to style the application with the look & feel we are looking for:

<div class="font-mono">
  <header class="px-xl py-md bg-primary-light text-xl font-bold shadow-md">Angular + Tailwind CSS + Nx</header>

  <main class="max-w-xl md:max-w-2xl lg:max-w-6xl mx-auto py-xl px-md md:px-xl grid grid-cols-1 gap-md md:grid-cols-2 lg:grid-cols-3">
    <div class="flex flex-col p-lg bg-secondary-light shadow-md hover:shadow-lg">
      <div class="pb-md text-lg font-bold">Angular</div>
      <p class="mb-xl flex-1">
        Angular is an application design framework and development platform for creating efficient and sophisticated single-page apps.
      </p>
      <a
        class="py-sm px-md bg-primary-dark hover:bg-primary text-white flex self-end"
        href="https://angular.io/"
        target="_blank"
        rel="noopener noreferrer"
      >
        Show me!
      </a>
    </div>

    <div class="flex flex-col p-lg bg-secondary-light shadow-md hover:shadow-lg">
      <div class="pb-md text-lg font-bold">Tailwind CSS</div>
      <p class="mb-xl flex-1">
        Tailwind CSS is a utility-first CSS framework packed with classes like flex, pt-4, text-center and rotate-90 that can be composed to build any design, directly in your markup.
      </p>
      <a
        class="py-sm px-md bg-primary-dark hover:bg-primary text-white flex self-end"
        href="https://tailwindcss.com/"
        target="_blank"
        rel="noopener noreferrer"
      >
        Show me!
      </a>
    </div>

    <div class="flex flex-col p-lg bg-secondary-light shadow-md hover:shadow-lg">
      <div class="pb-md text-lg font-bold">Nx</div>
      <p class="mb-xl flex-1">
        Nx is a smart, fast and extensible build system with first class monorepo support and powerful integrations.
      </p>
      <a
        class="py-sm px-md bg-primary-dark hover:bg-primary text-white flex self-end"
        href="https://nx.dev/"
        target="_blank"
        rel="noopener noreferrer"
      >
        Show me!
      </a>
    </div>
  </main>
</div>

With all set, let’s see it in action by running:

npx nx run app1:serve

Visiting https://localhost:4200External link icon in your browser should show the application looking like the following screenshot:

Application 1 screenshot

That’s it! We have successfully created our application to fulfill the requirements we had. Next, we are going to start extracting pieces of the UI into shared libraries to reuse them with the second application.

Tailwind CSS and Angular libraries in an Nx workspace

Before extracting our UI components into libraries, we need to take a step back and make sure we understand how Tailwind CSS works and the implications of the different types of libraries in an Nx workspace.

From Tailwind CSS docsExternal link icon:

Tailwind CSS works by scanning all of your HTML files, JavaScript components, and any other templates for class names, generating the corresponding styles and then writing them to a static CSS file.

Any project can use the Tailwind CSS CLIExternal link icon or PostCSSExternal link icon with the tailwindcss plugin to scan the relevant files in the project and collect the usage of the Tailwind CSS utility classes, functions, and custom CSS directives (custom CSS at-rulesExternal link icon). With that information, the final CSS styles are generated.

Angular uses PostCSS to support Tailwind CSS. As we saw in a previous section, with the help of an Nx generator, it’s pretty straightforward to configure a Tailwind CSS for applications. Libraries can also be easily configured, but there are some nuances regarding how they are processed and whether they need to be configured or not.

In an Nx workspace, a regular library (non-buildable and non-publishable) is just a slice of an application that is only built as part of the build process of an application that consumes it. Because of that, as long as the application that consumes it has Tailwind CSS configured, the library code will be processed as expected even though the library itself doesn’t have a Tailwind CSS configuration. In fact, adding a tailwind.config.js file to the library won’t have any effect whatsoever (it’ll be ignored) because the library is never built on its own.

On the other hand, buildable and publishable libraries are meant to be built on their own and their compiled output to be shared with the consumers. Therefore, they need to be able to process any Tailwind CSS directive or function (e.g. @apply, theme()) when they are built. If no Tailwind CSS directive or function is used, then the configuration is not needed.

Providing the configuration to a buildable or publishable library that doesn’t need it adds some unnecessary overhead to the build process. The executor will still load the configuration and the PostCSS plugin will process the stylesheets unnecessarily.

How does this work?

Tailwind CSS produces the relevant CSS code where the following directives and functions are used:

  • @tailwind
  • @apply
  • theme()
  • screen()

When the PostCSS plugin processes a file containing these, it processes them and produces the corresponding CSS code based on the provided configuration. If none of the above is used in a buildable or publishable library, no CSS is generated, and therefore, no configuration is needed. The actual CSS will be generated when building the application consuming those libraries.

To know more about the above directives and functions you can take a look at https://tailwindcss.com/docs/functions-and-directivesExternal link icon.

But we do use Tailwind CSS utility classes in the libraries and CSS needs to be generated for them. So, how is the CSS generated for those classes if the libraries are not configured?

If we recall from a previous section, in our application’s tailwind.config.js file, we have the following:

...

module.exports = {
  content: [
    join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'),
    ...createGlobPatternsForDependencies(__dirname),
  ],
  ...
};

The content property of the configuration tells Tailwind CSS where to look for usages of utility classes. When the PostCSS plugin finds a file using the @tailwind directive, it will collect all the utility classes for the layer specified by the directive in the files matching the glob patterns set in the content property of the configuration, and it will produce the CSS replacing the directive. It’s worth noting that the PostCSS plugin only scans the files collecting the utility classes that are used, it doesn’t process them. Only the file containing the @tailwind directive is updated with the resulting CSS.

Since we have our application configured to scan the relevant files within itself and also within its dependencies, the utility classes used in the libraries that are dependencies of the application will be picked up correctly and the CSS will be generated for them.

It’s important to note that the support for Tailwind CSS for Angular libraries is only available using the @nrwl/angular:ng-packagr-lite executor for buildable libraries and the @nrwl/angular:package executor for publishable libraries.

Below is a small decision tree to check whether a Tailwind CSS configuration is needed for your library in an Nx workspace:

Decision tree for the need of Tailwind CSS configuration in Angular libraries

Extracting the header into a library

Our application is looking good. At the same time, there’s a great opportunity to reuse some of its components in another application. Therefore, we are going to extract the shared components into several shared libraries.

The applications of this blog post are very simple and it’s probably not worthy to extract the components into multiple libraries. We are doing it for illustrative purposes to cover the different scenarios we might find in real-life applications.

We’ll start by extracting the header of the application into a reusable component and placing it into a library. To do so, we start by creating a new Angular library in our workspace by running:

npx nx generate @nrwl/angular:lib lib1

Next, we create the component for the header in the library we just generated and we export it so it can be imported by consumers:

npx nx generate @nrwl/angular:component header --project=lib1 --export

Add the markup for the header to the libs/lib1/src/lib/header/header.component.html:

<header class="px-xl py-md bg-primary-light text-xl font-bold shadow-md">
  Angular + Tailwind CSS + Nx
</header>

Import Lib1Module into our application’s AppModule:

...
import { Lib1Module } from '@angular-tailwind-nx/lib1';

@NgModule({
  ...
  imports: [BrowserModule, Lib1Module],
  ...
})
export class AppModule {}

And finally, replace the existing markup for the header in the apps/app1/src/app/app.component.html file with the newly created header component and leaving the rest of the file as-is:

<div class="font-mono">
  <angular-tailwind-nx-header></angular-tailwind-nx-header>

  ...
</div>

At this point, if we serve again the application, everything should still be working the same way as before. We successfully extracted the header into a shared library and made it reusable.

Extracting the card into a buildable library

Similar to the previous section we are going to start by creating a new library to add the card component to. The only difference is that this library is going to be buildable.

If you are not aware of what buildable libraries are or what problem do they intend to solve, please make sure to read https://nx.dev/ci/incremental-buildsExternal link icon and particularly pay attention to when should they be usedExternal link icon.

Run the following command to generate the library:

npx nx generate @nrwl/angular:lib lib2 --buildable

Next, we configure Tailwind CSS for it:

npx nx generate @nrwl/angular:setup-tailwind lib2

As explained in a previous section when we did the same for the application, the above command will install any required dependencies if needed, create the tailwind.config.js file and in the specific case of libraries, it will also add the tailwindConfig property to the build target of the project configuration.

Angular applications, buildable and publishable libraries can be created with Tailwind CSS support with a single command. We’ll see that in the upcoming section. The @nrwl/angular:setup-tailwind generator is used to add Tailwind CSS support to existing projects.

Then, we create the card component:

npx nx generate @nrwl/angular:component card --project=lib2 --export

We add the component to the library entry point located in libs/lib2/src/index.ts:

...
export * from './lib/card/card.component';

Then, we update the card component files to provide the desired functionality:

// card.component.ts
import { Component, Input } from '@angular/core';

@Component({
  selector: 'angular-tailwind-nx-card',
  templateUrl: './card.component.html',
  styleUrls: ['./card.component.css'],
})
export class CardComponent {
  @Input() title?: string;
  @Input() url?: string;
}
<!-- card.component.html -->
<div class="h-full flex flex-col p-lg bg-secondary-light shadow-md hover:shadow-lg">
  <div class="pb-md text-lg font-bold">{{ title }}</div>
  <p class="mb-xl flex-1">
    <ng-content></ng-content>
  </p>
  <a
    class="py-sm px-md bg-primary-dark hover:bg-primary text-white flex self-end"
    href="{{ url }}"
    target="_blank"
    rel="noopener noreferrer"
  >
    Show me!
  </a>
</div>

Import Lib2Module into our application’s AppModule:

...
import { Lib2Module } from '@angular-tailwind-nx/lib2';

@NgModule({
  ...
  imports: [BrowserModule, Lib1Module, Lib2Module],
  ...
})
export class AppModule {}

And finally, replace the existing markup for the cards in the apps/app1/src/app/app.component.html file with the newly created card component:

<div class="font-mono">
  <angular-tailwind-nx-header></angular-tailwind-nx-header>

  <main class="max-w-xl md:max-w-2xl lg:max-w-6xl mx-auto py-xl px-md md:px-xl grid grid-cols-1 gap-md md:grid-cols-2 lg:grid-cols-3">
    <angular-tailwind-nx-card title="Angular" url="https://angular.io/">
      Angular is an application design framework and development platform for creating efficient and sophisticated single-page apps.
    </angular-tailwind-nx-card>

    <angular-tailwind-nx-card title="Tailwind CSS" url="https://tailwindcss.com/">
      Tailwind CSS is a utility-first CSS framework packed with classes like flex, pt-4, text-center and rotate-90 that can be composed to build any design, directly in your markup.
    </angular-tailwind-nx-card>

    <angular-tailwind-nx-card title="Nx" url="https://nx.dev/">
      Nx is a smart, fast and extensible build system with first class monorepo support and powerful integrations.
    </angular-tailwind-nx-card>
  </main>
</div>

With that in place, we can serve the application and it should be working exactly as before, but our application is still not fully set up to consume the library build output. As it stands right now, when the application that’s consuming it is built, the library will be built together with it and its files will be processed as part of the application build pipeline.

To finish the buildable library setup, we can follow the instructions in https://nx.dev/ci/setup-incremental-builds-angular#adjust-the-app-executorExternal link icon. We need to install the @nrwl/web package, change the application build target executor to @nrwl/angular:webpack-browser, and change the application serve target executor to @nrwl/web:file-server:

yarn add -D @nrwl/[email protected]
{
  ...
  "targets": {
    "build": {
      "executor": "@nrwl/angular:webpack-browser",
      ...
    },
    "serve": {
      "executor": "@nrwl/web:file-server",
      "configurations": {
        "production": {
          "buildTarget": "app1:build:production"
        },
        "development": {
          "buildTarget": "app1:build:development"
        }
      },
      "defaultConfiguration": "development"
    },
    ...
  },
  ...
}

You can now go ahead and serve the application to check everything is working as expected. You should see the buildable library being built on its own before the application is built and served.

The @nrwl/web:file-server executor uses the http-serverExternal link icon package to serve the built artifacts of our application. This is a lightweight HTTP server and it doesn’t do things like live-reload or HMR. Be sure to refresh your browser while making changes to see them reflected.

Using Tailwind CSS directives and functions in buildable libraries

Our application is consuming a buildable library and still working as intended, but if we think about it, we didn’t configure our theme in the library’s tailwind.config.js file. So, how is it still working?

If we go back to the decision tree shared in a previous section, we’ll see that a buildable library only needs a Tailwind CSS configuration if we use a Tailwind CSS directive or function. As of right now, our library is not using any. We are just using some utility classes and those are processed correctly as part of the application build. You could go ahead and delete the tailwind.config.js file from the library and check that everything still works the same (if you do, please make sure to restore it before we continue).

Next, we are going to refactor our newly created card component to make use of some of these directives and functions and see the implications.

Update the card component files content as shown below:

/* card.component.css */
.card {
  @apply h-full flex flex-col p-lg shadow-md hover:shadow-lg;

  background-color: theme('colors.secondary.light');
}

.card-title {
  @apply text-lg font-bold;

  padding-bottom: theme('spacing.md');
}

.card-content {
  @apply mb-xl flex-1;
}
<!-- card.component.html -->
<div class="card">
  <div class="card-title">{{ title }}</div>
  <p class="card-content">
    <ng-content></ng-content>
  </p>
  ...
</div>

We created some CSS classes where we are applying the same styles we had in the component template. We are applying those styles by using a combination of the @apply directive and the theme function.

Please note we are not recommending creating CSS classes for scenarios like the above. The card component is already a reusable component and there’s no need to extract CSS classes to style it. We are only extracting these classes to showcase how Tailwind CSS directives and functions can be correctly processed in buildable libraries. Make sure to check out the Tailwind CSS recommendations for reusing stylesExternal link icon.

If we now serve our application (or build the library), we’ll find ourselves with the following error:

------------------------------------------------------------------------------
Building entry point '@angular-tailwind-nx/lib2'
------------------------------------------------------------------------------
/angular-tailwind-nx/libs/lib2/src/lib/card/card.component.css:2:3: The `p-lg` class does not exist. If `p-lg` is a custom class, make sure it is defined within a `@layer` directive.

This is to be expected. The library build is failing because now we are using some Tailwind CSS directives and functions, and therefore, those directives and functions are being processed within the library context. Since we haven’t touched the tailwind.config.js file, Tailwind CSS doesn’t know about our custom theme.

To solve the issue, we need to configure the library to be aware of our custom theme so it can process the library’s files correctly. Let’s update the theme property of the libs/lib2/tailwind.config.js file to match our application theme:

...
  theme: {
    colors: {
      primary: {
        light: '#5eead4',
        DEFAULT: '#14b8a6',
        dark: '#0f766e',
      },
      secondary: {
        light: '#bae6fd',
        DEFAULT: '#0ea5e9',
        dark: '#0369a1',
      },
      white: '#ffffff',
      black: '#000000',
    },
    spacing: {
      sm: '0.5rem',
      md: '1rem',
      lg: '1.5rem',
      xl: '2rem',
    },
  },
...

Now, we should see our application working correctly if we serve it again.

Sharing the Tailwind CSS configuration between the application and the buildable library

Though we have successfully solved the issue and our workspace now has a library that can be built on its own and be cached, the experience is not great. We had to duplicate the application configuration in the buildable library. This introduces a maintainability concern and it will most likely be a cause for errors due to having to maintain them in sync. Also, we only have one buildable library in this small example, but imagine a real-life scenario where hundreds of these libraries need to be kept in sync. A nightmare!

Well, no need to fret!

If we think about it, the same reasoning behind creating shared libraries applies to this. We just need to share the Tailwind CSS configuration. To do so, we have a couple of options:

  • Create a shared file containing and exporting the theme so it can be imported by every project’s tailwind.config.js file.
  • Create a Tailwind CSS presetExternal link icon to expose a base configuration for your projects.

The last option is the better one. We can take advantage of the Tailwind CSS built-in support for defining a base configuration to be reused across different projects. The first option is almost the same, with the difference that we have to manually handle merging the configurations.

We’ll go ahead and create a Tailwind CSS preset and we’ll then use it in our projects. Start by creating a tailwind.config.js file in the root of the workspace with the following content:

module.exports = {
  theme: {
    colors: {
      primary: {
        light: '#5eead4',
        DEFAULT: '#14b8a6',
        dark: '#0f766e',
      },
      secondary: {
        light: '#bae6fd',
        DEFAULT: '#0ea5e9',
        dark: '#0369a1',
      },
      white: '#ffffff',
      black: '#000000',
    },
    spacing: {
      sm: '0.5rem',
      md: '1rem',
      lg: '1.5rem',
      xl: '2rem',
    },
  },
  plugins: [],
};

We just added the configuration that is common to our projects to use as a base in each of them. Next, we need to add the preset configuration to each project.

Update both apps/app1/tailwind.config.js and libs/lib2/tailwind.config.js files to match the following:

const { createGlobPatternsForDependencies } = require('@nrwl/angular/tailwind');
const { join } = require('path');
const sharedTailwindConfig = require('../../tailwind.config');

module.exports = {
  presets: [sharedTailwindConfig],
  content: [
    join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'),
    ...createGlobPatternsForDependencies(__dirname),
  ],
};

Notice how we added the preset and removed almost all the configuration because it’s already defined in the preset.

That’s all it takes. You can go ahead and serve the application (or refresh the browser if you are already serving it) to check everything is running correctly.

Sharing the Tailwind CSS preset in a library

We now only have to maintain our theme in a single place as opposed to keeping in sync the configuration of all the different projects. But we can still improve the experience. As it stands, if you now make a change on the tailwind.config.js file located at the root of the workspace (our preset), the file server doesn’t pick up the change and therefore, it doesn’t rebuild the affected projects.

This happens because the file server is watching for changes under the apps and libs folders. The preset configuration is not under those directories, it’s in the root of the workspace.

It would be better if we place the preset configuration in a small shared library. By doing that, we not only solve the issue regarding detecting changes on it, but we also make its library appear on the Nx project graphExternal link icon, and with that, we benefit from all the goodies associated with the project graph (affected commands, enforcing module boundaries constraints, etc.).

This library is only going to contain the tailwind.config.js file and no targets in the project configuration. There’s no generator among the Nx core plugins that generate such an empty library. We could use one of the library generators and remove some content, but let’s create it manually.

Start by creating a new folder libs/tailwind-preset and moving the tailwind.config.js file we created in the previous section at the root of the workspace to that folder.

Next, add the project to the angular.json:

{
  "version": 2,
  "projects": {
    ...
    "tailwind-preset": "libs/tailwind-preset"
  }
}

Create the configuration for the project in libs/tailwind-preset/project.json:

{
  "projectType": "library",
  "root": "libs/tailwind-preset",
  "sourceRoot": "libs/tailwind-preset",
  "targets": {},
  "tags": []
}

And finally, adjust both apps/app1/tailwind.config.js and libs/lib2/tailwind.config.js files to import the preset from the correct location:

...
const sharedTailwindConfig = require('../../libs/tailwind-preset/tailwind.config');
...

Once again, if we serve our application everything should still be working as expected, but now our file server will pick up the changes made to the Tailwind CSS preset configuration.

Also, if we visualize the workspace projects we’ll see how app1 and lib2 now have a dependency on tailwind-preset:

Project graph showing dependencies between the projects in the workspace

Creating the second application

We are now at a stage where we can develop our second application without having to duplicate the common functionality. So, before going ahead and distributing our buttons in a publishable library, let’s first create the second application to see how we can reuse what we have been putting into libraries.

There’s one important thing to note though, this new application will have a different theme.

Generate the application by running the following command:

npx nx generate @nrwl/angular:app app2 --addTailwind --style=css --routing=false

The above command will generate the new application and it will configure Tailwind CSS as well. Using the --addTailwind flag will instruct the application generator to automatically run the @nrwl/angular:setup-tailwind generator when creating a new application.

The --addTailwind flag is available in both @nrwl/angular:app and @nrwl/angular:lib generators.

Let’s now update the application to use the shared components and achieve the layout we are after. Start by updating the apps/app2/src/app/app.module.ts to import Lib1Module and Lib2Module:

...
import { Lib1Module } from '@angular-tailwind-nx/lib1';
import { Lib2Module } from '@angular-tailwind-nx/lib2';

@NgModule({
  ...
  imports: [BrowserModule, Lib1Module, Lib2Module],
  ...
})
export class AppModule {}

Next, update the apps/app2/src/app/app.component.html file with the required markup and Tailwind CSS utility classes to achieve our application’s layout and using the component exported by the shared libraries we previously created:

<div class="font-mono">
  <angular-tailwind-nx-header></angular-tailwind-nx-header>

  <main class="max-w-xl md:max-w-2xl lg:max-w-6xl mx-auto py-xl px-md md:px-xl grid grid-cols-1 gap-md md:grid-cols-2 lg:grid-cols-3">
    <angular-tailwind-nx-card title="Angular" url="https://angular.io/">
      Angular is an application design framework and development platform for creating efficient and sophisticated single-page apps.
    </angular-tailwind-nx-card>

    <angular-tailwind-nx-card title="Tailwind CSS" url="https://tailwindcss.com/">
      Tailwind CSS is a utility-first CSS framework packed with classes like flex, pt-4, text-center and rotate-90 that can be composed to build any design, directly in your markup.
    </angular-tailwind-nx-card>

    <angular-tailwind-nx-card title="Nx" url="https://nx.dev/">
      Nx is a smart, fast and extensible build system with first class monorepo support and powerful integrations.
    </angular-tailwind-nx-card>
  </main>
</div>

Like we did with app1, we also need to update the build and serve targets configuration for app2 to be able to consume the buildable library compiled output. We do so by updating the app2 configuration located in the apps/app2/project.json file:

{
  ...
  "targets": {
    "build": {
      "executor": "@nrwl/angular:webpack-browser",
      ...
    },
    "serve": {
      "executor": "@nrwl/web:file-server",
      "configurations": {
        "production": {
          "buildTarget": "app1:build:production"
        },
        "development": {
          "buildTarget": "app1:build:development"
        }
      },
      "defaultConfiguration": "development"
    },
    ...
  },
  ...
}

Last but not least, we need to configure Tailwind CSS with our custom theme for app2. We’ll do that by updating the apps/app2/tailwind.config.js file with the following:

...

module.exports = {
  ...
  theme: {
    colors: {
      primary: {
        light: '#a5b4fc',
        DEFAULT: '#6366f1',
        dark: '#4338ca',
      },
      secondary: {
        light: '#e9d5ff',
        DEFAULT: '#a855f7',
        dark: '#7e22ce',
      },
      white: '#ffffff',
      black: '#000000',
    },
    spacing: {
      sm: '1rem',
      md: '1.5rem',
      lg: '2rem',
      xl: '3rem',
    },
  },
  ...
};

Now that we have the second application configured, let’s run it:

npx nx run app2:serve

You can pass a --port=4201 to the above command to run it in a different port if it’s in use or if you want to have both applications running side-by-side.

Now, open your browser and navigate to it where you should see the application looking like the following screenshot:

Application 2 screenshot

That does indeed look different, but something is off. The card background color is not right, it’s still the same used for app1 even though we provided a different theme. Also, some of the spacing for the elements within the card doesn’t seem to have changed according to our configuration.

What is going on here?

You might have realized a couple of things by now:

  • The card component comes from lib2 which is a buildable library and as such, it’s built on its own using its own Tailwind CSS configuration
  • app1 and lib2 use a Tailwind CSS preset to share the common configuration, while app2 is adding its own

So, the first bullet point above would explain why the card component looks like the one rendered using the theme for app1. But that’s not exactly what we are seeing, the buttons inside the card look different than what we have in app1. This is explained by the fact that the buttons are styled without using any Tailwind CSS directive or function, they just use utility classes, so the CSS for them is generated in the app2 build using the application configuration. The rest of the card does use directives and functions, so the CSS for that is generated in the lib2 build using the library configuration.

Also, we previously created a Tailwind CSS preset so we could share the base configuration among different projects. The problem is that all those projects shared a common theme, but app2 requires a different one, so we can’t just use the preset as it is right now.

So, how do we solve this?

Enter CSS variables!

We can configure the Tailwind CSS preset to use CSS variables. This will allow each application to provide its own values for the variables and therefore, it enables us to have multiple themes using the same Tailwind CSS configuration.

Let’s update our preset in the libs/tailwind-preset/tailwind.config.js file to use CSS variables instead of literal values:

module.exports = {
  theme: {
    colors: {
      primary: {
        light: 'var(--primary-light)',
        DEFAULT: 'var(--primary)',
        dark: 'var(--primary-dark)',
      },
      secondary: {
        light: 'var(--secondary-light)',
        DEFAULT: 'var(--secondary)',
        dark: 'var(--secondary-dark)',
      },
      white: 'var(--white)',
      black: 'var(--black)',
    },
    spacing: {
      sm: 'var(--spacing-sm)',
      md: 'var(--spacing-md)',
      lg: 'var(--spacing-lg)',
      xl: 'var(--spacing-xl)',
    },
  },
  plugins: [],
};

Next, we update the apps/app2/tailwind.config.js file to remove the explicit theme configuration and add the preset instead:

const { createGlobPatternsForDependencies } = require('@nrwl/angular/tailwind');
const { join } = require('path');
const sharedTailwindConfig = require('../../libs/tailwind-preset/tailwind.config');

module.exports = {
  presets: [sharedTailwindConfig],
  content: [
    join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'),
    ...createGlobPatternsForDependencies(__dirname),
  ],
};

Since our preset no longer has any literal values for the theme properties, we need to set the values for the CSS variables in the application. Edit the apps/app2/src/styles.css file with the values for the theme variables:

...

:root {
  /* Colors */
  --primary-light: #a5b4fc;
  --primary: #6366f1;
  --primary-dark: #4338ca;
  --secondary-light: #e9d5ff;
  --secondary: #a855f7;
  --secondary-dark: #7e22ce;
  --white: #ffffff;
  --black: #000000;

  /* Spacing */
  --spacing-sm: 1rem;
  --spacing-md: 1.5rem;
  --spacing-lg: 2rem;
  --spacing-xl: 3rem;
}

We need to do the same for app1. Edit the apps/app1/src/styles.css file with the values for the theme variables:

...

:root {
  /* Colors */
  --primary-light: #5eead4;
  --primary: #14b8a6;
  --primary-dark: #0f766e;
  --secondary-light: #bae6fd;
  --secondary: #0ea5e9;
  --secondary-dark: #0369a1;
  --white: #ffffff;
  --black: #000000;

  /* Spacing */
  --spacing-sm: 0.5rem;
  --spacing-md: 1rem;
  --spacing-lg: 1.5rem;
  --spacing-xl: 2rem;
}

Let’s serve again app2 and navigate to it to check the results of our changes:

Application 2 screenshot

Now we are talking!

This is what we wanted to see. Also app1 is still working as expected with its different theme. We are successfully styling two different applications with different themes while sharing some UI components and using the same Tailwind CSS base configuration.

Extracting the button into a publishable library

Now that both our applications are looking great, we want to share our awesome buttons with the community. So we are going to create a button component in a publishable library to be able to distribute it.

First, we create the publishable library with Tailwind CSS support:

npx nx generate @nrwl/angular:lib lib3 --publishable [email protected]/lib3 --addTailwind

Then, we update the libs/lib3/tailwind.config.js to use the shared preset:

const { createGlobPatternsForDependencies } = require('@nrwl/angular/tailwind');
const { join } = require('path');
const sharedTailwindConfig = require('../../libs/tailwind-preset/tailwind.config');

module.exports = {
  presets: [sharedTailwindConfig],
  content: [
    join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'),
    ...createGlobPatternsForDependencies(__dirname),
  ],
};

Then, we create the button component:

npx nx generate @nrwl/angular:component button --project=lib3 --export

We add the component to the library entry point located in libs/lib3/src/index.ts:

...
export * from './lib/button/button.component';

Then, we update the button component files to provide the desired functionality:

// button.component.ts
import { Component, Input } from '@angular/core';

@Component({
  selector: 'angular-tailwind-nx-button',
  templateUrl: './button.component.html',
  styleUrls: ['./button.component.css'],
})
export class ButtonComponent {
  @Input() href?: string;
}
<!-- button.component.html -->
<a
  class="py-sm px-md bg-primary-dark hover:bg-primary text-white"
  href="{{ href }}"
  target="_blank"
  rel="noopener noreferrer"
>
  <ng-content></ng-content>
</a>

Next, we need to update the card component in lib2 to use the button component. Import Lib3Module into Lib2Module:

...
import { Lib3Module } from '@angular-tailwind-nx/lib3';

@NgModule({
  imports: [CommonModule, Lib3Module],
  ...
})
export class Lib2Module {}

And finally, we replace the existing markup for the button in the libs/lib2/src/lib/card/card.component.html file with the new button component:

<div class="card">
  <div class="card-title">{{ title }}</div>
  <p class="card-content">
    <ng-content></ng-content>
  </p>
  <angular-tailwind-nx-button class="flex self-end" [href]="url">Show me!</angular-tailwind-nx-button>
</div>

Once more, we can check both applications and make sure everything is still working and nothing was affected by the changes made.

Distributing the publishable library styles

The recently created publishable library is already being used successfully by both applications, but it’s still not ready for distribution. If we were to share it now, external consumers will need to provide their own CSS for it because the library itself is not bundling any CSS with the styling for the button. We only used some Tailwind CSS utility classes and as we have seen throughout this blog post, the CSS for them is generated in files containing @tailwind directives (normally in application style entry points).

Within the workspace, it works as expected because the applications consuming it use the same shared Tailwind CSS preset and their dependencies are included in the content of their configuration.

The library needs to contain everything that’s needed for it to work and to achieve this, we are going to do something we already did with our buildable library: create our own classes using the @apply directive.

As we learned in a previous section, the @apply directive will be transformed into the CSS corresponding to the Tailwind CSS classes being applied. Thanks to this, our button component will contain the CSS needed to style it.

Go ahead and update the button component files with a new CSS class for the button:

/* button.component.css */
.atn-button {
  @apply py-sm px-md bg-primary-dark hover:bg-primary text-white;
}
<!-- button.component.html -->
<a class="atn-button" href="{{ href }}" target="_blank" rel="noopener noreferrer">
  <ng-content></ng-content>
</a>

I used the prefix atn (initials of Angular, Tailwind CSS, and Nx) for the CSS class name to prevent potential name collisions with the consumers' applications CSS.

Also, let’s update the libs/lib3/src/lib/button/button.component.ts file to set the component’s encapsulation to ViewEncapsulation.None to allow consumers to overwrite its styles more easily:

import { Component, Input, ViewEncapsulation } from '@angular/core';

@Component({
  ...
  encapsulation: ViewEncapsulation.None,
})
export class ButtonComponent {
  ...
}

If we build our library now, the styles for the button component will be correctly generated, but because we are using CSS variables for our theme, consumers would still need to provide their own values for them before they can use it.

We need to provide an initial theme that sets those CSS variables so the library components can be consumed without any additional setup. Actually, we are going to generate a couple of theme options so we can see how multiple themes can be provided.

Let’s start by creating a libs/lib3/src/styles/teal.css theme file where we are going to import the Tailwind CSS components and utilities layers and define the values for the CSS variables of our theme:

@tailwind components;
@tailwind utilities;

:root {
  /* Colors */
  --primary-light: #5eead4;
  --primary: #14b8a6;
  --primary-dark: #0f766e;
  --secondary-light: #bae6fd;
  --secondary: #0ea5e9;
  --secondary-dark: #0369a1;
  --white: #ffffff;
  --black: #000000;

  /* Spacing */
  --spacing-sm: 0.5rem;
  --spacing-md: 1rem;
  --spacing-lg: 1.5rem;
  --spacing-xl: 2rem;
}

Notice that we didn’t include the base layer like we have done so far in the applications style entry points. This is because this is a component library and the base layer generates a set of application-wide base styles and that’s not what we want to generate here.

Next, we generate our second theme by creating the libs/lib3/src/styles/indigo.css theme file with different values for the CSS variables:

@tailwind components;
@tailwind utilities;

:root {
  /* Colors */
  --primary-light: #a5b4fc;
  --primary: #6366f1;
  --primary-dark: #4338ca;
  --secondary-light: #e9d5ff;
  --secondary: #a855f7;
  --secondary-dark: #7e22ce;
  --white: #ffffff;
  --black: #000000;

  /* Spacing */
  --spacing-sm: 0.5rem;
  --spacing-md: 1rem;
  --spacing-lg: 1.5rem;
  --spacing-xl: 2rem;
}

With that in place, we now need to make sure those theme files are processed when we build the library. The @nrwl/angular:package executor is powered by the ng-packagrExternal link icon package to build the library. This is a tool recommended by Angular to ensure libraries are distributed using the Angular Package FormatExternal link icon. Unfortunately, it doesn’t have native support for building standalone stylesheets that are not referenced by a component, so we need to configure it ourselves.

To do so, we are going to use the Tailwind CSS CLI to process our stylesheets when the library is built and we’ll do it in parallel since they don’t depend on each other. One aspect to consider is that the @nrwl/angular:package executor will delete the destination folder before building. When running both processes in parallel, the styles might be generated first and then the directory containing them is deleted by the @nrwl/angular:package executor. Therefore, we are going to disable that behavior and we are going to control when to delete the destination folder to avoid any issues.

Another thing to consider is that the Tailwind CSS CLI only supports processing one file at a time, it doesn’t accept glob patterns or directories. We’ll need to run a command per theme in our library.

To orchestrate this, we are going to make the following changes to the lib3 project configuration:

  • Rename the existing build target to build-angular
  • Create a build-themes target that runs, in parallel, the Tailwind CSS CLI for every theme in our library
  • Create a build-lib target that runs, in parallel, the build-angular and build-themes targets
  • Create a build target that first, deletes the destination folder, and then runs the build-lib target

Edit the project configuration for the lib3 project located in the libs/lib3/project.json file with the changes described above and shown below:

{
  ...
  "targets": {
    "build-angular": {
      "executor": "@nrwl/angular:package",
      ...
    },
    "build-themes": {
      "executor": "@nrwl/workspace:run-commands",
      "outputs": ["dist/libs/lib3/themes"],
      "configurations": {
        "production": {
          "commands": [
            "tailwindcss -c libs/lib3/tailwind.config.js -i ./libs/lib3/src/styles/teal.css -o ./dist/libs/lib3/themes/teal.css -m",
            "tailwindcss -c libs/lib3/tailwind.config.js -i ./libs/lib3/src/styles/indigo.css -o ./dist/libs/lib3/themes/indigo.css -m"
          ]
        },
        "development": {
          "commands": [
            "tailwindcss -c libs/lib3/tailwind.config.js -i ./libs/lib3/src/styles/teal.css -o ./dist/libs/lib3/themes/teal.css",
            "tailwindcss -c libs/lib3/tailwind.config.js -i ./libs/lib3/src/styles/indigo.css -o ./dist/libs/lib3/themes/indigo.css"
          ]
        }
      },
      "defaultConfiguration": "production"
    },
    "build-lib": {
      "executor": "@nrwl/workspace:run-commands",
      "outputs": ["dist/libs/lib3"],
      "configurations": {
        "production": {
          "commands": [
            "nx run lib3:build-angular:production",
            "nx run lib3:build-themes:production"
          ]
        },
        "development": {
          "commands": [
            "nx run lib3:build-angular:development",
            "nx run lib3:build-themes:development"
          ]
        }
      },
      "defaultConfiguration": "production"
    },
    "build": {
      "executor": "@nrwl/workspace:run-commands",
      "outputs": ["dist/libs/lib3"],
      "configurations": {
        "production": {
          "commands": [
            "rm -rf dist/libs/lib3",
            "nx run lib3:build-lib:production"
          ],
          "parallel": false
        },
        "development": {
          "commands": [
            "rm -rf dist/libs/lib3",
            "nx run lib3:build-lib:development"
          ],
          "parallel": false
        }
      },
      "defaultConfiguration": "production"
    },
    ...
  },
  ...
}

The only thing remaining is to update the libs/lib3/ng-package.json to prevent the Angular build to delete the destination folder. We do that by setting the deleteDestPath option to false:

{
  ...
  "deleteDestPath": false
}

We can now build the library by running:

npx nx run lib3:build

If we check the output folder dist/libs/lib3, we’ll see there’s a themes folder in it with a couple of files indigo.css and teal.css:

Publishable library with the produced theme files highlighted

These theme files can now be used by the consumers of our library to properly style the components exposed by it. All they would need to do is to import one of those themes into their application styles entry point or index.html file.

They can also customize the included themes by overwriting any of the CSS variables of the theme or the specific styles of the atn-button CSS class.

Conclusion

We covered a lot in this article and hopefully, it gave a good walkthrough over the different scenarios we might find ourselves when using Angular and Tailwind CSS in an Nx workspace.

Doing a quick recap, we learned:

  • How to add support for Tailwind CSS in existing Angular projects using an Nx generator
  • How to create Angular projects with Tailwind CSS already configured using an Nx generator
  • How to share Tailwind CSS configuration among an application and its dependencies using presets
  • How to share Tailwind CSS configuration among multiple applications and their dependencies while still being able to have different styles
  • How to create and distribute multiple themes in an Angular publishable library using Tailwind CSS
← Return to all blog posts