Solid Sapper setup with PostCSS and Tailwind

I spent some time trying to get Sapper to play nice with PostCSS and Tailwind CSS

While it's straight forward to integrate PostCSS and Tailwind into plain Svelte projects, Sapper is a totally different beast. There are many moving parts. Rollup configuration is super complex, code is generated. It can be hard to grasp what's going on.

The Problem#

I needed to integrate PostCSS together with Sapper. Meanwhile it's not so hard to integrate plain Tailwind CSS into Sapper, turns out integrating PostCSS together with TailwindCSS requires a little more work. After trying a few different approaches I finally landed on something that works for me.

Why PostCSS?#

Plain CSS can take you far, but I often prefer to use Tailwind CSS. I find it really nice to work with declarative CSS instead of writing everything from scratch. I like Tailwind as it is, but I often use a few other PostCSS plugins too that help me work with Tailwind CSS more efficiently. Maybe a better word would be "augment" and not "help."

How Sapper manages CSS#

Sapper has an internal router built-in. Which is helpful. The router intercepts all link clicks and fetches each page individually when you visit it. When you click on the link that leads to another page in your app, Sapper will fetch the page in the background and replace the content in your Sapper app.

It will actually put the content into the slot in the src/routes/_layout.svelte page. That's how it's setup in the official boilerplate at least.

Sapper injects the styles for different components and pages when you navigate between the pages. When you visit a page, Sapper will fetch that page and also inject the style for that page and for the components it uses into the head tag of the document.

Sapper and Svelte scope CSS classes defined in the components to the components themselves, reducing the risk of overriding the CSS.

To understand more read the blog post The zen of Just Writing CSS.

It's actually a really nice feature that you get out of the box in Svelte! You can see that by inspecting elements in the dev tools console. Each styled element will have a svelte⁠-⁠[hash] class defined on it.

The Solution#

After wrestling with rollup-plugin-postcss for some time, I gave up and went with the simplest setup possible.

Instead of trying to integrate PostCSS into Rollup itself, I moved PostCSS processing outside of Rollup's pipeline. It's fast too, because the processing is done outside Rollup.

Here is how I did it.

Create a Sapper project

In order to fully understand what's needed, we will start from scratch by creating a standard Sapper project.

$ npx degit sveltejs/sapper-template#rollup sapper-with-postcss
$ cd sapper-with-postcss && npm i

You can now start the app by running npm run dev.

Setting up Tailwind#

Let's add Tailwind and Tailwind's typography plugin that we will use to style the blog posts.

$ npm add -D tailwindcss @tailwindcss/typography
$ npx tailwindcss init

We now need to replace Tailwind's configuration file with this.

// tailwind.config.js

