Tag: General

Multi-Thumb Sliders: General Case

The first part of this two-part series detailed how we can get a two-thumb slider. Now we’ll look at a general multi-thumb case, but with a different and better technique for creating the fills in between the thumbs. And finally, we’ll dive into the how behind the styling a realistic 3D-looking slider and a flat one.

Article Series:

  1. Multi-Thumb Sliders: Particular Two-Thumb Case
  2. Multi-Thumb Sliders: General Case (This Post)

A better, more flexible approach

Let’s say that, on a wrapper pseudo-element that covers the same area as the range inputs, we stack left-to–right linear-gradient() layers corresponding to each thumb. Each gradient layer is fully opaque (i.e. the alpha is 1) from the track minimum up to the thumb’s mid-line, after which it’s fully transparent (i.e. the alpha is 0).

Note that the RGB values don’t matter because all we care about are the alpha values. I personally use the red (for the fully opaque part) and transparent keywords in the code because they do the job with the least amount of characters.

How do we compute the gradient stop positions where we go from fully opaque to fully transparent? Well, these positions are always situated between a thumb radius from the left edge and a thumb radius from the right edge, so they are within a range that’s equal to the useful width (the track width, minus the thumb diameter).

This means we first add a thumb radius.Then we compute the progress by dividing the difference between the current thumb’s position and the minimum to the difference (--dif) between the maximum and the minimum. This progress value is a number in the [0, 1] interval — that’s 0 when the current thumb position is at the slider’s minimum, and 1 when the current thumb position is at the slider’s maximum. To get where exactly along that useful width interval we are, we multiply this progress value with the useful width.

The position we’re after is the sum between these two length values: the thumb radius and how far we are across the useful width interval.

The demo below allows us to see how everything looks stacked up in the 2D view and how exactly the range inputs and the gradients on their parent’s pseudo-element get layered in the 3D view. It’s also interactive, so we can drag the slider thumbs and see how the corresponding fill (which is created by a gradient layer on its parent’s pseudo-element) changes.

See the Pen by thebabydino (@thebabydino) on CodePen.

The demo is best viewed in Chrome and Firefox.

Alright, but simply stacking these gradient layers doesn’t give us the result we’re after.

The solution here is to make these gradients mask layers and then XOR them (more precisely, in the case of CSS masks, this means to XOR their alphas).

If you need a refresher on how XOR works, here’s one: given two inputs, the output of this operation is 1 if the input values are different (one of them is 1 and the other one is 0) and 0 if the input values are identical (both of them are 0 or both of them are 1)

The truth table for the XOR operation looks as follows:

Inputs Output
A B
0 0 0
0 1 1
1 0 1
1 1 0

You can also play with it in the following interactive demo, where you can toggle the input values and see how the output changes:

See the Pen by thebabydino (@thebabydino) on CodePen.

In our case, the input values are the alphas of the gradient mask layers along the horizontal axis. XOR-ing multiple layers means doing so for the first two from the bottom, then XOR-ing the third from the bottom with the result of the previous XOR operation and so on. For our particular case of left-to-right gradients with an alpha equal to 1 up to a point (decided by the corresponding thumb value) and then 0, it looks as illustrated below (we start from the bottom and work our way up):

SVG illustration. Illustrates the process described in the following three paragraphs.
How we XOR the gradient layer alphas (Demo).

Where both layers from the bottom have an alpha of 1, the resulting layer we get after XOR-ing them has an alpha of 0. Where they have different alpha values, the resulting layer has an alpha of 1. Where they both have an alpha of 0, the resulting layer has an alpha of 0.

Moving up, we XOR the third layer with the resulting layer we got at the previous step. Where both these layers have the same alpha, the alpha of the layer that results from this second XOR operation is 0. Where they have different alphas, the resulting alpha is 1.

Similarly, we then XOR the fourth layer from the bottom with the layer resulting from the second stage XOR operation.

In terms of CSS, this means using the exclude value for the standard mask-composite and the xor value for the non-standard -webkit-mask-composite. (For a better understanding of mask compositing, check out the crash course.)

This technique gives us exactly the result we want while also allowing us to use a single pseudo-element for all the fills. It’s also a technique that works for any number of thumbs. Let’s see how we can put it into code!

