CSS Code-splitting with Tailwind

At build time, Tailwind tracks the classes used in a project, adding the correct CSS to the build output. This gets bundled as one CSS dependency regardless of how, where, or why styles are used. Visitors benefit from loading more CSS on the first visit when they visit subsequent pages. However, most visitors might browse marketing pages, but never visit the app itself. As the CSS bundle grows, this can impact the load performance of such pages.

Tailwind V4 brought CSS-first configuration, which can be leveraged to create multiple bundles. A downside to this is visitors loading duplicate CSS if they visit pages using different bundles. Therefore, so choosing where to create separate bundles is important. A common pattern will be splitting app pages from marketing pages, and the one demonstrated here.

Project Structure

In this example, I’m using a basic Astro project, with a landing page, and an app. To define what files tailwind should scan for each bundle, the project should separate components used in different bundles.

src/
├── components/
│ ├── app/
│ │ ├── AppComponent.astro
│ │ └── AppLayout.astro
│ ├── shared/
│ │ └── SharedComponent.astro
│ └── site/
│ └── SiteLayout.astro
├── pages/
│ ├── app/
│ │ └── index.astro
│ └── index.astro
└── styles/
├── app.css
├── shared.css
└── site.css

In the example above, the app.css and site.css CSS configs are imported in their respective layouts. Logic used in both, such as theme variables, can be extracted to a shared.css file, then imported in each config.

Sourcing Files

app.css and site.css are acting as the two config files. Each of these will use a slightly different approach, but will source only the files needed for that bundle.

For site.css, this includes any pages and components excluding app/. These files can be excluded using @source not.

site.css
@import "tailwindcss";
@import "./shared.css";
@source not "../components/app";
@source not "../pages/app";

app.css only needs to include pages and components within the app/ directories, plus any shared components. Instead of using @source not to exclude files, source(none) can be used as a param on the tailwind import directive to explicitly declare only what is needed with @source.

app.css
@import "tailwindcss" source(none);
@import "./shared.css";
@source "../components/app";
@source "../components/shared";
@source "../pages/app";

Excluding files can be confusing if a project contains multiple bundles. Take care to ensure each config correctly sources all files used by pages including this bundle. Otherwise, this could lead to pages with missing styles.

Output

Two CSS files are emitted by the build. One for each entrypoint. With this, visitors only viewing marketing pages will not download the CSS required only for the app.

dist/
├── _astro/
│ ├── index.CT3EdmN3.css
│ ├── index.MFEO9qw_.css
├── app/
│ └── index.html
└── index.html

Summary

As with most posts here (when I decide to publish something…), this came about as a solution to a problem I was working on. Along the way I ran into a similar post by Toward Studio titled How to use multiple Tailwind CSS configs with ViteJS. Their post is a couple years old, but may be helpful for Vite projects still using Tailwind V3.

You can find the full project for this example on GitHub at TheOtterlord/tailwind-multiple-bundles.