module.exports = {
future: {
removeDeprecatedGapUtilities: true,
experimental: {
uniformColorPalette: true,
extendedFontSizeScale: true,
applyComplexClasses: true,
purge: {
// needs to be set if we want to purge all unused
// @tailwind/typography styles
mode: 'all',
content: ['./src/**/*.svelte', './src/**/*.html'],
theme: {
container: {
center: true,
extend: {},
variants: {},
plugins: [require('@tailwindcss/typography')],

Next thing we need to do is to create Tailwind's base file. We will put it in src/assets folder, that you need to create first, and we will name it global.pcss.

We are using .pcss extension just to distinguish that it's a PostCSS file. It's not something you have to do. Plain .css extension works just a good. I like to distinguish PostCSS files from plain CSS.

/* global.pcss */

@tailwind base;

body {
@apply bg-indigo-100;

@tailwind components;
@tailwind utilities;

Alright. Now that we are done with Tailwind configuration, let's wire it up into our PostCSS pipeline.

Setting up PostCSS with Tailwind

First things first. We need to install PostCSS cli and a few PostCSS plugins that we will use.

$ npm add -D postcss-cli
$ npm add -D cssnano postcss-load-config postcss-preset-env

Next, we need to create PostCSS configuration file in the project's root folder.

// postcss.config.js

const tailwind = require('tailwindcss');
const cssnano = require('cssnano');
const presetEnv = require('postcss-preset-env')({
features: {
// enable nesting
'nesting-rules': true,

const plugins =
process.env.NODE_ENV === 'production'
? [tailwind, presetEnv, cssnano]
: [tailwind, presetEnv];

module.exports = { plugins };

Cool! We are almost there. Theoretically, we have everything we need. We just need to wire everything up.

PostCSS in Svelte files#

Actually, I forgot something. We want to style our Svelte components with Tailwind and PostCSS too. In order for that to work we need to use the good ol' svelte⁠-⁠preprocess plugin.

$ npm add -D svelte-preprocess

Let's cheat a bit. We will create a svelte.config.js and setup the preprocessor there. Svelte config is needed for the editors to be able to work correctly. Syntax highlighting, intellisense and all those things.

We will later re-use the exported preprocessor in our Rollup config to keep things DRY.

// svelte.config.js

const autoProcess = require('svelte-preprocess');

module.exports = {
preprocess: autoProcess({ postcss: true }),

There are a few different ways to setup Svelte prepocessor, but I found this the most minimal. The reason it works is that we installed postcss-load-config plugin earlier. It will automatically load postcss.config.js file if it exists. No need to require it in our code!

Now that we have finished setting up the preprocessor, we need to import it in our Rollup config.

// rollup.config.js

// svelte.config is a CommonJS module
// it needs to be imported this way
const { preprocess } = require('./svelte.config');

// add preprocess to Svelte plugin in client section
hydratable: true,
emitCss: true,
preprocess, // <-- add this

// add preprocess to Svelte plugin in server section
generate: 'ssr',
hydratable: true,
preprocess, // <-- add this

Phew! Everything is now configured correctly. Hopefully.

Adjust your NPM scripts

Last thing we need to do is to wire everything together. We will do it by changing the scripts section in our package.json.

We have to install npm-run-all cli utility for this to work - npm add -D npm-run⁠-⁠all.

"scripts": {
"dev": "run-p watch:*",
"watch:css": "postcss src/assets/global.pcss -o static/global.css -w",
"watch:dev": "sapper dev",
"build": "run-s build:css build:sapper",
"build:css": "NODE_ENV=production postcss src/assets/global.pcss -o static/global.css",
"build:sapper": "sapper build --legacy",
"build:export": "sapper export --legacy",
"export": "run-s build:css build:export",
"start": "node __sapper__/build",
"serve": "serve ___sapper__/export",
"cy:run": "cypress run",
"cy:open": "cypress open",
"test": "run-p --race dev cy:run"

This requires some explanation. You can see that we have a watch:css script. What it does is replaces Sappers static/global.css with our Tailwind base file. We also need to explicitly set NODE_ENV to "production" in build:css since we are doing our PostCSS processing outside Sapper. It's needed by Tailwind in order to purge unused CSS styles from its base file.

Be careful not to set NODE_ENV to production in the Sapper build and export scripts. If you do, and you set any :global styles in your components, they will be purged leading to missing styles.

Oh, just another tip. If you what to use a background image in you CSS put it in the static folder. You can then use it your CSS like this.

.hero {

Test-driving the new setup#

To check that Tailwind and PostCSS works in Svelte components, replace your src/routes/index.svelte with this code.

<!-- index.svelte -->

<style lang="postcss">

.btn {
@apply bg-red-500 text-red-100 uppercase tracking-wide font-semibold
text-4xl px-4 py-3 shadow-lg rounded;

<title>Sapper project template</title>

<div class="space-y-10 text-center">
<h1 class="text-7xl uppercase font-bold">Great success!</h1>
<button class="btn">DO NOT PRESS THIS BUTTON!</button>

You can see that we set lang="postcss" in the style tag. That's not something that is required, PostCSS will still be processed. It's only so that editor understands it's dealing with PostCSS.

To see Tailwind's typography plugin in action change src/routes/blog/[slug].svelte to the code below.

<script context="module">
export async function preload({ params, query }) {
const res = await this.fetch(`blog/${params.slug}.json`);
const data = await res.json();

if (res.status === 200) {
return { post: data };
} else {
this.error(res.status, data.message);


export let post;


<!-- prose is a class from Tailwind typography plugin -->
<div class='prose prose-lg'>

{@html post.html}

And ... we are finally done!


Below you can see the setup in action running on Vercel. Make sure to check out individual blog posts to see Tailwind's typography plugin in action.

Oh, and please don't press that button. Don't say I didn't warn you!

sapper with postcss and tailwind

Live demo:

Mentioned and used plugins#


Implementing PostCSS in Sapper becomes clear when you understand how Sapper deals with CSS files.

We set up two separate PostCSS pipelines in our example app. First is processing Sapper's global CSS file. Second is replacing Sapper's component styling with PostCSS. We didn't actually change the way Sapper handles and serves CSS files, we only replaced it with PostCSS. Maybe "augmented" is a better word.

You can find the full code here

Now go and create some beautifully styled apps!