This article requires that you understand the basic terminologies and concepts used in Angular Schematics such as
Collection,Rule,Tree, etc. If you want to learn more about it, check here and here.
Recently, I decided to experiment with tailwindcss (after all the buzz around this utility-first CSS framework). The idea was to pair tailwindcss with Angular. I hunted for documentation on combining tailwindcss with Angular and came across a few helpful write-ups. Check them here and here.
As you read through these guides, it becomes clear that wiring tailwindcss into Angular isn't as simple as running one command — it requires a handful of manual steps.
Fortunately, I also stumbled upon this library that streamlines the integration beautifully (yes, just one command!). It leverages Angular schematics to handle the heavy lifting.
To gain a deeper understanding, I first walked through the manual setup and then tried out the schematics-based library (@ngneat/tailwind) to get tailwindcss working in Angular. My main objective was achieved.
But a few months earlier, I had dipped my toes into Schematics in Angular, only to shelve it for another interest (can't recall what exactly). I decided to revisit it, this time with the goal of building something functional but simple — not just learning the theory.
As noted earlier, I won't dive into the fundamentals of Angular Schematics here. For that, please consult this and this for thorough explanations. After studying these resources and exploring Schematics further, I'd summarize it like this:
Schematics are fundamentally about generating code and modifying existing files!
And the key files you'll be dealing with are:
collection.json– the schematic's definition file.index.ts– the file containing the schematic factory function.schema.json– holds the options that can be passed to the factory.schema.ts– defines the interface for those options.files– a directory for template files that get generated.
Armed with this knowledge, I realized that building a schematic for Angular + Tailwind CSS integration would be an excellent starting point for a beginner, for several reasons:
- It involves generating code (e.g., adding tailwindcss imports).
- It requires updating existing files (like
angular.json). - It needs creating new files (such as webpack config).
- It entails modifying
package.jsonand installing packages.
Essentially, it touches nearly everything Angular Schematics is meant to simplify. Plus, I had solid references — both blog posts and an existing schematic library.
Thanks to all the talented folks who've written about this subject and contributed to libraries built with Angular Schematics.
So, with the dual ambition of mastering Angular Schematics and building a real library, I dove into my next project – angular-tailwindcss-schematics.
In the following section, I'll walk you through the git commit history of this project, explaining for each step:
- What the step accomplishes (its purpose)?
- A link to the related code/files with explanations (if needed)
- How it connects to Angular Schematics.
The plan is to begin with the simplest schematic concepts and implementations, then progressively build toward the complete angular-tailwindcss-schematics project.
Want to see the code on Github? → Click here
Check out this simple Angular Schematic on npm → ngx-tailwindcss-schematic
Setting Up
First off, I created a git repository named angular-tailwindcss-schematics and pulled it to my local environment.
You'll need the @angular-devkit/schematics-cli package to use the schematics command in your terminal. This tooling lets you create a blank schematics project.
npm i -g @angular-devkit/schematics-cli
Creating the Project
Run: schematics blank angular-tailwindcss-schematics
- This command scaffolds a new schematics project, specifically designed to build a standalone schematic usable in any Angular CLI application.
- It generates the following files:
CREATE angular-tailwindcss-schematics/README.md (639 bytes)
CREATE angular-tailwindcss-schematics/.gitignore (191 bytes)
CREATE angular-tailwindcss-schematics/.npmignore (64 bytes)
CREATE angular-tailwindcss-schematics/package.json (587 bytes)
CREATE angular-tailwindcss-schematics/tsconfig.json (656 bytes)
CREATE angular-tailwindcss-schematics/src/collection.json (284 bytes)
CREATE angular-tailwindcss-schematics/src/angular-tailwindcss-schematics/index.ts (335 bytes)
CREATE angular-tailwindcss-schematics/src/angular-tailwindcss-schematics/index_spec.ts (539 bytes)
✔ Packages installed successfully.
Among all the generated files, two stand out:
collection.json– stores the schematic name, description, and the path to the factory method.angular-tailwindcss-schematics/src/angular-tailwindcss-schematics/index.ts– the factory file with a function that returns aRule. Most of our logic will live here.
Commit at this point: Code
Renaming to ng-add
In this commit, I changed the name from angular-tailwindcss-schematics to ng-add to align with conventions, since this will be a schematic you add to your app.
I also made the factory function in index.ts a default export and updated the collection.json accordingly.
Commit at this point: Code
Introducing the Schema File
The schema.json file is another crucial piece, allowing you to define additional options when running your schematic while also adding validation. For type safety, we can pair it with a schema.ts interface that outlines the available options.
Our default factory in index.ts now accepts options: Schema as a parameter. This options object carries the schema options passed via the command line.
Additionally, I linked this new schema.json in our collection.json.
Commit at this point: Code
Setting Up the Files Directory
Here, I created a files directory (src/ng-add/files) to house template files that the schematic can use to generate outputs.
You'll notice terms like dasherize and classify. These are helper functions from the schematics library (strings module) that transform names for templating purposes. Find more details here.
I've updated index.ts with basic logic to generate a file based on a template from the files directory, adding comments to clarify the flow.
Commit at this point: Code
Compiling and Executing Schematics
At this stage, we have all the essential components of a schematic, though it doesn't do much yet! You can now build and test it.
// Build
npm run build
// OR run in watch mode
npm run build:watch
// Run
// dry-run mode
schematics .:ng-add
// normal mode
schematics .:ng-add --debug false
We now have a functional Angular Schematic in its basic form. Next, we'll modify the generated files and code to create a schematic that adds tailwindcss to any Angular CLI application or Nx workspace.
To integrate tailwindcss, our workspace needs these changes:
- Add
tailwindcssdependencies topackage.jsonand install them.
tailwindcss
postcss-import
postcss-loader
@angular-builders/custom-webpack
postcss-scss (only needed if selected style type is scss)
- Update the project's default styles file with
tailwindcss imports.
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
-
Create these config files:
- tailwind.config.js
- webpack.config.js
-
Modify
angular.jsonto use a custom Angular webpack builder.
Modifying the index.ts File
Before implementing the above, let me outline the tweaks I made to the default function in index.ts.
// ng-add/index.ts (entry point for schematic execution)
import { chain, Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
import { Schema } from './schema';
// You don't have to export the function as default. You can also have more than one rule factory per file.
/** Rule factory: returns a rule (function) */
export default function (options: Schema): Rule {
// this is a rule. It takes a `tree`, apply changes and returns
// updated tree for further processing by next rule.
// this way the schematic rules are composable.
return (tree: Tree, context: SchematicContext) => {
// Read `angular.json` as buffer
const workspaceConfigBuffer = tree.read('angular.json');
if (!workspaceConfigBuffer) {
throw new SchematicsException('Could not find an Angular workspace configuration'); ? (Make sure you are in Angular Cli workspace)
}
// parse config only when not null
const workspaceConfig: workspace.WorkspaceSchema = JSON.parse(workspaceConfigBuffer.toString());
// if project is not passed (--project), use default project name
if (!options.project && workspaceConfig.defaultProject) {
options.project = workspaceConfig.defaultProject;
}
const projectName = options.project as string;
// elect project from projects array in `angular.json` file
const project: workspace.WorkspaceProject = workspaceConfig.projects[projectName];
if (!project) {
throw new SchematicsException(`Project ${projectName} is not defined in this workspace.`);
}
// compose all rules using chain Rule.
return chain([])(tree, context); ?
};
}
Our default factory in index.ts is the only exported function there, referenced from collection.json.
In the code, when you run this schematic, I've added checks to ensure you're in an Angular CLI workspace with a valid project. If either condition fails, an exception gets thrown.
A key addition is the chain rule function. It returns a Rule by merging several other Rules. More on that here. So, for each tailwindcss task, we'll create a function that returns a Rule, then call those from the chain function like so:
// Example
// ng-add/index.ts
export default function (options: Schema): Rule {
return (tree: Tree, context: SchematicContext) => {
...
...
// compose all rules using chain Rule.
return chain([addDependencies(options)])(tree, context);
}
}
/**
* Add required dependencies to package.json file.
* @private
*/
function addDependencies(options: Schema): Rule {
...
...
}
With this skeleton in place, we only have two tasks left:
- Write a function for each
tailwindcsstask. - Update the
chain([])function.
There are multiple approaches to build and test the schematic from an Angular CLI workspace. See the ReadMe
Let's tackle these changes step by step.
Enhancing package.json with a dedicated update function
The earlier section highlighted the exact set of packages that tailwindcss relies on, including various CSS preprocessors such as css, scss, sass, and less. This commit focuses on two key objectives:
- Presenting the user with a choice for the project's default
CSStype. - Using that selection to trigger a method that injects the necessary packages into the
package.jsonfile.
For the first objective, the schema.json must be modified to include a prompt that presents the available CSS options. Correspondingly, the schema.ts file requires an update to accommodate this new field. The relevant code modifications can be reviewed below.
Git commit at this stage: Code
To accomplish the second objective, a new method named addDependencies(options: Schema): Rule has been introduced. This function receives the options object, which contains all custom settings like cssType, and proceeds to update the package.json file with the dependencies essential for tailwindcss. These are incorporated as Devdependencies.
When the default project style isn't
css, an additional dependency,postcss-${options.cssType}, needs to be included.
The logic of this method is straightforward and leverages several helper utilities sourced from @schematics/angular/utility/dependencies.
Git commit at this stage: Code
Finally, the chain([addDependencies(options)]) function located in the index.ts file needed to be refreshed to integrate this new step.
Introducing a function for tailwindcss configuration files
As established previously, running the schematic must result in the creation of configuration files for both tailwind and webpack.
This commit adds the addTemplateFiles(options: Schema): Rule method, which iterates over the files directory and transfers those template files into the user's project.
Git commit at this stage: Code
As with prior steps, the chain([addDependencies(options), addTemplateFiles(options)]) must be updated to include this new method.
Adding a function for tailwindcss imports in the style file
The project's default style file (e.g., styles.css or styles.scss) needs to be enriched with the imports required by tailwindcss. A helper method has been created to construct the appropriate import string.
export function getTailwindCSSImports(): string {
return `
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
`;
}
To satisfy this requirement, identifying the default style file for the project is crucial.
This information resides within the angular.json file. Specifically, the architect[buildTarget].options object contains a styles array that lists the default style file path. I will leverage this data.
// angular.json
// architect['build'].options
...
...
"architect": {
"build": { ? // buildTarget: 'build'
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/sample-app",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": true,
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss" ? // default style file
],
"scripts": []
},
...
...
...
...
Therefore, a helper method named getProjectDefaultStyleFile(project: workspace.WorkspaceProject, fileExtension: string) has been added to locate and return the style path.
This function receives the project object (essentially a subset of the angular.json structure) and the preferred css file type (selected by the user), then navigates through the JSON hierarchy to extract the necessary path.
Git commit at this stage: Code
With the style path identified, the principal function, updateStylesFile(options: Schema, project: workspace.WorkspaceProject): Rule, has been implemented to modify the style file using this default path.
Git commit at this stage: Code
The chain([addDependencies(options), updateStylesFile(options, project), addTemplateFiles(options)]) method must be amended to incorporate this new function.
Adding a function to update angular.json
The subsequent task involves updating the angular.json file to integrate the custom webpack builder and the webpack config file. This step is essential since tailwindcss must be part of the build pipeline (further details here) to ensure its proper integration and to unlock its full feature set.
// Custom webpack builder
@angular-builders/custom-webpack
// webpack config file
webpack.config.js
Navigating the JSON structure of angular.json once more, this time focusing on the architect[buildTarget].builder key, we locate where the change is required. The value for this key must be replaced with the custom webpack builder, and the architect[buildTarget].options object should be updated to reference the custom webpack file.
A helper function has been added to retrieve the correct target based on a builder name. This function takes the project object and a builderName (such as browser, devServer, etc.) and returns the corresponding target.
// File: src/ng-add/utils.ts
/** Gets all targets from the given project that match the specified builder name. */
export function getTargetsByBuilderName(project: workspace.WorkspaceProject, builderName: string) {
const targets = project.architect || {};
return Object.keys(targets)
.filter((name) => targets[name].builder === builderName)
.map((name) => targets[name]);
}
After obtaining the relevant target, we can modify the builder and options properties as needed.
Git commit at this stage: Code
The chain([addDependencies(options), updateStylesFile(options, project), addTemplateFiles(options), updateAngularJsonFile(workspaceConfig, project)]) must be adjusted to include this new functionality.
Adding a function to install the dependencies
The concluding piece is a method designed to install all the dependencies that were added to the package.json for tailwindcss. The built-in NodePackageInstallTask from '@angular-devkit/schematics/tasks' will be used.
Git commit at this stage: Code
The chain([addDependencies(options), updateStylesFile(options, project), addTemplateFiles(options), updateAngularJsonFile(workspaceConfig, project), install()]) should be updated to include this final method.
The complete chain([]) method now encompasses all the rules, executing them sequentially and passing the evolving tree from one rule to the next.
/** Rule factory: returns a rule (function) */
export default function (options: Schema): Rule {
// this is a rule (function). It takes a `tree` and returns updated `tree`.
return (tree: Tree, context: SchematicContext) => {
...
...
// compose all rules using chain Rule.
return chain([
addDependencies(options),
updateStylesFile(options, project),
addTemplateFiles(options),
updateAngularJsonFile(workspaceConfig, project),
install(),
])(tree, context);
};
}
For guidance on running and building, please consult the ReadMe
- The npm package can be accessed here —> ngx-tailwindcss-schematic
- The full source code is also available on Github
I would like to extend a special thanks to the Angular team for their excellent work. Keep it up!
Feel free to share your feedback, suggestions, or point out any mistakes in the comments. You can also connect with me on Twitter (@esanjiv).
Happy learning! Thank you!
