[Go Make Things] Rethinking modular CSS and build-free design systems

One of my big goals with Kelp, my UI library for people who love HTML, was build-free customization.

I want to use the best of modern HTML, CSS, and JavaScript to let people customize the hell out it without every having to open up terminal or run a build step.

Today, I wanted to share how I decided to approach modularizing CSS and customizing which components get loaded. Let's dig in!

The code structure

Kelp's CSS is very modular.

I have a bunch of small CSS files in a bunch of directories that make it a lot easier for me to manage the code.

For development purposes, I'm using the native CSS @import rule. This lets me edit code and immediately see my changes without waiting for a build.

Going into this project, I had viewed that as a "development code only" state.

A build for me but not for thee

My original plan was for me to have a build step that would prevent an end-user from needing one.

I had planned to add ESBuild to my deploy process. Then, I would compile my modular CSS files into a handful of pre-compiled bundles…

  • kelp.complete.css with literally everything.
  • kelp.core.css with just the core files.
  • kelp.layout.css with only the layout files.
  • kelp.utilities.css with only the utility classes.
  • And so on and so on…

This would make it easier for developers to just grab what they wanted without having to grab everything or run their own build.

But then I started working with the code more…

Lots of little edge-cases

As I started working with Kelp to build the docs site itself, I started thinking through various use-cases…

  • What if someone wants just the .container and .grid classes, but none of the other layout options?
  • What if someone needs just three or four utility class options, but not all of them?
  • How I handle the myriad of component options as they get built out?

It would be really easy to end up with dozens or even hundreds of compiled files to account for all of the different combinations some might need.

Technically, it can be done.

But is that actually a better user experience? I don't think it is.

Embracing the @import

For as long as I've been a developer, the CSS @import rule has been frowned upon.

CSS is render blocking, and in the HTTP/1 world, where only two files could be downloaded at a time, lots of CSS @import's could create substantial render delays.

But in the HTTP/2 world, I've started to question this old wisdom.

I tested this a few years ago and found that using @import resulted in render times that were 400ms slower on a 3G connection.

Harry Roberts from CSS Wizardry ran a similar test and found larger systems, like Bootstrap, were substantially slower.

But I'm using this approach on the KelpUI.com website right now, and it's fast as hell!

On a high-speed connection, the site starts rendering in about 1100ms (1.1s) on initial render, and 700ms (0.7s) on susequent renders. On a 3g connection, it takes 4s on initial render, and 1.7s on subsequent renders.

Would a concatenated build be faster? Yes, definitely.

Is using @import slow? I personally don't think so.

The benefits of this approach

I love the experience of being able to just edit a file, open a browser, and see my changes live!

But this approach has another really nice benefit: "buildless builds."

Let's say you want to use Kelp, and want to customize it to only include the components you need.

In a build-process world, you'd need to…

  1. Delete or comment out the @import statements for files you don't want.
  2. Open terminal.
  3. Run npm install
  4. Run npm build

But using @import natively, there's a single step: comment out what you don't want.

/* All of the core files, then... */    /* Layout */  @import "./layout/containers.css";  @import "./layout/grid.css";  /*@import "./layout/stack.css";  @import "./layout/split.css";  @import "./layout/cluster.css";  @import "./layout/sidecar.css";*/    /* Components */  @import "./components/callouts.css";  /*@import "./components/avatar.css";  @import "./components/skeleton.css";*/    /* Utilities */  /*@import "./utilities/fill.css";  @import "./utilities/aspect-ratios.css";*/  @import "./utilities/text.css";  @import "./utilities/size.css";  @import "./utilities/margin.css";  @import "./utilities/padding.css";  /*@import "./utilities/flex.css";  @import "./utilities/gap.css";  @import "./utilities/align.css";  @import "./utilities/justify.css";*/  @import "./utilities/accessibility.css";

The developer experience on this is hard to beat, and the end-user experience is relatively the same (though I know some folks will disagree with me about that).

Some caveats and considerations

There are some caveats to everything I've written up to this point.

Don't nest your @import statements

My arguments about performance being relatively the same completely fall apart if you start nesting your @import statements.

For example, if my main kelp.css file looked like this…

@import "./layers.css";  @import "./native.css"  @import "./layouts.css"  @import "./components.css"  @import "./utilities.css"

And then each of those had its own @import statements…

@import "./layout/containers.css";  @import "./layout/grid.css";  @import "./layout/stack.css";  @import "./layout/split.css";  @import "./layout/cluster.css";  @import "./layout/sidecar.css";

Performance would start to bog down quick because of the waterfall effect this would have on render blocking.

Keep things to a single layer and you're probably good.

Big libraries will take longer

Kelp is relatively light and lean.

If the CSS was in the 100s of KBs, concatenated CSS would start to look a lot better in terms of performance.

I suspect much of "how close" the performance data was is related to how small the overall bundle is.

While still slower than a concatenated file, loading a bunch of modular files with the <link> element is marginally faster than @import.

<link rel="stylesheet" type="text/css" href="./css/layers.css">  <link rel="stylesheet" type="text/css" href="./css/theme/palette.css">  <link rel="stylesheet" type="text/css" href="./css/theme/colors.css">  <link rel="stylesheet" type="text/css" href="./css/theme/fonts.css">  <link rel="stylesheet" type="text/css" href="./css/theme/sizes.css">  <!-- ... -->

It gets really tedious, but if you have a CMS that can handle it for you, maybe not so bad!

What about cache busting?

The way the JSDelivr CDN works, all of the @import files have a versioned URL. Updating to a new version automatically clears the old browser cache.

If you're downloading Kelp and running your own setup locally, though, cache busting is a problem you'd need to figure out.

A buildless builder

I just finished building a custom theme builder, and am now focused on a buildless custom install tool.

The idea is that you can select which components you want, and it will spit out a custom CSS file for you.

I want to add cache-busting query parameters, let users toggle between @import and <link>, use the CDN or a local set, and maybe even create an option to concatenate everything by using fetch() to grab the CSS files and merge them together.

What I very much want to maintain, though, is that goal I had from the start: Easily customized. No build step.

Learn more about Kelp at KelpUI.com.

Like this? A Go Make Things membership is the best way to support my work and help me create more free content.

Cheers,
Chris

Want to share this with others or read it later? View it in a browser.

Share :

Facebook Twitter Google+ Lintasme

Related Post:

0 Komentar untuk "[Go Make Things] Rethinking modular CSS and build-free design systems"

Back To Top