In order to keep things fully flexible, we start by altering the Pug code such that it allows to add or remove a thumb and update everything else accordingly by simply adding or removing an item from an array of thumb objects, where every object contains a value and a label (which will be only for screen readers):

- let min = -50, max = 50; - let thumbs = [ -   { val: -15, lbl: 'Value A' },  -   { val: 20, lbl: 'Value B' },  -   { val: -35, lbl: 'Value C' },  -   { val: 45, lbl: 'Value D' } - ]; - let nv = thumbs.length;  .wrap(role='group' aria-labelledby='multi-lbl'        style=`$ {thumbs.map((c, i) => `--v$ {i}: $ {c.val}`).join('; ')};               --min: $ {min}; --max: $ {max}`)   #multi-lbl Multi thumb slider:     - for(let i = 0; i < nv; i++)       label.sr-only(for=`v$ {i}`) #{thumbs[i].lbl}       input(type='range' id=`v$ {i}` min=min value=thumbs[i].val max=max)       output(for=`v$ {i}` style=`--c: var(--v$ {i})`)

In the particular case of these exact four values, the generated markup looks as follows:

<div class='wrap' role='group' aria-labelledby='multi-lbl'       style='--v0: -15; --v1: 20; --v2: -35; --v3: 45; --min: -50; --max: 50'>   <div id='multi-lbl'>Multi thumb slider:</div>   <label class='sr-only' for='v0'>Value A</label>   <input type='range' id='v0' min='-50' value='-15' max='50'/>   <output for='v0' style='--c: var(--v0)'></output>   <label class='sr-only' for='v1'>Value B</label>   <input type='range' id='v1' min='-50' value='20' max='50'/>   <output for='v1' style='--c: var(--v1)'></output>   <label class='sr-only' for='v2'>Value C</label>   <input type='range' id='v2' min='-50' value='-35' max='50'/>   <output for='v2' style='--c: var(--v2)'></output>   <label class='sr-only' for='v3'>Value D</label>   <input type='range' id='v3' min='-50' value='45' max='50'/>   <output for='v3' style='--c: var(--v3)'></output> </div>

We don’t need to add anything to the CSS or the JavaScript for this to give us a functional slider where the <output> values get updated as we drag the sliders. However, having four <output> elements while the wrapper’s grid still has two columns would break the layout. So, for now, we remove the row introduced for the <output> elements, position these elements absolutely and only make them visible when the corresponding <input> is focused. We also remove the remains of the previous solution that uses both pseudo-elements on the wrapper.

