If you use Tailwind CSS with Jekyll, you have probably seen this advice:
Add
<script src="https://cdn.tailwindcss.com"></script>to your<head>.
It works instantly, which is great for prototyping. But it comes with a real cost in production: that CDN script downloads the entire Tailwind library — about 3 MB — on every page load, scans your HTML at runtime, and then applies the styles. Your visitors pay that cost every single visit.
The proper fix is to compile Tailwind ahead of time so only the CSS classes you actually use end up in the final file. A typical compiled output is around 10–100 KB — a 30× reduction.
The problem is that now you have two commands to run every time you want to build your site:
npx tailwindcss -i ./assets/css/tailwind.src.css -o ./assets/css/tailwind.css --minify
bundle exec jekyll build
Forget the first one and your site ships with stale CSS. This tutorial shows you how to make Jekyll run that command for you automatically.
What We’re Building
A small Jekyll plugin — 12 lines of Ruby — that hooks into Jekyll’s build lifecycle and compiles Tailwind before Jekyll processes any files. After this setup:
- You run
bundle exec jekyll buildas normal - Tailwind compiles automatically first
- You never think about it again
Prerequisites
Before starting, make sure you have:
- A Jekyll site (any version)
- Node.js installed (
node -vshould print a version number) tailwindcssinstalled in your project:
npm install --save-dev tailwindcss
Step 1 — Create the Input CSS File
Tailwind needs a source file to start from. Create assets/css/tailwind.src.css:
@tailwind base;
@tailwind components;
@tailwind utilities;
This is just three lines that tell Tailwind which parts of its library to include. You will never edit this file directly.
Step 2 — Create tailwind.config.js
Tailwind needs to know which files to scan for CSS classes. Create tailwind.config.js at the root of your project:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./_layouts/**/*.html',
'./_includes/**/*.html',
'./_posts/**/*.{html,md}',
'./*.html',
'./*.md',
],
theme: {
extend: {},
},
plugins: [],
}
The content array tells Tailwind which files to scan. It reads every class name it finds (like text-lg, bg-blue-500, flex) and includes only those in the output. Everything else is stripped out — that is where the size reduction comes from.
If you have pages in subdirectories, add them here. For example:
'./tools/**/*.html',
'./magazine/**/*.html',
Step 3 — Create the Jekyll Plugin
Create the file _plugins/tailwind_compiler.rb:
Jekyll::Hooks.register :site, :after_reset do |site|
src = File.join(site.source, 'assets', 'css', 'tailwind.src.css')
out = File.join(site.source, 'assets', 'css', 'tailwind.css')
tw = File.join(site.source, 'node_modules', '.bin', 'tailwindcss')
if File.exist?(tw) && File.exist?(src)
Jekyll.logger.info 'TailwindCSS:', 'Compiling...'
system("#{tw} -i #{src} -o #{out} --minify")
Jekyll.logger.info 'TailwindCSS:', 'Done'
else
Jekyll.logger.warn 'TailwindCSS:', 'Skipping — binary or src not found'
end
end
That’s it. Let’s walk through what each line does.
Jekyll::Hooks.register :site, :after_reset — Jekyll fires different events during a build. :after_reset runs right at the start, before Jekyll reads any files. That makes it the perfect moment to compile CSS so Jekyll can copy the freshly compiled file into _site/.
File.join(site.source, ...) — site.source is the root folder of your Jekyll project. This builds the correct path to each file regardless of where Jekyll is running from.
if File.exist?(tw) && File.exist?(src) — A safety check. If node_modules is not installed or the source file is missing, the plugin logs a warning and skips instead of crashing the build. This means the plugin is safe to commit even for team members who have not run npm install yet.
system("#{tw} -i #{src} -o #{out} --minify") — Runs the Tailwind CLI with the --minify flag so the output is as small as possible.
Step 4 — Update Your Layout
Replace the CDN script tag in your layout’s <head>:
<!-- Before -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- After -->
<link rel="stylesheet" href="/assets/css/tailwind.css">
Step 5 — Add the Output File to .gitignore
The compiled tailwind.css is a build artifact — it should be generated, not committed:
# .gitignore
assets/css/tailwind.css
If you are deploying to a platform that does not run npm install and jekyll build (for example, you just push static files), leave tailwind.css out of .gitignore and commit it instead. The plugin will still re-compile it on every local build.
Step 6 — Run It
bundle exec jekyll build
You will see Tailwind’s output in the build log:
TailwindCSS: Compiling...
TailwindCSS: Done
Generating... done in 1.234 seconds.
The compiled assets/css/tailwind.css now exists and Jekyll copies it into _site/assets/css/tailwind.css as part of the normal build.
Why Not Just Use npm run build?
You can chain commands in package.json:
"build": "npx tailwindcss -i ... -o ... --minify && bundle exec jekyll build"
This works, but it means:
- Everyone on the project has to remember to use
npm run buildinstead ofbundle exec jekyll build - CI/CD pipelines need to be configured to use the npm script
jekyll serve --livereloadwill not recompile Tailwind automatically
The plugin approach keeps everything inside Jekyll’s own build process. You use the same jekyll build command you always have.
Deployment
If you are deploying to Cloudflare Pages or Netlify, set the build command to:
npm install && bundle exec jekyll build
npm install puts the Tailwind binary in node_modules/. The plugin finds it there and compiles automatically during jekyll build.
Summary
| Step | What you created |
|---|---|
| 1 | assets/css/tailwind.src.css — Tailwind entry point |
| 2 | tailwind.config.js — tells Tailwind which files to scan |
| 3 | _plugins/tailwind_compiler.rb — auto-runs before every build |
| 4 | Updated <head> to use compiled CSS instead of CDN |
| 5 | Added compiled output to .gitignore |
Your Jekyll site now ships with a CSS file that contains only the styles it actually uses, compiled automatically on every build — no extra commands, no manual steps.