11

I have created a library in angular which is styled using tailwind. This is then been push to NPM and then imported into a new project, but the CSS is not getting applied. I have referenced the node-module path in my tailwind.config.ts:

content: [
  "./src/**/*.{html,ts}",
  './node_modules/components-name/**/*.{html,js,ts}'
],

What am I missing?

Tailwind is working if I apply it directly to the new application, it just doesn't work with the imported library.

2
  • I think this will help you fix your problem: stackoverflow.com/a/70937474/18624880 Commented Mar 31, 2022 at 16:44
  • From TailwindCSS v4 onwards the source discovery has changed. An automatic source detector has been integrated, which allows adding (by @source directive) and disabling (by .gitignore) access, but by default, the TailwindCSS engine discovers everything on its own. Commented Feb 12 at 15:55

5 Answers 5

8

If you expect all depender apps to utilize tailwind, you can use tailwind classes in your library HTML and have them configure a content path of ./node_modules/my-lib/esm2020/**/*.mjs. It finds the inlined/escaped classes in the Ivy compiled files. esm2020 to scope the scan.


Update 11/30/22 - allowing the use of @apply in the library

@applys are not resolved in precompiled library code as these files are not processed in that lifecycle. As a workaround, you can pre-process your components to resolve @apply styles before building the library.

  1. Create a tailwind.config.js to use in the compilation
    • If your library project has a demo-app (highly suggest for impl testing), could utilize it's config file, unless you've got some crazy config in there. Since we're not rendering @tailwind components or anything, we won't get any excess styles

projects/my-lib/tailwind.config.js

module.exports = {
  content: [
    './projects/my-lib/**/*.{html,ts,css,scss}',
  ],
};

Note the content path is still relative from project root as that's the context it's ran at

  1. Create precompiler process
    • Tailwind resolve into a new file (mostly so we don't mess things up accidentally locally)
    • Point component at the new file
import { readFile, writeFile } from "fs";
import { sync } from 'glob';
import { exec } from 'child_process';

const libRoot      = 'projects/my-lib/src/lib';
const tailwindConf = 'tailwind.config.js'; // may be apps/demo when using NX
const processedExt = '.precompiled.scss';
const styleRegex   = /styleUrls:\s*\[([^\]]+)]/;

// Find all `.scss` files and tailwind process them
sync(`${libRoot}/**/*.component.scss`).forEach(file => {
  const cssFile = file.replace(/\.scss$/, processedExt);
  exec(`npx tailwind -c ${tailwindConf} -i ${file} -o ${cssFile}`, (err, stdout, stderr) => {
    if (err) {
      console.error(stderr);
      throw err;
    }
  });
});

// .component.ts update
// Find all components with `styleUrls` and switch `.scss` extension to our precompiled file names
sync(`${libRoot}/**/*.component.ts`).forEach(file => {
  readFile(file, (err, data) => {
    if (err) throw err;
    const content = data.toString();
    const match = content.match(styleRegex);
    if (match) {
      const styleUrls = match[1]
        .split(',')
        .map(s => s.trim().replace('.scss', processedExt))
        .join(', ');

      writeFile(file, content.replace(styleRegex, `styleUrls: [${styleUrls}]`), (err) => {
        if (err) throw err;
      });
    }
  });
});

This should only be ran by your CI process and never committed. Also this could easily be switched to javascript instead of typescript

Other possible ways to do this (untested) without the .component.ts update:

  • Utilize environment.prod.ts's production: true flag to decide the style file to use
    • styleUrls: [ environment.prod ? 'my.component.precompiled.scss' : 'my.component.scss' ],
    • Gotta remember this for all new components
  • Change the tailwind compile to output to the same scss file
    • Less moving parts - I liked the separate file so I'd realize quickly if it were accidentally ran/committed
  1. Add CI precompile command to package.json
    • "build:ci": "node --require ts-node/register projects/my-lib/src/precompile.ts && npm run build:my-lib"
      • Very rough implementation - remove --require ts-node/register if converted to javascript

I use NX workspace, so I added a new target in the library's project.json:

"ci": {
  "executor": "nx:run-commands",
  "options": {
    "command": "node --require ts-node/register libs/my-lib/src/precompile.ts"
  }
},

and added a the package.json entry as:

"build": "nx run-many --all --target build",
"build:ci": "npx nx ci && npm run build",

allowing build to still be used locally.

  1. Build and Package/Release as normal
    • With @apply's resolved, all should flow well
    • If you used tailwind utility classes in HTML, be sure to see the very beginning of this answer

Tailwindless Depender

