After using it in a couple small projects I've fallen in love with the Tailwind CSS framework as a way to structure and apply CSS, and as a design system. When I was looking for a static site generator to build this site with, one of my search criteria was support for Tailwind CSS, either baked-in, or easy to add.
I ultimately ended up adopting the Zola static site generator due to its simplicity, clear documentation, performance (haha millisecond builds go brrr), and it having a template language I preferred over the one used by Hugo. Despite Zola having an inbuilt support for processing SASS/SCSS, it does so with a SASS processor that's written in Rust and included in the Zola binary. As Tailwind CSS is build on top of the PostCSS tool, we can't throw the Tailwind styles into the Zola CSS system and call it a day.
Ways to include Tailwind in a Zola project
There are two possible ways to include Tailwind in a Zola project:
- Include the prebuilt CSS file from a CND.
- Make a custom build process for the CSS to use alongside Zola.
Prebuilt
Using the prebuilt version of Tailwind is a decent solution if you want to whip up a quick prototype. Add the stylesheet in the page head and start writing classes. The file size of the prebuilt version of Tailwind is large compared to what you can achieve by building your own (and most importantly purging the styles, but more on that later). The difference can be 5-10x after compression (2.5kb to 27kb).
The prebuilt version also lacks some of the more advanced features of the framework that need to be separately enabled, like group-hover and focus-within. You also miss out on using additional plugins to augment and extend Tailwind (e.g. @tailwindcss/custom-forms that makes integrating HTML forms with Tailwind less of a pain).
As such, to cut down on the stylesheet size and give you more power over the available styles, setting up a custom build process is a good option.
Tailwind CSS Build Process
Setup
To build and configure Tailwind yourself, a separate build process needs to be set up. The idea is to build Tailwind and then include the resulting style file like you would use any other static file with Zola.
As stated, Tailwind is built with PostCSS - a tool for generating and modifying CSS with Javascript. If we want to build Tailwind, we need to use PostCSS, which requires a separate build tool to run. I chose Parcel as the build tool as it includes PostCSS.
This article assumes you have already installed Zola and have setup the directory structure with
zola init
. All the commands are relative to the root folder of the Zola structure.
As PostCSS in included in Parcel, the only two dependencies we need to install are Parcel and Tailwind. To start you need a new npm/yarn project. I'm going to use yarn, but the steps should work the same with npm.
yarn init; # or npm init
yarn add parcel-bundler tailwindcss; # or npm install ...
Note that you might not need to add Parcel to your project if you are gonna build the site on your computer and deploy the static files manually. Instead you can install it globally to use in any project. As I will be using Netlify to build and serve these files, I want Parcel included in the project dependencies.
You can also install Parcel as a devDependency using the --dev
flag, but my Netlify builds run in production mode where the devDependencies are not installed.
Styles
Next we need to add the base Tailwind style file into our project. I keep the styles that need PostCSS processing separate from the Zola file structure and incorporate only the finished styles in the Zola build process.
Create a folder called styles
and add a file styles.css
with the base Tailwind styles as the contents:
@tailwind base;
@tailwind components;
@tailwind utilities;
Processing the Styles
Next we need to configure PostCSS to use the Tailwind plugin (and the autoprefixer plugin), so add the following to postcss.config.js
.
module.exports = {
plugins: [
require("tailwindcss"),
require("autoprefixer")
],
};
Lastly we want to set up two Parcel commands, one to watch our CSS files and rebuild them whenever something changes, and one to build a production version.
One drawback of running a separate build system for the styles is the time building them takes compared to the speed that Zola builds the rest of the site with. Fortunately with Tailwind you are rarely writing new CSS but modifying the template files instead, so the increased time is often not an issue.
To setup the watch script add the following into your package.json
"scripts": {
"watch": "parcel watch styles/styles.css --out-dir static/styles --no-hmr --no-source-maps"
}
The script will watch the styles.css
file we set up earlier, and build new styles to the Zola static files directory. We also disable the hot module reload functionality because Zola will handle that for us, and disable source maps, as our CSS files will often not be complicated.
Now we only need to include the styles in our templates and we're ready to begin styling our site.
We use the Zola get_url
shortcode to get the absolute URL of the stylesheet.
Workflow
For a smooth workflow you can launch two tabs of your terminal and start yarn run watch
in one and zola serve
in the other. When you make any changes to your styles, Parcel will process your styles and output them to the Zola static files directory. Zola will pick up on those changes and recompile your site.
Styling the site is as easy as using the Tailwind classes in your template files. The Tailwind docs have great examples of the how and why of styling with utility classes.
Styling Markdown Content
When Zola processes your content, the markdown is converted into HTML elements. As we can't control the classes of these elements (nor would we want to pollute the markdown files with HTML classes), we need to style the elements directly. However, we don't want to just style say the <p>
tag as those styles would then bleed into other elements on our pages. Instead we want to scope all markdown related styles inside a specific selector. This can be done multiple way but I chose to put all my markdown content inside a div with a specific class.
{{page.content | safe}}
This way I can scope all styles under the .markdown-content
selector and don't have to worry about the styles bleeding to unexpected elements.
For readability I separated the markdown styles into their own CSS file which can be imported into the main styles.css
file.
@tailwind base;
@tailwind components;
@import "./markdown-content.css";
@tailwind utilities;
Note: it's Parcel here that's resolving the
@import
statements. Normally with PostCSS you would need to use something like postcss-import to handle import statements (and work around some restrictions it comes with), but Parcel happily deals with them for us.
Even though we have to style the markdown content elements in a CSS file, we can still use the Tailwind classes by utilizing the Tailwind @apply directive.
.markdown-content {
@apply font-serif;
}
.markdown-content p {
@apply mb-5;
}
.markdown-content a {
@apply text-pink-500;
}
.markdown-content a:hover {
@apply text-pink-400 underline;
}
/* rest of the styles */
You can either manually prefix the style declarations with .markdown-content
or use one of the PostCSS plugins to provide nesting and other features. Personally I use the postcss-preset-env plugin and write my CSS using the future CSS nesting syntax.
.markdown-content {
@apply font-serif;
& p {
@apply mb-5;
}
& a {
@apply text-pink-500;
&:hover {
@apply text-pink-400 underline;
}
}
/* rest of the styles */
}
Building for Production
To build a production version of the stylesheet, we need to add a separate build script. The Parcel build
command will create an optimized version of our styles that are better suited for public sites than the development version created by the watch
command.
Add a new build
script to your package.json
. I chose to omit the source maps from the build version too, but there's no need to disable to hot module reload functionality here, as it is not created by the build
command anyway.
"scripts": {
"build": "parcel build styles/styles.css --out-dir static/styles --no-source-maps",
"watch": "parcel watch styles/styles.css --out-dir static/styles --no-hmr --no-source-maps"
},
Now we can build the production version of the styles with
yarn run build;
As mentioned at the start if this article, the pre-built version of Tailwind is fairly large. In reality the one built by our build process is most likely even larger! To actually make our styles production ready we need to purge unnecessary styles, i.e. delete classes that are not used in our HTML files. This can be done with PurgeCSS (a dependency of Tailwind CSS).
To use PurgeCSS on our styles we need to configure Tailwind to look through our template files for the HTML classes we actually use.
Add the purge
option to your tailwind.config.js
and list you template and theme folders (if you are using a theme built with Tailwind).
module.exports = {
purge: ["./templates/**/*.html", "./theme/**/*.html"],
theme: {},
variants: {},
plugins: [],
};
You should also read the Tailwind docs on controlling file size to understand how the process works and what you need to take into account when writing your classes.
Another thing you need to consider for production builds is cache busting. It's best for performance to have the visitor browsers cache your stylesheets. This, however, necessitates some mechanism for bypassing the cache whenever the stylesheet actually changes.
Zola has a built-in method of bypassing caches by appending a hash calculated from the file contents at the end of a static asset url. This is enabled by using the cachebust=true
parameter of the get_url
method.
Unfortunately at the time of writing the
get_url
implementation has a bug when used with thecachebust
parameter. See here for a workaround.
Cache busting combined with some cache control headers (e.g. on Netlify) means that the visitor browser only needs to fetch the stylesheet once every time it changes. And, since we are using Tailwind, that should not be often.
If you are using an external service to build and host your Zola site, remember to add the yarn run build
before zola build
in the build command. For example, if you deploy your site on Netlify, you can specify the build command in netlify.toml
[build]
publish = "public"
command = "yarn run build && zola build"
Finished, finally
We've now set up a system to compile and include styles build with Tailwind CSS to our site built with the Zola static site generator, and are ready get stylin'!
Got any questions, tips or special workflows of your own? Comment down below! (Github account required.)