Design Systems CLI: Multi-Build CSS
In January of this year, my team at Intuit open-sourced our project Design Systems CLI. It’s a tool that simplifies every part of developing components and libraries in a design system. Since then, we’ve been hard at work scaling and improving the project for teams inside and outside of Intuit. I’m excited to announce that we recently released some new features to simplify CSS imports and enable multiple CSS builds for design systems.
Building and publishing CSS can be complicated, which is why many developers choose to use a CSS-in-JS solution like Styled Components. This post is not focused on the trade-offs of that architecture, but our team has found PostCSS to be more ideal for performance and customization in our design systems. This is why our initial release of DS-CLI was focused on making PostCSS-based builds simple to configure for any design system.
What We Had
The build system we initially created for Design Systems CLI treated your JavaScript and CSS as separate artifacts. The only overlap was CSS Modules mapping the classnames between the two.
// Example directory structure for built design system component.dist/
esm/
index.js
commonjs/
index.js
main.css
For someone using that component, they would then need to import JavaScript and CSS separately:
import { Example } from '@ds/example';
import '@ds/example/dist/main.css';
This is a fairly standard pattern that you’ll see in the documentation for projects like Webpack loaders and NextJS. While CSS and JavaScript are two separate things, in a design system component they can rarely function without the other also being loaded. This leads to some common problems:
- People have to remember to import CSS. We would frequently have teams using a component for the first time without knowing they needed to import the CSS. This was even harder for teams who use a mix of CSS and CSS-in-JS which does not need imports.
- Your CSS imports rely on the folder structure of your built output, which could change.
- Nested CSS causes problems. Many of our components depend on other components —
Icon
is a common one. In order to load both we either needed to: include theIcon
CSS in every other component’s CSS (creating duplicates), ask people to import dependent CSS (poor developer experience), or create tools to help.
This problem led us to build babel-plugin-include-styles, which automatically would search for design systems components and import dependent CSS. This worked in many cases, but required extra configuration and would break if you had multiple versions of a component. It was still far from ideal, and as a result some developers would have to import multiple CSS files to import a single component.
A New Set of Constraints
With these problems in mind we started thinking about changes we could make to the build process to make CSS easier to import, as small as possible, and widely compatible. We settled on the following priorities:
- Automatic imports by default.
- Compatibility with most popular project templates like CRA and Next.
- Support for multi-build CSS for optimization.
Why multiple builds?
While the first two constraints probably seem straightforward given our issues with importing, “multi-build CSS” was a feature we had been looking at implementing in DS-CLI for a while. Our team builds React components that are used across Intuit products like TurboTax, QuickBooks, Mint, and ProConnect. Each of those products has a different theme built with our PostCSS plugin, and components can switch them at any time. Some teams write UI that exists in multiple products, so they need all of these themes, but many do not. In those cases we thought it made sense to allow teams to only load the theme they use, which cut the CSS size but about 20% in our rough estimates. Multiple builds would also allow us to publish CSS that wasn’t scoped with CSS-modules, or was Internet Explorer compatible.
Progress
Meeting all of these constraints required changes to the DS-CLI build process, our theming architecture, and our babel packages. The major enhancements are documented below:
PostCSS Themed Changes
In versions v2.0.0 and v2.0.1 of PostCSS Themed we added forceSingleTheme
andoptimizeSingleTheme
parameters which allow you to only include one theme in your CSS output. This allows use-cases like ours, where we would like to build all themes by default but also build single-theme files if teams want to optimize load time.
Design Systems CLI — CSS Imports
In v2.3.0 we introduced a new cssImport
flag for the build plugin. This is a very simple feature, which automatically inserts a CSS import into only the ESM build of your component. The ESM part is important, because we found some tools that use CommonJS (like test-runners) don’t really like CSS imports by default. If you have a bundler set up with a style-loader, however, the ESM import should work without any other setup.
Design Systems CLI — Multi-Build CSS
Also in v2.3.0, we added a cssConfigs
flag that allows you to generate multiple CSS outputs. For this to work, you specify the path to multiple PostCSS configs and DS-CLI will build all of them and name them accordingly. For our project the configurations were 90% the same so we defined a base one that all of the variants extended. Each of our PostCSS configurations specifies a different forceSingleTheme
in order to generate a CSS file per product theme.
Design Systems CLI — Switching Between CSS Builds
Also in v2.3.0, we added babel-plugin-replace-styles
. This package is how we manage teams switching out the main.css
for a different output from the “multi-build” setup. This plugin configures babel to overwrite the default import statements to point to a different file, if it exists.
Design Systems CLI — NextJS Support for CSS imports
Finally, in v2.6.0 we added the next-esm-css
package which makes automatic CSS imports compatible with NextJS.
Overall, these changes allowed us to meet all of the goals we outlined for our CSS builds. CSS importing is easier for design systems users, and should work with little configuration in most projects. We can also produce multiple CSS builds to segment features, while allowing component consumers to switch between the different files easily.
Example
To give you an example of the power of this setup, I’ll highlight how this works in practice.
Let’s say you have two products, Foo and Bar, which each have a different color scheme. You can quickly scaffold a design system to output themed components in a variety of ways.
// Example directory structure for built design system component.components/
Button/
src/
index.ts
theme.ts
postcss/
main/
postcss.config.js
foo/
postcss.config.js
bar/
postcss.config.js
When built, this will produce a component with three CSS output files:
dist/
esm/
index.js // Automatically imports main.css
commonjs/
index.js
main.css // Includes Foo and Bar themes
foo.css // Includes only Foo themes
bar.css // Includes only Bar themes
Anyone using your components will get both themes included by default, and the component can switch between them. However, a team working on the Foo product can exclude the Bar theme using babel-plugin-replace-styles
, which will instead load foo.css
. Doing this saves them ~20% of the CSS cost and removes the need for a theme provider.
Hopefully this illustrates some of the possibilities using the new features! Components are easier to use, and there are lots of optimization opportunities you can make when you have multiple build artifacts.
Contact
We’d love to hear your thoughts! If you have any suggestions or ideas using this system, feel free to contact us! You can open issues on Design Systems CLI or reach out to Tyler Krupicka, Andrew Lisowski, or Adam Dierkens on Twitter. Thanks for reading!