If you want applications to be able to utilize your library without them installing tailwind you could supply a stylesheet containing all the helper classes you used.

  1. Create a stylesheet to contain all used utilities

projects/my-lib/style.scss

@tailwind utilities;
  1. Add a postbuild to your package.json to produce the stylesheet, assuming you use npm run build to build the library.
    "postbuild": "npx tailwind -c projects/my-lib/tailwind.config.js -i projects/my-lib/style.scss -o dist/my-lib/style.scss",
  1. Direct depender projects to then include this compiled stylesheet:
@import 'my-lib/style.scss'

Note tailwind does not compile SCSS into CSS - need to run through a SASS processor if you want to supply CSS.

Downside of this is all utility classes used in all components are produced, even if the depender app doesn't use them (same happens for projects using tailwind, so not so bad). Also the depender project may produce duplicate utility classes if using tailwind itself.

Plus side is your library doesn't require the depender to have tailwind.

Note that you still need the above process to resolve @apply's - this only gathers the utility classes used in the HTML

Sign up to request clarification or add additional context in comments.

5 Comments

I think this is by far, the cleanest "hacky" way of making TailwindCSS work with Angular libraries, the downside being that if it's a UI components library, as the library grows, so does the amount of unused CSS, when imported to any project, basically circumventing the idea of tree-shaking CSS from the project.
Agreed, but I'd assume the utility classes being used for components would 80%+ overlap with the application's uses. But using tailwind in general has the same down-side that all utilities are compiled into your front-loaded CSS, even if those parts of the app go un-accessed.
Thanks, @Charly. Just a question, why it does not work with @apply ? Which is the reason? And did you find in the last months a workaround also for this?
@ptesser Sorry for the delay - the @apply doesn't work when packaged as a library. From what I can tell, these are resolved then ran through SCSS compilation - the compiled .mjs files are expected to be compiled to CSS already and are not resolved. I played around with various way but never came up with a good solution. I did however come up with another solution that may fit your needs - updating this answer shortly.
For me it was the .mjs missing from content
7

Automatic Source Detection from TailwindCSS v4

For the TailwindCSS v4 engine, we do not need to specify the sources we use. It automatically finds them. However, it may also search in the node_modules folder, where it can "incorrectly" detect classes that it assumes we are using. This is how unused classes can end up in the output.

The detection, however, takes into account the rules listed in the .gitignore file and ignores the paths that are blocked there. Therefore, it is always a good idea to create a .gitignore and block node_modules in it:

/node_modules/

Important Note: Starting from v4.0.18 on 2025-03-28, automatic source detection ignores node_modules automatically, regardless of .gitignore.

But you can still specify individual sources using the @source directive.

Old note (mainly before v4.0.18): The automatic discovery ignores the contents of .gitignore. It is recommended to create a .gitignore file and include /node_modules/ in it. However, this will exclude all package discoveries.

How can I discover my package named my_package?

Simply by using the @source CSS directive through the CSS-first configuration:

@import "tailwindcss";

@source "../node_modules/my_package/";

A related question for developers creating a component package could be:

Extra: Exclude source with CSS directive since v4.1.0

If you want to exclude a path from TailwindCSS's automatic source detection, but you don't want to add that path to your .gitignore file, you can use the @source not directive to disable it specifically for TailwindCSS.

@source not "../src/bootstrap-components/";

Or, if for some reason you don't have and don't want to create a .gitignore file (e.g., for a quick reproduction), it might also be a good idea to exclude the node_modules folder from TailwindCSS's source detection this way. That way, you can avoid unexpected class names in your output that might be picked up from some package (including TailwindCSS itself).

@import "tailwindcss";

@source not "../node_modules/";
@source "../node_modules/my_package/";

8 Comments

Thanks a lot, that worked perfectly for me and no need for any post build scripts
Is @source "../node_modules/my_package/" the only way? ... Would be much easier if we could do @source 'my_package' directly instead of having to through node_modules .... We don't have to do import { something } from '../node_modules/my_package'. We can simply do import { something } from 'my_package'
Yes, it exclusively accepts a path. As a development suggestion, it could be proposed that if the path doesn't start with a dot, it should default to resolving from node_modules. However, this presents be a challenge in locating the node_modules folder, since the main.css file can be in various locations depending on the project structure; especially when it's implemented in a sub-project.
@kcsujeet The good news is that you only need to do this once for each required package, and it won't even appear in the compiled CSS, so skipping the path prefix isn't relevant; not even for the sake of saving space.
@rozsazoltan thanks you for the reply. would be great if we didn't have to do the relative path. JS works without it without having to worry about the project structure. But this feature is still a life saver for now.
|
4