.wrap {   /* same as before */   grid-template-rows: max-content #{$ h}; /* only 2 rows now */    &::after {     background: #95a;     // content: ''; // don't display for now     grid-column: 1/ span 2;     grid-row: 3;   } }  input[type='range'] {   /* same as before */   grid-row: 2; /* last row is second row now */ }  output {   color: transparent;   position: absolute;   right: 0; 	   &::after {     content: counter(c);     counter-reset: c var(--c);   } }

We’ll be doing more to prettify the result later, but for now, here’s what we have:

See the Pen by thebabydino (@thebabydino) on CodePen.

Next, we need to get those thumb to thumb fills. We do this by generating the mask layers in the Pug and putting them in a --fill custom property on the wrapper.

//- same as before - let layers = thumbs.map((c, i) => `linear-gradient(90deg, red calc(var(--r) + (var(--v$ {i}) - var(--min))/var(--dif)*var(--uw)), transparent 0)`);  .wrap(role='group' aria-labelledby='multi-lbl'    style=`$ {thumbs.map((c, i) => `--v$ {i}: $ {c.val}`).join('; ')};      --min: $ {min}; --max: $ {max};     --fill: $ {layers.join(', ')}`)   // - same as before

The generated HTML for the particular case of four thumbs with these values can be seen below. Note that this gets altered automatically if we add or remove items from the initial array:

<div class='wrap' role='group' aria-labelledby='multi-lbl'    style='--v0: -15; --v1: 20; --v2: -35; --v3: 45;      --min: -50; --max: 50;     --fill:        linear-gradient(90deg,          red calc(var(--r) + (var(--v0) - var(--min))/var(--dif)*var(--uw)),          transparent 0),        linear-gradient(90deg,          red calc(var(--r) + (var(--v1) - var(--min))/var(--dif)*var(--uw)),          transparent 0),        linear-gradient(90deg,          red calc(var(--r) + (var(--v2) - var(--min))/var(--dif)*var(--uw)),          transparent 0),        linear-gradient(90deg,          red calc(var(--r) + (var(--v3) - var(--min))/var(--dif)*var(--uw)),          transparent 0)'>   <div id='multi-lbl'>Multi thumb slider:</div>   <label class='sr-only' for='v0'>Value A</label>   <input type='range' id='v0' min='-50' value='-15' max='50'/>   <output for='v0' style='--c: var(--v0)'></output>   <label class='sr-only' for='v1'>Value B</label>   <input type='range' id='v1' min='-50' value='20' max='50'/>   <output for='v1' style='--c: var(--v1)'></output>   <label class='sr-only' for='v2'>Value C</label>   <input type='range' id='v2' min='-50' value='-35' max='50'/>   <output for='v2' style='--c: var(--v2)'></output>   <label class='sr-only' for='v3'>Value D</label>   <input type='range' id='v3' min='-50' value='45' max='50'/>   <output for='v3' style='--c: var(--v3)'></output> </div>

Note that this means we need to turn the Sass variables relating to dimensions into CSS variables and replace the Sass variables in the properties that use them:

.wrap {   /* same as before */   --w: 20em;   --h: 4em;   --d: calc(.5*var(--h));   --r: calc(.5*var(--d));   --uw: calc(var(--w) - var(--d));   background: linear-gradient(0deg, #ccc var(--h), transparent 0);   grid-template: max-content var(--h)/ var(--w);   width: var(--w); }

We set our mask Oo the wrapper’s ::after pseudo-element:

.wrap {   /* same as before */      &::after {     content: '';     background: #95a;     grid-column: 1/ span 2;     grid-row: 2;      /* non-standard WebKit version */     -webkit-mask: var(--fill);     -webkit-mask-composite: xor;      /* standard version, supported in Firefox */     mask: var(--fill);     mask-composite: exclude;   } }

Now we have exactly what we want and the really cool thing about this technique is that all we need to do to change the number of thumbs is add or remove thumb objects (with a value and a label for each) to the thumbs array in the Pug code — absolutely nothing else needs to change!

See the Pen by thebabydino (@thebabydino) on CodePen.

Prettifying tweaks

What we have so far is anything but a pretty sight. So let’s start fixing that!

Option #1: a realistic look

Let’s say we want to achieve the result below:

Screenshot. The track and fill are the same height as the thumbs. The track looks carved into the page, while the fill and the thumb have a convex look inside it.
The realistic look we’re after.

A first step would be to make the track the same height as the thumb and round the track ends. Up to this point, we’ve emulated the track with a background on the .wrap element. While it’s technically possible to emulate a track with rounded ends by using layered linear and radial gradients, it’s really not the best solution, especially when the wrapper still has a free pseudo-element (the ::before).

.wrap {   /* same as before */   --h: 2em;   --d: var(--h);      &::before, &::after {     border-radius: var(--r);     background: #ccc;     content: '';     grid-column: 1/ span 2;     grid-row: 2;   }      &::after {     background: #95a;      /* non-standard WebKit version */     -webkit-mask: var(--fill);     -webkit-mask-composite: xor;      /* standard version, supported in Firefox */     mask: var(--fill);     mask-composite: exclude;   } }

See the Pen by thebabydino (@thebabydino) on CodePen.

Using ::before to emulate the track opens up the possibility of getting a slightly 3D look:

<pre rel="SCSS"><code class="language-scss">.wrap {   /* same as before */      &::before, &::after {     /* same as before */     box-shadow: inset 0 2px 3px rgba(#000, .3);   }      &::after {     /* same as before */     background:        linear-gradient(rgba(#fff, .3), rgba(#000, .3))       #95a;   } }

I’m by no means a designer, so those values could probably be tweaked for a better looking result, but we can already see a difference:

See the Pen by thebabydino (@thebabydino) on CodePen.

This leaves us with a really ugly thumb, so let’s fix that part as well!

We make use of the technique of layering multiple backgrounds with different background-clip (and background-origin) values.

@mixin thumb() {   border: solid calc(.5*var(--r)) transparent;   border-radius: 50%; /* make circular */   box-sizing: border-box; /* different between Chrome & Firefox */   /* box-sizing needed now that we have a non-zero border */   background:      linear-gradient(rgba(#000, .15), rgba(#fff, .2)) content-box,      linear-gradient(rgba(#fff, .3), rgba(#000, .3)) border-box,      currentcolor;   pointer-events: auto;   width: var(--d); height: var(--d); }

I’ve described this technique in a lot of detail in an older article. Make sure you check it out if you need a refresher!

The above bit of code would do close to nothing, however, if the currentcolor value is black (#000) which it is right now. Let’s fix that and also change the cursor on the thumbs to something more fitting:

input[type='range'] {   /* same as before */   color: #eee;   cursor: grab;      &:active { cursor: grabbing; } }

The result is certainly more satisfying than before:

See the Pen by thebabydino (@thebabydino) on CodePen.

Something else that really bothers me is how close the label text is to the slider. We can fix this by introducing a grid-gap on the wrapper:

.wrap {   /* same as before */   grid-gap: .625em; }

But the worst problem we still have are those absolutely positioned outputs in the top right corner. The best way to fix this is to introduce a third grid row for them and move them with the thumbs.

The position of the thumbs is computed in a similar manner to that of the sharp stops of the gradient layers we use for the fill mask.

Initially, we place the left edge of the outputs along the vertical line that’s a thumb radius --r away from the left edge of the slider. In order to middle align the outputs with this vertical line, we translate them back (to the left, in the negative direction of the x-axis, so we need a minus sign) by half of their width (50%, as percentage values in translate() functions are relative to the dimensions of the element the transform is applied to).

In order to move them with the thumbs, we subtract the minimum value (--min) from the current value of the corresponding thumb (--c), divide this difference by the difference (--dif) between the maximum value (--max) and the minimum value (--min). This gives us a progress value in the [0, 1] interval. We then multiply this value with the useful width (--uw), which describes the real range of motion.

.wrap {   /* same as before */   grid-template-rows: max-content var(--h) max-content; }  output {   background: currentcolor;   border-radius: 5px;   color: transparent;   grid-column: 1;   grid-row: 3;   margin-left: var(--r);   padding: 0 .375em;   transform: translate(calc((var(--c) - var(--min))/var(--dif)*var(--uw) - 50%));   width: max-content;      &::after {     color: #fff;     content: counter(c);     counter-reset: c var(--c);   } }

See the Pen by thebabydino (@thebabydino) on CodePen.

This looks much better at a first glance. However, a closer inspection reveals that we still have a bunch of problems.

The first one is that overflow: hidden cuts out a bit of the <output> elements when we get to the track end.

In order to fix this, we must understand what exactly overflow: hidden does. It cuts out everything outside an element’s padding-box, as illustrated by the interactive demo below, where you can click the code to toggle the CSS declaration.

See the Pen by thebabydino (@thebabydino) on CodePen.

This means a quick fix for this issue is to add a big enough lateral padding on the wrapper .wrap.

padding: 0 2em;

We’re styling our multi-thumb slider in isolation here, but, in reality, it probably won’t be the only thing on a page, so, if spacing is limited, we can invert that lateral padding with a negative lateral margin.

If the nearby elements still have the default have position: static, the fact that we’ve relatively positioned the wrapper should make the outputs go on top of what they overlap, otherwise, tweaking the z-index on the .wrap should do it.

The bigger problem is that this technique we’ve used results in some really weird-looking <output> overlaps when were dragging the thumbs.

Increasing the z-index when the <input> is focused on the corresponding <output> as well solves the particular problem of the <output> overlaps:

input[type='range'] {   &:focus {     outline: solid 0 transparent; 		     &, & + output {       color: darkorange;       z-index: 2;     }   } }

However, it does nothing for the underlying issue and this becomes obvious when we change the background on the body, particularly if we change it to an image one, as this doesn’t allow the <output> text to hide in it anymore:

See the Pen by thebabydino (@thebabydino) on CodePen.

This means we need to rethink how we hide the <output> elements in the normal state and how we reveal them in a highlight state, such as :focus. We also want to do this without bloating our CSS.

The solution is to use the technique I described about a year ago in the “DRY Switching with CSS Variables” article: use a highlight --hl custom property where the value is 0 in the normal state and 1 in a highlight state (:focus). We also compute its negation (--nothl).

* {   --hl: 0;   --nothl: calc(1 - var(--hl));   margin: 0;   font: inherit }

As it is, this does nothing yet. The trick is to make all properties that we want to change in between the two states depend on --hl and, if necessary, its negation (code>–nothl).

$ hlc: #f90;  @mixin thumb() {   /* same as before */   background-color: $ hlc; }  input[type='range'] {   /* same as before */   filter: grayScale(var(--nothl));   z-index: calc(1 + var(--hl));      &:focus {     outline: solid 0 transparent;          &, & + output { --hl: 1; }   } }  output {   /* same grid placement */   margin-left: var(--r);   max-width: max-content;   transform: translate(calc((var(--c) - var(--min))/var(--dif)*var(--uw))); 	   &::after {     /* same as before */     background:        linear-gradient(rgba(#fff, .3), rgba(#000, .3))       $ hlc;     border-radius: 5px;     display: block;     padding: 0 .375em;     transform: translate(-50%) scale(var(--hl));   } }

See the Pen by thebabydino (@thebabydino) on CodePen.

We’re almost there! We can also add transitions on state change:

$ t: .3s;  input[type='range'] {   /* same as before */   transition: filter $ t ease-out; }  output::after {   /* same as before */   transition: transform $ t ease-out; }

See the Pen by thebabydino (@thebabydino) on CodePen.

A final improvement would be to grayscale() the fill if none of the thumbs are focused. We can do this by using :focus-within on our wrapper:

.wrap {   &::after {     /* same as before */     filter: Grayscale(var(--nothl));     transition: filter $ t ease-out;   } 	   &:focus-within { --hl: 1; } }

And that’s it!

See the Pen by thebabydino (@thebabydino) on CodePen.

Option #2: A flat look

Let’s see how we can get a flat design. For example:

Screenshot. All slider components are flat, no shadows or gradients. The track and fill are narrower than the thumbs and middle aligned with these. The track has a striped background. The thumbs are scaled down and reveal circular holes in the track around them in their unfocused state.
The flat look we’re after.

The first step is to remove the box shadows and gradients that give our previous demo a 3D look and make the track background a repeating gradient.:

See the Pen by thebabydino (@thebabydino) on CodePen.

The size change of the thumb on :focus can be controlled with a scaling transform with a factor that depends on the highlight switch variable (--hl).

@mixin thumb() {   /* same as before */   transform: scale(calc(1 - .5*var(--nothl)));   transition: transform $ t ease-out; }

See the Pen by thebabydino (@thebabydino) on CodePen.

But what about the holes in the track around the thumbs?

The mask compositing technique is extremely useful here. This involves layering radial gradients to create discs at every thumb position and, after we’re done with them, invert (i.e. compositing with a fully opaque layer) the result to turn those discs into holes.

SVG illustration. Shows that XOR-ing a bunch of radial-gradient() layers gives us a layer with opaque discs and everything else transparent and when, we xor this resulting layer with a fully opaque one, this fully opaque layer acts as an inverter, turning the discs into transparent holes in an otherwise fully opaque layer.
How we XOR the gradient layer alphas (Demo).

This means altering the Pug code a bit so that we’re generating the list of radial gradients that create the discs corresponding to each thumb. In turn, we’ll invert those in the CSS:

//- same as before - let tpos = thumbs.map((c, i) => `calc(var(--r) + (var(--v$ {i}) - var(--min))/var(--dif)*var(--uw))`); - let fill = tpos.map(c => `linear-gradient(90deg, red $ {c}, transparent 0)`); - let hole = tpos.map(c => `radial-gradient(circle at $ {c}, red var(--r), transparent 0)`)  .wrap(role='group' aria-labelledby='multi-lbl'    style=`$ {thumbs.map((c, i) => `--v$ {i}: $ {c.val}`).join('; ')};      --min: $ {min}; --max: $ {max};     --fill: $ {fill.join(', ')};      --hole: $ {hole.join(', ')}`)   // -same wrapper content as before

This generates the following markup:

<div class='wrap' role='group' aria-labelledby='multi-lbl'    style='--v0: -15; --v1: 20; --v2: -35; --v3: 45;      --min: -50; --max: 50;     --fill:        linear-gradient(90deg,          red calc(var(--r) + (var(--v0) - var(--min))/var(--dif)*var(--uw)),          transparent 0),        linear-gradient(90deg,          red calc(var(--r) + (var(--v1) - var(--min))/var(--dif)*var(--uw)),          transparent 0),         linear-gradient(90deg,           red calc(var(--r) + (var(--v2) - var(--min))/var(--dif)*var(--uw)),           transparent 0),         linear-gradient(90deg,           red calc(var(--r) + (var(--v3) - var(--min))/var(--dif)*var(--uw)),           transparent 0);       --hole:         radial-gradient(circle           at calc(var(--r) + (var(--v0) - var(--min))/var(--dif)*var(--uw)),           red var(--r), transparent 0),         radial-gradient(circle           at calc(var(--r) + (var(--v1) - var(--min))/var(--dif)*var(--uw)),           red var(--r), transparent 0),         radial-gradient(circle           at calc(var(--r) + (var(--v2) - var(--min))/var(--dif)*var(--uw)),           red var(--r), transparent 0),         radial-gradient(circle           at calc(var(--r) + (var(--v3) - var(--min))/var(--dif)*var(--uw)),           red var(--r), transparent 0)'>   <!-- same content as before --> </div>

In the CSS, we set a mask on both pseudo-elements and give a different value for each one. We also XOR the mask layers on them.

In the case of ::before, the mask is the list of radial-gradient() discs XOR-ed with a fully opaque layer (which acts as an inverter to turn the discs into circular holes). For ::after, it’s the list of fill linear-gradient() layers.

.wrap {   /* same as before */      &::before, &::after {     content: '';     /* same as before */          --mask: linear-gradient(red, red), var(--hole);      /* non-standard WebKit version */     -webkit-mask: var(--mask);     -webkit-mask-composite: xor;      /* standard version, supported in Firefox */     mask: var(--mask);     mask-composite: exclude;   } 	   &::after {     background: #95a;     --mask: var(--fill);   } }

See the Pen by thebabydino (@thebabydino) on CodePen.

The final step is to adjust the track, fill height, and middle align them vertically within their grid cell (along with the thumbs):

.wrap {   /* same as before */      &::before, &::after {     /* same as before */     align-self: center;     height: 6px;   } }

We now have our desired flat multi-thumb slider!

See the Pen by thebabydino (@thebabydino) on CodePen.

The post Multi-Thumb Sliders: General Case appeared first on CSS-Tricks.

CSS-Tricks

, , ,

A Quick CSS Audit and General Notes About Design Systems

I’ve been auditing a ton of CSS lately and thought it would be neat to jot down how I’m going about doing that. I’m sure there are a million different ways to do this depending on the size and scale of your app and how your CSS works under the hood, so please take all this with a grain of salt.

First a few disclaimers: at Gusto, the company I work for today, our engineers and designers all write in Sass and use webpack to compile those files into CSS. Our production environment minifies all that code into a single CSS file. However, our CSS is made up of three separate domains. so I downloaded them all to my desktop because I wanted to test them individually.

Here’s are those files and what they do:

  • manifest.css: a file that’s generated from all our Sass functions, mixins and contains all of our default HTML styles and utility classes.
  • components.css: a file that consists of our React components such as Button.scss, Card.scss, etc. This and manifest.css both come from our Component Library repo and are imported into our main app.
  • app.css: a collection of styles that override our components and manifest. Today, it exists in our main application repo.

After I downloaded everything, I threw them into an S3 bucket and ran them through CSS Stats. (I couldn’t find a command line tool that I liked, so I decided stuck with this tool.) The coolest thing about CSS Stats is that it provides a ton of clarity about the health and quality of a site’s CSS, and in turn, a design system. It does this by showing the number of unique font-size and unique background-color CSS declarations there are, as well as a specificity graph for that particular CSS file.

I wanted to better understand our manifest.css file first. As I mentioned, this file contains all our utility classes (such as padding-top-10px and c-salt-500) as well as our normalize and reset CSS files, so it’s pretty foundational for everything else. I started digging through the results:

There are some obvious issues here, like the fact that there are 101 unique colors and 115 unique background colors. Why is this a big deal? Well, it’s a little striking to me because our team had already made a collection of Sass functions to output a very specific number of colors. In our Figma UI Kit and variables_color.scss (which gets compiled into our manifest file, we declare a total of 68 unique colors:

So, where are all these extra colors coming from? Well, I assume that they’re coming from Bootstrap. Back when we started building the application, we hastily built on top of Bootstrap’s styles without refactoring things as we went. There was a certain moment when this started to hurt as we found visual inconsistencies across our application and hundreds of lines of code being written that simply overrode Bootstrap. In a rather gallant CSS refactor, I removed Bootstrap’s CSS from our main application and archived it inside manifest.css, waiting for the day when we could return to it and refactor it all.

These extra colors are likely come from that old Bootstrap file, but it’s probably worth investigating some more. Anyway, the real issue with this for me is that my understanding of the design system is different from what’s in the front-end. That’s a big problem! If my understanding of the design system is different from how the CSS works, then there’s enormous potential for engineers and designers to pick up on the wrong patterns and for confusion to disseminate across our organization. Think about the extra bloat and lack of maintainability, not to mention other implications.

I was reading Who Are Design Systems For? by Matthew Ström and perked up when he quotes a talk by Julie Ann-Horvath where she’s noted as saying, “a design system doesn’t exist until it’s in production.” Following the logic, it’s clear the design system I thought we had didn’t actually exist.

Going back to manifest.css though: the specificity graph for this file should be perfectly gradual and yet there are some clear spikes that show there’s probably a bit more CSS that needs to be refactored in there:

Anyway, next up is our components.css. Remember that’s the file that our styles for our components come from so I thought beforehand that it’s bound to be a little messier than our manifest file. Throwing it into CSS Stats returns the following:

CSS-Stats shows some of the same problems — like too many font sizes (what the heck is going on with that giant font size anyway?) — but there are also way too many custom colors and background-colors. I already had a hunch about what the biggest issue with this CSS file was before I started and I don’t think the problem is not shown in this data here at all.

Let me explain.

A large number of our components used to be Bootstrap files of one kind or another. Take our Accordion.jsx React component, for instance. That imports an accordion.css file which is then compiled with all the other component’s CSS into a components.css file. The problem with this is that some Accordion styles affect a lot more than just that component. CSS from this this file bleeds into other patterns and classes that aren’t tied to just one component. It’s sort of like a poison in our system and that impacts our team because it makes it difficult to reliably make changes to a single component. It also leads to a very fragile codebase.

So I guess what I’m saying here is that tools like CSS Stats are wondrous things to help us check core vital signs for CSS health, but I don’t think they’ll ever really capture the full picture.

Anyway, next up is the app.css file:

This is the “monolith” — the codebase that our design systems team is currently trying to better understand and hopefully refactor into a series of flexible and maintainable React components that others can reuse again and again.

What worries me about this codebase is the specificity of it all what happens when something changes in the manifest.css or in our components.css? Will those styles be overridden in the monolith? What will happen to the nice and tidy component styles that we import into a new project?

Subsequently, I don’t know where I stole this, but I’ve been saying it an awful lot lately — you should always be able to predict what your CSS is going to do, whether that’s a single line of code or a giant codebase of intermingled styles. That’s what design systems are all about — designing and building predictable interfaces for the future. And if our compiled CSS has all these unpredictable and unknowable parts to it, then we need to gather everyone together to fix it.

Anywho, I threw some of the data into a Dropbox Paper doc after all this to make sure we start tackling these issues and see gradual improvements over time. That looks something like this today:

How have you gone about auditing your CSS? Does your team code review CSS? Are there any tricks and tips you’d recommend? Leave a comment below!

The post A Quick CSS Audit and General Notes About Design Systems appeared first on CSS-Tricks.

CSS-Tricks

, , , , , ,
[Top]