Getting Tailwind CSS to integrate nicely with Svelte + Sapper

Ryandi Tjia 7 May 2020

#Before we begin

Scouring the internet I found 3 different ways of integrating Tailwind with Sapper.

The first one is from Tailwind’s official Sapper setup example. It’s a showstopper because it doesn’t let me use @apply nor @screen directives inside Svelte’s style tags.

The second one involves using rollup-plugin-postcss. It doesn’t exhibit the ‘slow dev start and rebuild’ issue that the third one has, but it triggers reload twice every edit that could sometimes bug out and require manual reload—we aren’t animals here. That, coupled with inability to inline critical CSS, caused me to look elsewhere.

The third one is what I ended up using and is the basis of this post. This method is pretty much taken from this template repo and modified to my liking. Special thanks to Jacob Babich for creating this template.

Update

Someone with a quite hefty tailwind.config.js told me that their dev server takes 5 mins to start, even though it’s on a pretty beefy machine.

If you too have an extensive tailwind.config.js, turn back and use the second method because this third method doesn’t scale. There’s no accompanying blog post, but you can refer to this older checkpoint of my Sapper boilerplate.

Pros of (not unique to) this method:

  • Purge unused Tailwind classes in production builds (build and export)
  • Use Tailwind’s @apply and @screen directives inside Svelte’s style tag
  • No additional script to run
  • Inline critical CSS (optional)
  • Nest CSS rule (optional)

Cons:

  • Doesn’t work with Svelte’s class: directive yet (incorrectly purged in production). I created an issue about this. You can get around this by implementing purge manually (Jacob’s template above does so).
  • Dev server takes a while to boot up (> 15s). This slowness doesn’t seem to be present in production builds.
  • Editing the global CSS file (where you initiate Tailwind) takes very long to rebuild (> 10s). Depending on your workflow, you might not have to do this often.

So, it’s established that I’m not a very good salesperson. But hopefully I convinced you, no buyer’s remorse beyond this point. Let’s get to it.

#Setting up the project

As our previous post, we’ll be following Sapper’s getting started doc to scaffold our new project.

TerminalShell
npx degit "sveltejs/sapper-template#rollup" my-app
cd my-app
npm install

Add the required devDependencies, short explanation about each dependency to follow.

TerminalShell
npm install -D tailwindcss svelte-preprocess postcss postcss-load-config postcss-import postcss-preset-env cssnano
  • tailwindcss: self-explanatory
  • svelte-preprocess: tells Svelte compiler to do extra things, in this case preprocess style tags as PostCSS
  • postcss: required by svelte-preprocess to do PostCSS things
  • postcss-load-config: tells svelte-preprocess to read postcss.config.js
  • postcss-import: allows splitting CSS files and importing them together using @import syntax
  • postcss-preset-env: includes autoprefixer and allows nesting
  • cssnano: minifies code in production

#Creating Tailwind config

TerminalShell
npx tailwind init

This will create an empty tailwind.config.js file at the root of your project. We’ll come back to this file when we set up purging later.

#Setting up preprocessing

This is the bulk of the work. First, create postcss.config.js at the root of the project and paste in the following:

postcss.config.jsJavaScript
const mode = process.env.NODE_ENV
const dev = mode === 'development'

module.exports = {
	plugins: [
		require('postcss-import')(),
		require('tailwindcss')(),
		require('postcss-preset-env')({
			// Full list of features: https://github.com/csstools/postcss-preset-env/blob/master/src/lib/plugins-by-id.js#L36
			features: {
				'nesting-rules': true, // delete if you don’t want nesting (optional)
			},
		}),

		// Minify if prod
		!dev &&
			require('cssnano')({
				preset: ['default', { discardComments: { removeAll: true } }],
			}),
	],
}

Next, create svelte.config.js, also at the root of your project and paste in the following:

svelte.config.jsJavaScript
const preprocess = require('svelte-preprocess')

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

If you’re using VS Code with Svelte extension, this file also tells them that we’re using PostCSS. I’m not 100% sure how it works but it makes VS Code throw fewer errors so that’s a win in my book.

Then, open up rollup.config.js and add these in (do not paste and replace the whole file):

rollup.config.jsJavaScript
// …
const { preprocess } = require('./svelte.config') // <-- this line is new

export default {
	client: {
		// …
		plugins: [
			// …
			svelte({
				dev,
				hydratable: true,
				emitCss: true, // change to false to inline critical CSS (optional)
				preprocess: [preprocess], // <-- this line is new
			}),
			// …
		],
		// …
	},

	server: {
		// …
		plugins: [
			// …
			svelte({
				generate: 'ssr',
				dev,
				preprocess: [preprocess], // <-- this line is new
			}),
		],
	},
}