Simplest solution that's proved working with Angular 16 (2023)

It works for HTML classes aswell as @apply.

Create another tailwind.config.js in your library root, e.g. ./projects/ngx-lib/tailwind.config.js

/** @type {import('tailwindcss').Config} */
module.exports = {
   content: ['./projects/ngx-lib/src/**/*.{html,ts}'],
};

The problem is, that we can not scope all Tailwind base-styles out of the box, so we need to fix this.

Create projects/ngx-tib/src/theme/tailwind-setup.scss and put in

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

Also create projects/ngx-tib/src/theme/tailwind.scss.

In your lib.component.scss, add:

ngx-lib {
   @import "../theme/tailwind";
}

In your package json, add this script and always run it before building the library:

"build:lib": "npm run build:lib:tailwind && ng build ngx-lib",
"build:lib:tailwind": "tailwind -c projects/ngx-lib/tailwind.config.js -i projects/ngx-lib/src/theme/tailwind-setup.scss -o projects/ngx-lib/src/theme/tailwind.scss",

In the end, the SCSS compiler will do the prefixing for us.

Development:

If you are importing the library to your demo app during development, you will need to add the library paths to the root project's tailwind.config.js:

content: [
   './src/**/*.{html,ts}',
   './projects/ngx-lib/src/**/*.{html,ts}'
],

3 Comments

If the project that's using the package overrides a default Tailwind color, and the library being imported uses that color, will the library component have this new color or the Tailwind default?
@Evanss good point. Since Tailwind does not use CSS variables for it's color palette, it will keep the default colors unfortunately. You can check my repository if you like to try it yourself: github.com/btxtiger/angular-library-skeleton You can switch between dist and local component in app.module.ts
Can I use .css extensions over the proposed .scss? My library uses css files generally.
3

It's actually very easy, no need for precompiler hacks

Got this solution from the Tailwind docs, which suggest using the npx tailwind build tool (instead ofnpx tailwindcss-cli, like other solutions on the Internet suggest).

Step 1

In your library folder, have the Tailwind config content as if the origin is the base project folder:

module.exports = {
  content: [
    './projects/my-lib/**/*.{html,ts,css,scss}',
    './**/*.{html,ts,css,scss}',
  ],
};

Step 2

In step 3, a tailwind.scss file will be generated into the my-lib/src/lib folder, so (as suggested here) you have to include the tailwind.scss file in your component:

styleUrls: ['../tailwind.scss'].

(Careful with the path)

Step 3

Run this command

npx tailwindcss@latest -c ./projects/my-lib/tailwind.config.js -o ./projects/my-lib/src/lib/tailwind.scss 

before building the library. package.json

{
  "scripts": {
    "build:my-lib": "npx tailwindcss@latest -c ./projects/my-lib/tailwind.config.js -o ./projects/my-lib/src/lib/tailwind.scss & ng build my-lib"
  }
}

7 Comments

This unfortunately does not work, and leaves you with an unscoped stylesheet that contains a lot more styles than required
@btx it's kind of weird that everyone has their own working solution. this is what works for me though, I get a 600~ line tailwind.scss file that only has what is used in the scope I define in the tailwind.config.js
This was the simplest answer in my opinion. Minimal tailwind.scss file. I pointed to my tailwind.config.js living in the root of my project for the tailwind command. Works well.
@Tovar if you ship your library to npm, you will be required to bundle @tailwind-base in your library, to make sure your component is properly redered. And Tailwind does not support scoping the base styles. Other people might use normalize.css or equal and not tailwind base. Thats why my solution is slightly different from yours.
@btx in my solution, Tailwind (specifically, the npx tailwind CLI tool) is only used in build, meaning it is only a dev dependency since only the build result should be published to NPM (according to the docs)
|
0

With angular 19 and tailwind 4.1, I simply add @source in my main style.scss, and all tailwind CSS classes used by my lib are correctly exported

@use "tailwindcss";
@source "../node_modules/my-lib";

From documentation : https://tailwindcss.com/docs/functions-and-directives#source-directive

Use the @source directive to explicitly specify source files that aren't picked up by Tailwind's automatic content detection:

2 Comments

This just looks like a repetition this answer: stackoverflow.com/a/79433685/15167500
@use "tailwindcss"; is incorrect. SCSS and TailwindCSS are not officially supported together. If you want to understand this better or find a workaround for using them in parallel, I recommend this answer (although it’s not Angular-specific, it discusses SCSS + TailwindCSS): @import "tailwindcss"; does not work when used in a file with an .scss extension or TailwindCSS with SCSS how to working or stackoverflow.com/a/79743713/15167500

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.