When I decided to try to base my current personal website on Eleventy, I didn’t want to reinvent the wheel: I tested all the Eleventy starters built with Tailwind CSS that I could find in Starter Projects from the documentation.
Many of the starters seemed to integrate Tailwind CSS in a contrived way. Also, some of them seemed to assume that no one updates Tailwind’s configuration on the fly while working on a website. That’s why I integrated Eleventy with Tailwind CSS and Alpine.js myself. I have reason to believe that you’ll like the simplicity of my solution.
Good design is as little design as possible.
—Dieter Rams, 10 Principles for Good Design
If you’re uninterested in the details, feel free to grab my starter and jump right in.
Getting started
I’m going to assume you have a general understanding of Tailwind CSS, HTML, JavaScript, Nunjucks, the command line, and npm.
Let’s start by with a new a folder, then cd
to it in the command line, and initialize it with a package.json
file:
npm init -y
Now we can install Eleventy and Tailwind CSS:
npm install -D @11ty/eleventy tailwindcss@latest
We need to create a page to test whether we’ve successfully set things up. In a real use case, our pages will use templates, so we’ll do that here as well. That’s where Nunjucks fits into the mix, serving as a templating engine.
Let’s make a new file called index.njk
in the project folder. We’ll designate it as the homepage:
{% extends "_includes/default.njk" %}
{% block title %}It does work{% endblock %}
{% block content %}
<div class="fixed inset-0 flex justify-center items-center">
<div>
<span class="text-change">Good design</span><br/>
<span class="change">is<br/>as little design<br/>as possible</span>
</div>
</div>
{% endblock %}
Basic templating
Now let’s create a new folder in the project folder called _includes
(and yes, the folder name matters). Inside this new folder, we’ll create a file called default.njk
that we’ll use as the default template for our layout. We’ll keep things simple with a basic HTML boilerplate:
<!DOCTYPE html>
<html lang="en">
<head>
<title>
{% block title %}Does it work?{% endblock %}
</title>
<meta charset="UTF-8"/>
{% if description %}
<meta name="description" content="{{description}}"/>
{% endif %}
<meta http-equiv="x-ua-compatible" content="ie=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover"/>
<link rel="stylesheet" href="/style.css?v={% version %}"/>
{% block head %}{% endblock %}
</head>
<body>
{% block content %}
{{ content | safe }}
{% endblock %}
</body>
</html>
Configuring Tailwind CSS
Let’s take care of a test for Tailwind CSS in as few moves as possible. First, create a new subfolder called styles
and a file in it called tailwind.config.js
:
module.exports = {
content: ['_site/**/*.html'],
safelist: [],
theme: {
extend: {
colors: {
change: 'transparent',
},
},
},
plugins: [],
}
Then, create a file called tailwind.css
in that same styles
folder:
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer utilities {
.change {
color: transparent;
}
}
Starting and building the project
Now let’s create another new file in the same root directory called .gitignore
. This will allow us to define what files to skip when committing the project to a repo, like on GitHub:
_site/
.DS_Store
node_modules/
Next, we’ll create a file called .eleventy.js
(note the leading dot) that basically configures Eleventy, telling it what files to watch and where to save its work:
const now = String(Date.now())
module.exports = function (eleventyConfig) {
eleventyConfig.addWatchTarget('./styles/tailwind.config.js')
eleventyConfig.addWatchTarget('./styles/tailwind.css')
eleventyConfig.addPassthroughCopy({ './_tmp/style.css': './style.css' })
eleventyConfig.addShortcode('version', function () {
return now
})
};
We can now update the package.json
file with all of the scripts we need to start and build the site during development. The dependencies should already be there from the initial setup.
{
"scripts": {
"start": "eleventy --serve & npx tailwindcss -i styles/tailwind.css -c styles/tailwind.config.js -o _site/style.css --watch",
"build": "ELEVENTY_PRODUCTION=true eleventy && NODE_ENV=production npx tailwindcss -i styles/tailwind.css -c styles/tailwind.config.js -o _site/style.css --minify"
},
"devDependencies": {
"@11ty/eleventy": "^1.0.0",
"tailwindcss": "^3.0.0"
}
}
Hey, great job! We made it. Let’s officially start the site:
npm start
Open the page http://localhost:8080
in your browser. It’s not gonna look like much, but check out the page title in the browser tab:

We can still do a little more checking to make sure everything’s good. Open up /styles/tailwind.config.js
and change the transparent
color value to something else, say black
. Tailwind’s configuration should reload, along with the page in your browser.
Don’t lose sight of your browser and edit /styles/tailwind.css
by changing transparent
to black
again. Your CSS file should reload and refresh in your browser.
Now we can work nicely with Eleventy and Tailwind CSS!
Optimizing the output
At this point, Tailwind CSS works with Eleventy, but the generated HTML isn’t perfect because it contains stuff like redundant newline characters. Let’s clean it up:
npm install -D html-minifier
Add the following line to the beginning of the .eleventy.js
file:
const htmlmin = require('html-minifier')
We also need to configure htmlmin
in .eleventy.js
as well:
eleventyConfig.addTransform('htmlmin', function (content, outputPath) {
if (
process.env.ELEVENTY_PRODUCTION &&
outputPath &&
outputPath.endsWith('.html')
) {
let minified = htmlmin.minify(content, {
useShortDoctype: true,
removeComments: true,
collapseWhitespace: true,
});
return minified
}
return content
})
We’re using a transform here which is an Eleventy thing. Transforms can modify a template’s output. At this point, .eleventy.js
should look like this:
const htmlmin = require('html-minifier')
const now = String(Date.now())
module.exports = function (eleventyConfig) {
eleventyConfig.addWatchTarget('./styles/tailwind.config.js')
eleventyConfig.addWatchTarget('./styles/tailwind.css')
eleventyConfig.addShortcode('version', function () {
return now
})
eleventyConfig.addTransform('htmlmin', function (content, outputPath) {
if (
process.env.ELEVENTY_PRODUCTION &&
outputPath &&
outputPath.endsWith('.html')
) {
let minified = htmlmin.minify(content, {
useShortDoctype: true,
removeComments: true,
collapseWhitespace: true,
})
return minified
}
return content
})
}
Let’s run npm start
once again. You’ll see that nothing has changed and that’s because optimization only happens during build. So, instead, let’s try npm run build
and then look at the _site
folder. There shouldn’t be a single unnecessary character in the index.html
file. The same goes for the style.css
file.
A project built like this is now ready to deploy. Good job! 🏆
Integrating Alpine.js
I decided to switch to Eleventy from Gatsby.js because it just felt like too much JavaScript to me. I’m more into the reasonable dose of vanilla JavaScript mixed with Alpine.js. We won’t get into the specifics of Alpine.js here, but it’s worth checking out Hugo DiFrancesco’s primer because it’s a perfect starting point.
Here’s how we can install it to our project from the command line:
npm install -D alpinejs
Now we need to update .eleventy.js
with this to the function that passes things through Alpine.js:
eleventyConfig.addPassthroughCopy({
'./node_modules/alpinejs/dist/cdn.js': './js/alpine.js',
})
Lastly, we’ll open up _includes/default.njk
and add Alpine.js right before the closing </head>
tag:
<script src="/js/alpine.js?v={% version %}"></script>
We can check if Alpine is working by adding this to index.njk
:
{% extends "_includes/default.njk" %}
{% block title %}It does work{% endblock %}
{% block content %}
<div class="fixed inset-0 flex justify-center items-center">
<div>
<span class="text-change">Good design</span><br/>
<span class="change">is<br/>as little design<br/>as possible</span><br/>
<span x-data="{message:'🤖 Hello World 🤓'}" x-text="message"></span>
</div>
</div>
{% endblock %}
Launch the project:
npm start
If Alpine.js works, you’ll see “Hello World” in your browser. Congratulations, times two! 🏆🏆
I hope you can see how quick it can be to set up an Eleventy project, including integrations with Nunjucks for templating, Tailwind for styles and Alpine.js for scripts. I know working with new tech can be overwhelming and even confusing, so feel free to email me at [email protected]
if you have problems starting up or have an idea for how to simplify this even further.
Great article! Tailwind has Purge “built in” the latest version which can be configured in the tailwind config file. Maybe we could leverage that instead of doing it in the postcss config? https://tailwindcss.com/docs/controlling-file-size
Hi Mario! I’m glad you liked the article. :)
Thank you for the suggestion – a valid point!
Leveraging “built in” Purge makes sense if you don’t use PostCSS for anything else in your setup. In this case, the setup is as minimalistic as possible, so I’m not using PostCSS only for Tailwind, but also for autoprefixer, to minify the CSS after removing the unused parts (and these are only the elements listed in the article; I use PostCSS in my projects for other things as well).
Also, using PostCSS here is not my idea. :) The Tailwind’s documentation states:
“For most projects, you’ll want to add Tailwind as a PostCSS plugin in your build chain.”
This article is an attempt to get Eleventy, Tailwind, and Alpine to work together in the most “natural” way possible.
One more time: Thank you, Mario! :)
If I’m missing something, please let me know.
It doesn’t sound like Mario is suggesting you do away with PostCSS completely, but rather leverage Tailwind’s built-in PurgeCSS to simplify things even further.
tailwind.config.js:
postcss.config.js:
tailwind.css:
Nice article Greg. Minor remark, but still a remark: Was following along and already used to working with tailwind when I thought it was odd I couldn’t simply test a bg-blue-300 or something. None of the colors, and then I saw how you defined the colors in tailwind.config. It’s easier (better?) to do it via extend.
theme: {
extend: {
colors: {
cyan: “#9cdbff”,
},
},
},
since the unused ones will be stripped away anyway. Perhaps using the new purge option in tailwind itself even.
https://tailwindcss.com/docs/controlling-file-size/
Thank you, guys! You convinced me. I updated the article and the starter. Mario, Justin, Tom, thank you. :)
Tailwind is weird. Build a mega package of CSS and then define what’s not in it?
When developing its really useful to have all the available utility styles at your fingertips. But you don’t need every style available on production – only the ones you’re using.
Really appreciate this article. Ive thought for a while that this stack would be a brilliant in terms of customisability but also lightweight. Will definitely be using this guide soon for some personal projects.
Hey Greg!
Great article! However, you no longer need the purge dependency separately! If you run your PostCSS script with the NODE_ENV set to production, and configure the purge key in your tailwind file, it will handle it for you!
My personal website uses this exact same stack, and is configured with less dependencies and all CSS and JS passing through Rollup for minification!
It is available here: https://github.com/mattwaler/website
Hi, didn’t quite understand why we rendering styles to _tmp and then copying to _site, why not render straight into _site, thanks!
Hi! :) A great question. Without the workaround you mentioned, we couldn’t enjoy a neat hot reloading experience.
Got a problem :(
I do this but then
bg-indigo-500
doesn’t work. I can only usebg-indigo
.Thanks for this starter, just started with Eleventy and got familiar with Tailwind CSS and naturally also to Alpine JS. Looking forward to build and design the project I had in mind for years. Thanks.
Nice work, just maybe add the README.md to the .eleventyignore and I think the js output file is still not minified
So I thought it would be a good idea to copy this idea of appending a version tag onto the link to the style sheet.
But checking out the source of the different pages that were generated by eleventy, all using the same
head.njk
component which is included into mybase.njk
I get to see a different version tag for the same CSS file. I guess this result makes it not interesting to use this tactic.Here’s a few of the generated version tags: 1614537697642, 1614537697643, 1614537697634; for three different pages.
The tag should only be generated once for one build, now it’s generated every time a single page is written. Maybe with a slightly different approach this could work. Maybe it’s just me, I don’t know.
Would it be possible to generate this tag into a variable, either in eleventy.js or another .js file outside and then use this same tag thoughout the complete (re)build until another (re)build?
in eleventy.js
in default/head.njk
Hi Michel! Thank you for your comment. You’re right, I’m sorry, I updated the article. :)
I was going through your tutorial and could not make alpine work. update .eleventy.js with
the alpine node module is now called cdn.js not alpine.js
Thank you, eb! Updated. :)
Thanks for this article Greg. As a relative newcomer (only used Hugo before) to the Eleventy/Tailwind/Alpine combination, I have found it a breath of fresh air.
I just have one question. Your “npm start” command creates a version controlled css file and yet the build command just outputs to a styles.css file. What would be the adjusted build command to incorporate version control/cachebusting into my final _site folder?
Sorry for this, probably easily answered question, but, as I say, I am relatively new to this and can find no simple assist online.
Thanks for a great question, Richard! :)
<link rel="stylesheet" href="/style.css?v={% version %}"/>
in the template is responsible for version control/cache busting.Also, if you host your website on – for example – Netlify, AFAIK you don’t need
?v={% version %}
for version control/cache busting.Nice combo. Alpine looks like something I would like. I do like Tailwind when it is all set up, but it is such a hassle to do.
I really wanted this to work but I can’t pull it off. The config variant from “at this point, .eleventy.js should look like this” is hitting me with 3 errors in VSC. Declaration or statement expected, but I can’t see it.
Thank you, Little! :) I just tested it in VSC on macOS. No errors. I suspect the source of the problem is one of the extensions you have installed in your VSC. Good luck! :)
Hm, I don’t understand why this setup doesn’t work on Windows. Probably some issue with server sync, or ./ in the paths but CORS error No-sniff appear and can’t import css. Maybe it is just me, but for me it doesn’t work :(
@Ivan the problem with Windows is the chaining of commands with “&” or “&&” in the start and build scripts. Try this:
and change the scripts to:
There’s a small bug in your code,
const now = String(Date.now())
being outside of the function means that the value is not recalculated after the initial load. subsequent hot reloads will only trigger the function. that line should sit inside the function being exported.