Hold on just a little bit longer, we’re almost there.

#Remove default global CSS

Tailwind comes with a pretty good reset, it’d be wise to remove Sapper’s default so nothing surprises us.

src/template.htmlHTML
<!-- Remove this line -->
<link rel="stylesheet" href="global.css" />

And delete the CSS file since it’s no longer needed.

TerminalShell
rm static/global.css

With that out of the way, we’re good to add Tailwind in.

#Finally adding Tailwind

Create a new CSS file: src/assets/global.pcss. The naming and placement aren’t special, it could be src/main.css if you so wish.

I have multiple CSS files (one for @font-face, one for NProgress, another for Markdown), hence the folder. And the .pcss extension is for VS Code to show a different icon, and to remind me that these aren’t normal CSS files.

src/assets/global.pcssCSS
@import 'tailwindcss/base';
/* @import './your-custom-base.css'; */
@import 'tailwindcss/components';
/* @import './your-custom-components.css'; */
@import 'tailwindcss/utilities';
/* @import './your-custom-utilities.css';  */

If you followed my previous post on NProgress, this is where you’d do @import 'nprogress/nprogress.css'; or @import './my-custom-nprogress.css';

Next, create a Svelte component that will import it as global style.

src/components/GlobalStyle.svelteSvelte
<style global>
	@import '../assets/global.pcss';
</style>

Then import this component in our layout.

src/routes/_layout.svelteSvelte
<script>
	// …
	import GlobalStyle from '../components/GlobalStyle.svelte'
	// …
</script>

<GlobalStyle />

<!-- … -->

Why a separate component instead of putting the style tag inside of layout? Remember the slowdown I mentioned earlier? If you do so, editing even the tiniest of markup inside of layout will trigger the long rebuild.

Phew. Congratulations, Tailwind should now work.

#Taking Tailwind for a ride

Run the dev server. It’ll take some time, don’t say I didn’t warn you.

TerminalShell
npm run dev

Replace the entirety of src/routes/about.svelte with the following:

src/routes/about.svelteSvelte
<script>
	let isToggled = false

	function toggle() {
		isToggled = !isToggled
	}
</script>

<style>
	.custom-btn {
		@apply bg-red-500 px-4 py-3 shadow rounded;
	}

	.secret-text {
		@apply opacity-0 pointer-events-none;

		&.is-shown {
			@apply opacity-100 pointer-events-auto;
		}
	}

	dl {
		& dt {
			@apply text-xl;
		}

		& dd {
			@apply font-normal text-2xl;

			@screen lg {
				@apply font-bold;
			}

			&:hover {
				@apply text-blue-700;
			}
		}
	}
</style>

<div class="space-y-16">
	<div class="text-3xl text-purple-800 italic">
		I’m a purple-colored italic text
	</div>

	<div>
		<button class="custom-btn" on:click="{toggle}">
			I’m a red button. Click me
		</button>

		<div class="secret-text" class:is-shown="{isToggled}">
			You found a secret text
		</div>
	</div>

	<!-- If you enable nesting -->
	<dl>
		<dt>What am I</dt>
		<dd>
			I’m a text that should turn bold at large viewport. And blue when hovered.
		</dd>
	</dl>
</div>

Visit localhost:3000/about and check if it works. This test case pretty much covers what I need to build UI quickly.

But, if you make a production build right now, you’ll ship a ton of unused CSS to the user (> 1MB total). Let’s rectify this with purge.

#Setting up purge

Tailwind v1.4 comes with built-in PurgeCSS that we’ll make use of instead of setting up manually.

First, it needs to detect that NODE_ENV is set to production. Sapper doesn’t do this by default for its sapper build and sapper export commands. So we have to make a slight edit.

package.jsonJSON
"build": "NODE_ENV=production sapper build --legacy",
"export": "NODE_ENV=production sapper export --legacy",

Next, open up tailwind.config.js and modify the line that says purge: [].

tailwind.config.jsJavaScript
purge: ['./src/**/*.svelte', './src/**/*.html'],

That’s it. The generated CSS should now be < 10KB in size.

#Before you go

I have a hunch that what’s causing the dev slowdown is rollup bundling the whole of Tailwind and its sourcemaps to its JS file. I’m seeing 15–30 MB of __sapper__/dev/client/client.somehash.js file. If we could just disable sourcemaps I think this slowdown could be halved or better.

If you happen to know a fix, please reach me at Twitter @ryanditjia. Also feel free to holler if you have difficulty getting any of this to work. My DMs are open.