Tag: Case

The Case for ‘Developer Experience’

A good essay from Jean Yang.

What I mean by developer experience is the sum total of how developers interface with their tools, end-to-end, day-in and day-out. Sure, there’s more focus than ever on how developers use and adopt tools, and there are entire talks and panels devoted to the topic of so-called “DX” — yet large parts of developer experience are still largely ignored. With developers spending less than a third of their time actually writing code, developer experience includes all the other stuff: maintaining code, testing, security issues, addressing incidents, and more. And many of these aspects of developer experience continue getting ignored because they’re complex, they’re messy, and they don’t have “silver bullet” solutions.

She makes the case that DX has perhaps been generally oversimplified and there are categories of tools that have very different DX:

My major revelation was that there are actually two categories of tools — and therefore, two different categories of developer experience needs: abstraction tools (which assume we code in a vacuum) and complexity-exploring tools (which assume we work in complex environments). Most developer experience until now has been solely focused on the former category of abstraction, where there are more straightforward ways to understand good developer experience than the former. 

Reminds me of how Shawn thinks:

It’s time we look beyond the easy questions in developer experience and start addressing the uncomfortable ones.

Direct Link to ArticlePermalink


The post The Case for ‘Developer Experience’ appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, ,

The Case of the Stolen Domain Names

Back in 2011, the domain name for this site, css-tricks.com, was stolen. “Domain Hijacking,” they call it. It wasn’t just this site, but around 12 others in the design and development space. To this day, none of us really know how it happened and who was behind it, although I believe all the domains are back to their original owners now.

The registrants involved varied, so even that wasn’t a common thread. My best guess was that the bad guys got access to my email but deleted all trace of emails related to domain transfers. Or it was an inside job.

One odd thing about it, for me, was that they never got around to changing any DNS information even though domain name itself was stolen. So the site remained up and I had access to it the whole time. I blogged about it in real time and then again once it was safe.

No amount of backups in the world can save you from a domain name getting stolen. If the DNS, which you no longer control, is changed away from your current hosting, that’s it, it’s gone and it isn’t coming back until you reassert control over that domain name and re-point the DNS.

Another site that was affected was David Walsh’s site. David got it worse because he actually lost access for a while, and got a ransom email like I never did. David’s domain registrar was name.com, and they were the ones who fought on his behalf to get it back. They produced a cool little video about it all:

An especially vile twist to all this was the fact that the domains weren’t just transferred away from one domain registrar to another, but they made three “hops” through different registrants. The whole purpose of that is to make it especially hard to ever get back.

In my case, my domain registrant was GoDaddy, and the story is remarkably similar to David’s. They had to go to war with the situation and get the domain transferred back to them, back through all the hops. I remain grateful to GoDaddy to this day for how they were willing to fight that war, and especially grateful that they won, although I do still remain curious how it happened. These days all my domains remain on GoDaddy, and css-tricks.com especially has just about every lock you can possibly put on the dang thing.

David got his back because name.com literally called up the bad guy and apparently applied enough threats that the bad guy himself transferred it back. I believe mine was more of a company-to-company affair.

I feel especially bad for people this happens to who don’t have the ability to make as big of a stink about it as David and I did. Without using Twitter to, as David put it, “put pressure on” (he had a hashtag and everything), he might not have gotten fires that were hot enough and under the right butts to get it done. As we can see with the video above, a good outcome on something like this is good marketing; and a bad outcome is, well, exactly the opposite.

Just three years after that saga went down, my website host was compromised, and that was another whole saga. (I don’t think it was related, but who knows.) The bad guy in that story went by the name Earl Drudge and we even interviewed him on ShopTalk Show.

The post The Case of the Stolen Domain Names appeared first on CSS-Tricks.

CSS-Tricks

, , ,
[Top]

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

, , ,
[Top]

Multi-Thumb Sliders: Particular Two-Thumb Case

This is a concept I first came across a few years back when Lea Verou wrote an article on it. Multi-range sliders have sadly been removed from the spec since, but something else that has happened in the meanwhile is that CSS got better — and so have I, so I recently decided to make my own 2019 version.

In this two-part article, we’ll go through the how, step-by-step, first building an example with two thumbs, then identify the issues with it. We’ll solve those issues, first for the two-thumb case then, in part two, come up with a better solution for the multi-thumb case.

Note how the thumbs can pass each other and we can have any possible order, with the fills in between the thumbs adapting accordingly. Surprisingly, the entire thing is going to require extremely little JavaScript.

Article Series:

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

Basic structure

We need two range inputs inside a wrapper. They both have the same minimum and maximum value (this is very important because nothing is going to work properly otherwise), which we set as custom properties on the wrapper (--min and --max). We also set their values as custom properties (--a and --b).

- let min = -50, max = 50 - let a = -30, b = 20;  .wrap(style=`--a: $ {a}; --b: $ {b}; --min: $ {min}; --max: $ {max}`)   input#a(type='range' min=min value=a max=max)   input#b(type='range' min=min value=b max=max)

This generates the following markup:

<div class='wrap' style='--a: -30; --b: 20; --min: -50; --max: 50'>   <input id='a' type='range' min='-50' value='-30' max='50'/>   <input id='b' type='range' min='-50' value='20' max='50'/> </div>

Accessibility considerations

We have two range inputs and they should probably each have a <label>, but we want our multi-thumb slider to have a single label. How do we solve this issue? We can make the wrapper a <fieldset>, use its <legend> to describe the entire multi-thumb slider, and have a <label> that’s only visible to screen readers for each of our range inputs. (Thanks to Zoltan for this great suggestion.)

But what if we want to have a flex or grid layout on our wrapper? That’s something we probably want, as the only other option is absolute positioning and that comes with its own set of issues. Then we run into a Chromium issue where <fieldset> cannot be a flex or grid container.

To go around this, we use the following ARIA equivalent (which I picked up from this post by Steve Faulkner):

- let min = -50, max = 50 - let a = -30, b = 20;  .wrap(role='group' aria-labelledby='multi-lbl' style=`--a: $ {a}; --b: $ {b}; --min: $ {min}; --max: $ {max}`)   #multi-lbl Multi thumb slider:   label.sr-only(for='a') Value A:   input#a(type='range' min=min value=a max=max)   label.sr-only(for='b') Value B:   input#b(type='range' min=min value=b max=max)

The generated markup is now:

<div class='wrap' role='group' aria-labelledby='multi-lbl' style='--a: -30; --b: 20; --min: -50; --max: 50'>   <div id='multi-lbl'>Multi thumb slider:</div>   <label class='sr-only' for='a'>Value A:</label>   <input id='a' type='range' min='-50' value='-30' max='50'/>   <label class='sr-only' for='b'>Value B:</label>   <input id='b' type='range' min='-50' value='20' max='50'/> </div>

If we set an aria-label or an aria-labelledby attribute on an element, we also need to give it a role.

Basic styling

We make the wrapper a middle-aligned grid with two rows and one column. The bottom grid cell gets the dimensions we want for the slider, while the top one gets the same width as the slider, but can adjust its height according to the group label’s content.

$ w: 20em; $ h: 1em;  .wrap {   display: grid;   grid-template-rows: max-content $ h;   margin: 1em auto;   width: $ w; }

To visually hide the <label> elements, we absolutely position them and clip them to nothing:

.wrap {   // same as before   overflow: hidden; // in case <label> elements overflow   position: relative; }  .sr-only {   position: absolute;   clip-path: inset(50%); }

Some people might shriek about clip-path support, like how using it cuts out pre-Chromium Edge and Internet Explorer, but it doesn’t matter in this particular case! We’re getting to the why behind that in a short bit.

We place the sliders, one on top of the other, in the bottom grid cell:

input[type='range'] {   grid-column: 1;   grid-row: 2; }

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

We can already notice a problem however: not only does the top slider track show up above the thumb of the bottom one, but the top slider makes it impossible for us to even click and interact with the bottom one using a mouse or touch.

In order to fix this, we remove any track backgrounds and borders and highlight the track area by setting a background on the wrapper instead. We also set pointer-events: none on the actual <input> elements and then revert to auto on their thumbs.

@mixin track() {   background: none; /* get rid of Firefox track background */   height: 100%;   width: 100%; }  @mixin thumb() {   background: currentcolor;   border: none; /* get rid of Firefox thumb border */   border-radius: 0; /* get rid of Firefox corner rounding */   pointer-events: auto; /* catch clicks */   width: $ h; height: $ h; }  .wrap {   /* same as before */   background: /* emulate track with wrapper background */      linear-gradient(0deg, #ccc $ h, transparent 0); }  input[type='range'] {   &::-webkit-slider-runnable-track,    &::-webkit-slider-thumb, & { -webkit-appearance: none; }      /* same as before */   background: none; /* get rid of white Chrome background */   color: #000;   font: inherit; /* fix too small font-size in both Chrome & Firefox */   margin: 0;   pointer-events: none; /* let clicks pass through */      &::-webkit-slider-runnable-track { @include track; }   &::-moz-range-track { @include track; }      &::-webkit-slider-thumb { @include thumb; }   &::-moz-range-thumb { @include thumb; } }

Note that we’ve set a few more styles on the input itself as well as on the track and thumb in order to make the look consistent across the browsers that support letting clicks pass through the actual input elements and their tracks, while allowing them on the thumbs. This excludes pre-Chromium Edge and IE, which is why we haven’t included the -ms- prefix — there’s no point styling something that wouldn’t be functional in these browsers anyway. This is also why we can use clip-path to hide the <label> elements.

If you’d like to know more about default browser styles in order to understand what’s necessary to override here, you can check out this article where I take an in-depth look at range inputs (and where I also detail the reasoning behind using mixins here).

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

Alright, we now have something that looks functional. But in order to really make it functional, we need to move on to the JavaScript!

Functionality

The JavaScript is pretty straightforward. We need to update the custom properties we’ve set on the wrapper. (For an actual use case, they’d be set higher up in the DOM so that they’re also inherited by the elements whose styles that depend on them.)

addEventListener('input', e => {   let _t = e.target;   _t.parentNode.style.setProperty(`--$ {_t.id}`, +_t.value) }, false);

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

However, unless we bring up DevTools to see that the values of those two custom properties really change in the style attribute of the wrapper .wrap, it’s not really obvious that this does anything. So let’s do something about that!

Showing values

Something we can do to make it obvious that dragging the thumbs actually changes something is to display the current values. In order to do this, we use an output element for each input:

- let min = -50, max = 50 - let a = -30, b = 20;  .wrap(role='group' aria-labelledby='multi-lbl' style=`--a: $ {a}; --b: $ {b}; --min: $ {min}; --max: $ {max}`)   #multi-lbl Multi thumb slider:   label.sr-only(for='a') Value A:   input#a(type='range' min=min value=a max=max)   output(for='a' style='--c: var(--a)')   label.sr-only(for='b') Value B:   input#b(type='range' min=min value=b max=max)   output(for='b' style='--c: var(--b)')

The resulting HTML looks as follows:

<div class='wrap' role='group' aria-labelledby='multi-lbl' style='--a: -30; --b: 20; --min: -50; --max: 50'>   <div id='multi-lbl'>Multi thumb slider:</div>   <label class='sr-only' for='a'>Value A:</label>   <input id='a' type='range' min='-50' value='-30' max='50'/>   <output for='a' style='--c: var(--a)'></output>   <label class='sr-only' for='b'>Value B:</label>   <input id='b' type='range' min='-50' value='20' max='50'/>   <output for='b' style='--c: var(--b)'></output> </div>

We display the values in an ::after pseudo-element using a little counter trick:

output {   &::after {     counter-reset: c var(--c);     content: counter(c);   } }

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

It’s now obvious these values change as we drag the sliders, but the result is ugly and it has messed up the wrapper background alignment, so let’s add a few tweaks! We could absolutely position the <output> elements, but for now, we simply squeeze them in a row between the group label and the sliders:

.wrap {   // same as before   grid-template: repeat(2, max-content) #{$ h}/ 1fr 1fr; }  [id='multi-lbl'] { grid-column: 1/ span 2 }  input[type='range'] {   // same as before   grid-column: 1/ span 2;   grid-row: 3; }  output {   grid-row: 2;      &:last-child { text-align: right; }      &::after {     content: '--' attr(for) ': ' counter(c) ';'     counter-reset: c var(--c);   } }

Much better!

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

Setting separate :focus styles even gives us something that doesn’t look half bad, plus allows us to see which value we’re currently modifying.

input[type='range'] {   /* same as before */   z-index: 1;    &:focus {     z-index: 2;     outline: dotted 1px currentcolor;          &, & + output { color: darkorange }   } }

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

All we need now is to create the fill between the thumbs.

The tricky part

We can recreate the fill with an ::after pseudo-element on the wrapper, which we place on the bottom grid row where we’ve also placed the range inputs. This pseudo-element comes, as the name suggests, after the inputs, but it will still show up underneath them because we’ve set positive z-index values on them. Note that setting the z-index works on the inputs (without explicitly setting their position to something different from static) because they’re grid children.

The width of this pseudo-element should be proportional to the difference between the higher input value and the lower input value. The big problem here is that they pass each other and we have no way of knowing which has the higher value.

First approach

My first idea on how to solve this was by using width and min-width together. In order to better understand how this works, consider that we have two percentage values, --a and --b, and we want to make an element’s width be the absolute value of the difference between them.

Either one of the two values can be the bigger one, so we pick an example where --b is bigger and an example where --a is bigger:

<div style='--a: 30%; --b: 50%'><!-- first example, --b is bigger --></div> <div style='--a: 60%; --b: 10%'><!-- second example, --a is bigger --></div>

We set width to the second value (--b) minus the first (--a) and min-width to the first value (--a) minus the second one (--b).

div {   background: #f90;   height: 4em;   min-width: calc(var(--a) - var(--b));   width: calc(var(--b) - var(--a)); }

If the second value (--b) is bigger, then the width is positive (which makes it valid) and the min-width negative (which makes it invalid). That means the computed value is the one set via the width property. This is the case in the first example, where --b is 70% and --a is 50%. That means the width computes to 70% - 50% = 20%, while the min-width computes to 50% - 70% = -20%.

If the first value is bigger, then the width is negative (which makes it invalid) and the min-width is positive (which makes it valid), meaning the computed value is that set via the min-width property. This is the case in the second example, where --a is 80% and --b is 30%, meaning the width computes to 30% - 80% = -50%, while the min-width computes to 80% - 30% = 50%.

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

Applying this solution for our two thumb slider, we have:

.wrap {   /* same as before */   --dif: calc(var(--max) - var(--min));      &::after {     content: '';     background: #95a;     grid-column: 1/ span 2;     grid-row: 3;     min-width: calc((var(--a) - var(--b))/var(--dif)*100%);     width: calc((var(--b) - var(--a))/var(--dif)*100%);   } }

In order to represent the width and min-width values as percentages, we need to divide the difference between our two values by the difference (--dif) between the maximum and the minimum of the range inputs and then multiply the result we get by 100%.

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

So far, so good… so what?

The ::after always has the right computed width, but we also need to offset it from the track minimum by the smaller value and we can’t use the same trick for its margin-left property.

My first instinct here was to use left, but actual offsets don’t work on their own. We’d have to also explicitly set position: relative on our ::after pseudo-element in order to make it work. I felt kind of meh about doing that, so I opted for margin-left instead.

The question is what approach can we take for this second property. The one we’ve used for the width doesn’t work because there is no such thing as min-margin-left.

A min() function is now in the CSS spec, but at the time when I coded these multi-thumb sliders, it was only implemented by Safari (it has since landed in Chrome as well). Safari-only support was not going to cut it for me since I don’t own any Apple device or know anyone in real life who does… so I couldn’t play with this function! And not being able to come up with a solution I could actually test meant having to change the approach.

Second approach

This involves using both of our wrapper’s (.wrap) pseudo-elements: one pseudo-element’s margin-left and width being set as if the second value is bigger, and the other’s set as if the first value is bigger.

With this technique, if the second value is bigger, the width we’re setting on ::before is positive and the one we’re setting on ::after is negative (which means it’s invalid and the default of 0 is applied, hiding this pseudo-element). Meanwhile, if the first value is bigger, then the width we’re setting on ::before is negative (so it’s this pseudo-element that has a computed width of 0 and is not being shown in this situation) and the one we’re setting on ::after is positive.

Similarly, we use the first value (--a) to set the margin-left property on the ::before since we assume the second value --b is bigger for this pseudo-element. That means --a is the value of the left end and --b the value of the right end.

For ::after, we use the second value (--b) to set the margin-left property, since we assume the first value --a is bigger this pseudo-element. That means --b is the value of the left end and --a the value of the right end.

Let’s see how we put it into code for the same two examples we previously had, where one has --b bigger and another where --a is bigger:

<div style='--a: 30%; --b: 50%'></div> <div style='--a: 60%; --b: 10%'></div>
div {   &::before, &::after {     content: '';     height: 5em;   }      &::before {     margin-left: var(--a);     width: calc(var(--b) - var(--a));   }    &::after {     margin-left: var(--b);     width: calc(var(--a) - var(--b));   } }

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

Applying this technique for our two thumb slider, we have:

.wrap {   /* same as before */   --dif: calc(var(--max) - var(--min));      &::before, &::after {     grid-column: 1/ span 2;     grid-row: 3;     height: 100%;     background: #95a;     content: ''   }      &::before {     margin-left: calc((var(--a) - var(--min))/var(--dif)*100%);     width: calc((var(--b) - var(--a))/var(--dif)*100%)   }      &::after {     margin-left: calc((var(--b) - var(--min))/var(--dif)*100%);     width: calc((var(--a) - var(--b))/var(--dif)*100%)   } }

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

We now have a nice functional slider with two thumbs. But this solution is far from perfect.

Issues

The first issue is that we didn’t get those margin-left and width values quite right. It’s just not noticeable in this demo due to the thumb styling (such as its shape, dimensions relative to the track, and being full opaque).

But let’s say our thumb is round and maybe even smaller than the track height:

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

We can now see what the problem is: the endlines of the fill don’t coincide with the vertical midlines of the thumbs.

This is because of the way moving the thumb end-to-end works. In Chrome, the thumb’s border-box moves within the limits of the track’s content-box, while in Firefox, it moves within the limits of the slider’s content-box. This can be seen in the recordings below, where the padding is transparent, while the content-box and the border are semi-transparent. We’ve used orange for the actual slider, red for the track and purple for the thumb.

Animated gif. Chrome only moves the thumb within the left and right limits of the track's content-box.
Recording of the thumb motion in Chrome from one end of the slider to the other.

Note that the track’s width in Chrome is always determined by that of the parent slider – any width value we may set on the track itself gets ignored. This is not the case in Firefox, where the track can also be wider or narrower than its parent <input>. As we can see below, this makes it even more clear that the thumb’s range of motion depends solely on the slider width in this browser.

Animated gif. Firefox moves the thumb within the left and right limits of the actual range input's content-box.
Recording of the thumb motion in Firefox from one end of the slider to the other. The three cases are displayed from top to bottom. The border-box of the track perfectly fits the content-box of the slider horizontally. It’s longer and it’s shorter).

In our particular case (and, to be fair, in a lot of other cases), we can get away with not having any margin, border or padding on the track. That would mean its content-box coincides to that of the actual range input so there are no inconsistencies between browsers.

But what we need to keep in mind is that the vertical midlines of the thumbs (which we need to coincide with the fill endpoints) move between half a thumb width (or a thumb radius if we have a circular thumb) away from the start of the track and half a thumb width away from the end of the track. That’s an interval equal to the track width minus the thumb width (or the thumb diameter in the case of a circular thumb).

This can be seen in the interactive demo below where the thumb can be dragged to better see the interval its vertical midline (which we need to coincide with the fill’s endline) moves within.

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

The demo is best viewed in Chrome and Firefox.

The fill width and margin-left values are not relative to 100% (or the track width), but to the track width minus the thumb width (which is also the diameter in the particular case of a circular thumb). Also, the margin-left values don’t start from 0, but from half a thumb width (which is a thumb radius in our particular case).

$ d: .5*$ h; // thumb diameter $ r: .5*$ d; // thumb radius $ uw: $ w - $ d; // useful width  .wrap {   /* same as before */   --dif: calc(var(--max) - var(--min)); 	   &::before {     margin-left: calc(#{$ r} + (var(--a) - var(--min))/var(--dif)*#{$ uw});     width: calc((var(--b) - var(--a))/var(--dif)*#{$ uw});   }      &::after {     margin-left: calc(#{$ r} + (var(--b) - var(--min))/var(--dif)*#{$ uw});     width: calc((var(--a) - var(--b))/var(--dif)*#{$ uw});   } }

Now the fill starts and ends exactly where it should, along the midlines of the two thumbs:

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

This one issue has been taken care of, but we still have a way bigger one. Let’s say we want to have more thumbs, say four:

Animated gif. Shows a slider with four thumbs which can pass each other and be in any order, while the fills are always between the two thumbs with the two smallest values and between the two thumbs with the two biggest values, regardless of their order in the DOM.
An example with four thumbs.

We now have four thumbs that can all pass each other and they can be in any order that we have no way of knowing. Moreover, we only have two pseudo-elements, so we cannot apply the same techniques. Can we still find a CSS-only solution?

Well, the answer is yes! But it means scrapping this solution and going for something different and way more clever — in part two of this article!

Article Series:

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

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

CSS-Tricks

, , , ,
[Top]

A Use Case for a Parent Selector

Having a “parent selector” in CSS is mentioned regularly as something CSS could really use. I feel like I’ve had that thought plenty of times myself, but then when I ask my brain for a use case, I find it hard to think of one. Well, I just had one so I thought I’d document it here.

A classic parent/child:

<div class="parent">   <div class="child"></div> </div>

Say it makes a lot of sense for this parent to have hidden overflow and also for the child to use absolute positioning.

.parent {    overflow: hidden;    position: relative; }  .child {    position: absolute;  }

Now let’s say there’s one special circumstance where the child needs to be positioned outside the parent and still be visible. Hidden overflow is still a good default for the vast majority of situations, so it’s best to leave that rule in place, but in this very specific situation, we need to override that overflow.

.special-child {    position: absolute;     bottom: -20px; /* needs to be slightly outside parent */ }  /* Not real, but just to make a point */ .special-child:parent(.parent) {    overflow: visible; }

That selector above is fake but it’s saying, “Select the parent of .special-child,” which would allow that override as needed. Maybe it’s like this:

.parent < .special-child {  }

…which is selecting the element on the left rather than the right. Who knows? Probably both of those are problematic somehow and the final syntax would be something else. Or maybe we’ll never get it. I have no idea. Just documenting a real use case I had.

You might be thinking, “Why not just use another special class on the parent?” I would have, but the parent was being injected by a third-party library through an API that did not offer to add a class of my choosing on it. Ultimately, I did have to add the class to the parent by writing some custom JavaScript that queried the DOM to find the .special-child, find the parent, then add the class there.

Do y’all have some other use-cases for a parent selector?

The post A Use Case for a Parent Selector appeared first on CSS-Tricks.

CSS-Tricks

, ,
[Top]

Case Study: lynnandtonic.com 2019 refresh

Lynn Fisher walks us step-by-step through the redesign process of her latest outstanding personal website. In this design, increasing the width of the browser window will cause the illustrations on the page crack to open and reveal more within them:

This case study reminded me that Lynn also has an archive of every case study and project that she’s made over the years and that it’s most certainly worth checking out.

Direct Link to ArticlePermalink

The post Case Study: lynnandtonic.com 2019 refresh appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]

A Business Case for Dropping Internet Explorer

The distance between Internet Explorer (IE) 11 and every other major browser is an increasingly gaping chasm. Adding support for a technologically obsolete browser adds an inordinate amount of time and frustration to development. Testing becomes onerous. Bug-fixing looms large. Developers have wanted to abandon IE for years, but is it now financially prudent to do so?

First off, we’re talking about a dead browser

Development of IE came to an end in 2015. Microsoft Edge was released as its replacement, with Microsoft announcing that “the latest features and platform updates will only be available in Microsoft Edge”.

Edge was a massive improvement over IE in every respect. Even so, Edge was itself so far behind in implementing web standards that Microsoft recently revealed that they were rebuilding Edge from the ground up using the same technology that powers Google Chrome.

Yet here we are, discussing whether to support Edge’s obsolete ancient relative. Internet Explorer is so bad that a Principal Program Manager at the company published a piece entitled The perils of using Internet Explorer as your default browser on the official Microsoft blog. It’s a browser frozen in time; the web has moved on.

Newspaper headlines from 2015
Publications have spelled the fall of IE since 2015.

Browsers are moving faster than ever before. Consider everything that has happened since 2015. CSS Grid. Custom properties. IE11 will never implement any new features. It’s a browser frozen in time; the web has moved on.

It blocks opportunities and encourages inefficiency

The landscape of browsers has also changed dramatically since Microsoft deprecated IE in 2015. Google developer advocate Sam Thorogood has compiled a list of all the features that are supported by every browser other than IE. Once the new Chromium version of Edge is released, this list will further increase. Taken together, it’s a gargantuan feature set, comprising new HTML elements, new CSS properties and new JavaScript features. Many modern JavaScript features can be made compatible with legacy browsers through the use of polyfills and transpilation. Any CSS feature added to the web over the last four years, however, will fail to work in IE altogether.

Let’s dig a little deeper into the features we have today and how they are affected by IE11. Perhaps most notable of all, after decades of hacking layouts on the web, we finally have CSS grid, which massively simplifies responsive layout. Together with CSS custom properties, object-fit, display: contents and intrinsic sizing, they’re all examples of useful CSS features that are likely to leave a website looking broken if they’re not supported. We’ve had some major additions to CSS over the last five years. It’s the cumulative weight of so many things that undermines IE as much as one killer feature.

While many additions to the web over the last five years have been related to layout and styling, we’ve also had huge steps forwards in functionality, such as Progressive Web Apps. Not every modern API is unusable for websites that need to stay backwards compatible. Most can be wrapped in an if statement.

if ('serviceWorker' in navigator) { // do some stuff with a service worker } else {   // ??? }

You will, however, be delivering a very different experience to IE users. Increasingly, support for IE will limit the choice of tools that are available as library and frameworks utilize modern features.

Take this announcement from Evan You about the release of Vue 3, for example:

The new codebase currently targets evergreen browsers only and assumes baseline native ES2015 support.

The Vue 3 codebase makes use of proxies — a JavaScript feature that cannot be transpiled. MobX is another popular framework that also relies on proxies. Both projects will continue to maintain backwards-compatible versions, but they’ll lack the performance improvements and API niceties gained from dropping IE. Pika, a great new approach to package management, requires support for JavaScript modules, which are not supported in IE. Then there is shadow DOM — a standardized part of the modern web platform that is unlikely to degrade gracefully.

Supporting it takes tremendous effort

When assessing how much extra work is required to provide backwards compatibility for a deprecated browser like IE11, the long list of unimplemented features is only part of the problem. Browsers are incredibly complex pieces of software and, despite web standards, browsers are inconsistent. IE has long been the most bug-ridden browser that is most at odds with web standards. Flexbox (a technology that developers have been using since 2013), for example, is listed on caniuse.com as having partial support on IE due to the “large amount of bugs present.”

IE also offers by far the worst debugging experience — with only a primitive version of DevTools. This makes fixing bugs in IE undoubtedly the most frustrating part of being a developer, and it can be massively time-consuming — taking time away from organizations trying to ship features.

There’s a difference between support — making sure something is functional and looks good enough — versus optimization, where you aim to provide the best experience possible. This does, however, create a potentially confusing grey area. There could be differences of opinion on what constitutes good enough for IE. This comment about IE9 from Dave Rupert is still relevant:

The line for what is considered “broken” is fuzzy. How visually broken does it have to be in order to be functionally broken? I look for cheap fixes, but this is compounded by the fact the offshore QA team doesn’t abide in that nuance, a defect is a defect, which gets logged and assigned to my inbox and pollutes the backlog…Whether it’s polyfills, rogue if-statements, phantom styles, or QA kickbacks; there are costs and technical debt associated with rendering this site on an ever-dwindling sliver of browsers.

If you’re going to take the approach of supporting IE functionally, even if it’s not to the nth degree, still confines you to polyfill, transpile, prefix and test on top of everything else.

It’s already been abandoned by many top websites

Website logos

Popular websites to officially drop support for IE include Youtube, GitHub, Meetup, Slack, Zendesk, Trello, Atlassian, Discord, Spotify, Behance, Wix, Huddle, WhatsApp, Google Earth and Yahoo. Even some of Microsoft’s own product’s, like Teams, have severely reduced support for IE.

Whats App unsupported browser screen

Twitter displays a banner informing IE users that they will not receive the best experience and redirects users to a much older version of the Twitter website. When we think of disruptive companies that are pushing the best in web design, Monzo, Apple Music and Stripe break horribly in IE, while foregoing a warning banner.

Stripe website viewed in Internet Explorer
Stripe offers no support or warning.

Why the new Chromium-powered Edge browser matters

IE usage has been on a slower downward trend following an initial dramatic fall. There’s one primary reason the browser continues to hang on: ancient business applications that don’t work in anything else. Plenty of large companies still use applications that rely on APIs that were never standardized and are now obsolete. Thankfully, the new Edge looks set to solve this issue. In a recent post, the Microsoft Edge Team explained how these companies will finally be able to abandon IE:

The team designed Internet Explorer mode with a goal of 100% compatibility with sites that work today in IE11. Internet Explorer mode appears visually like it’s just a part of the next Microsoft Edge…By leveraging the Enterprise mode site list, IT professionals can enable users of the next Microsoft Edge to simply navigate to IE11-dependent sites and they will just work.

After using the beta version for several months, I can say it’s a genuinely great browser. Dare I say, better than Google Chrome? Microsoft are already pushing it hard. Edge is the default browser for Windows 10. Hundreds of millions of devices still run earlier versions of the operating system, on which Edge has not been available. The new Chromium-powered version will bring support to both Windows 7 and 8. For users stuck on old devices with old operating systems, there is no excuse for using IE anymore. Windows 7, still one of the world’s most popular operating systems, is itself due for end-of-life in January 2020, which should also help drive adoption of Edge when individuals and businesses upgrade to Windows 10.

In other words, it’s the perfect time to drop support.

Performance costs

All current browsers support ECMAScript 2015 (the latest version of JavaScript) — and have done so for quite some time. Transpiling JavaScript down to an older (and slower) version is still common across the industry, but at this point in time is needed only for Internet Explorer. This process, allowing developers to write modern syntax that still works in IE negatively impacts performance. Philip Walton, an engineer at Google, had this to say on the subject:

Larger files take longer to download, but they also take longer to parse and evaluate. When comparing the two versions from my site, the parse/eval times were also consistently about twice as long for the legacy version. […] The cost of shipping lots of unneeded JavaScript to low-end mobile browsers can be significant! We (on the Chrome team) have seen numerous occurrences of polyfill bloat adding seconds to the total startup time of websites on low-end mobile devices.

It’s possible to take a differential serving approach to get around this issue, but it does add a small amount of complexity to build tooling. I’m not sure it’s worth bothering when looking at the entire picture of what it already takes to support IE.

Yet another example: IE requires a massive amount of polyfills if you’re going to utilize modern APIs. This normally involves sending additional, unnecessary code to other browsers in the process. An alternative approach, polyfill.io, costs an additional, blocking HTTP request — even for modern browsers that have no need for polyfills. Both of these approaches are bad for performance.

As for CSS, modern features like CSS grid decrease the need for bulky frameworks like Bootstrap. That’s lots of extra bites we’re unable to shave off if we have to support IE. Other modern CSS properties can replace what’s traditionally done with JavaScript in a way that’s less fragile and more performant. It would be a boon for both performance and cost to take advantage of them.

Let’s talk money

One (overly simplistic) calculation would be to compare the cost of developer time spent on fixing IE bugs and the amount lost productivity working around IE issues versus the revenue from IE users. Unless you’re a large company generating significant revenue from IE, it’s an easy decision. For big corporations, the stakes are much higher. Websites at the scale of Amazon, for example, may generate tens of millions of dollars from IE users, even if they represent less than 1% of total traffic.

I’d argue that any site at such scale would benefit more by dropping support, thanks to reducing load times and bounce rates which are both even more important to revenue. For large companies, the question isn’t whether it’s worth spending a bit of extra development time to assure backwards compatibility. The question is whether you risk degrading the experience for the vast majority of users by compromising performance and opportunities offered by modern features. By providing no incentive for developers to care about new browser features, they’re being held back from innovating and building the best product they can.

It’s a massively valuable asset to have developers who are so curious and inquisitive that they explore and keep up with new technology. By supporting IE, you’re effectively disengaging developers from what’s new. It’s dispiriting to attempt to keep up with what’s new only to learn about features we can’t use. But this isn’t about putting developer experience before user experience. When you improve developer experience, developers are enabled to increase their productivity and ship features — features that users want.

Web development is hard

It was reported earlier this year that the car rental company Hertz was suing Accenture for tens of millions of dollars. Accenture is a Fortune Global 500 company worth billions of dollars. Yet Hertz alleged that, despite an eye-watering price tag, they “never delivered a functional site or mobile app.”

According to The Register:

Among the most mind-boggling allegations in Hertz’s filed complaint is that Accenture didn’t incorporate a responsive design… Despite having missed the deadline by five months, with no completed elements and weighed down by buggy code, Accenture told Hertz it would cost an additional $ 10m – on top of the $ 32m it had already been paid – to finish the project.

The Accenture/Hertz affair is an example of stunning ineptitude but it was also a glaring reminder of the fact that web development is hard. Yet, most companies are failing to take advantage of things that make it easier. Microsoft, Google, Mozilla and Apple are investing massive amounts of money into developing new browser features for a reason. Improvements and innovations that have come to browsers in recent years have expanded what is possible to deliver on the web platform while making developers’ lives easier.

Move fast and ship things

The development industry loves terms — like agile and disruptive — that imply light-footed innovation. Yet rather than focusing on shipping features and creating a great experience for the vast bulk of users, we’re catering to a single outdated legacy browser. All the companies I’ve worked for have constantly talked about technical debt. The weight of legacy code is accurately perceived as something that slows down developers. By failing to take advantage of what modern browsers have to offer, the code we write today is legacy code the moment it is written. By writing for the modern web, you don’t only increase productivity today but also create code that’s easier to maintain in the future. From a long-term perspective, it’s the right decision.

Recruitment and retainment

Developer happiness won’t be viewed as important to the bottom line by some business stakeholders. However, recruiting good engineers is notoriously difficult. Average tenure is low compared to other industries. Nothing can harm developer morale more than a day of IE debugging. In a survey of 76,118 developers conducted by Mozilla “Having to support specific browsers (e.g. IE11)” was ranked as the most frustrating thing in web development. “Avoiding or removing a feature that doesn’t work across browsers” came third while testing across different browsers reached fourth place. By minimising these frustrations, deciding to end support for IE can help with engineer recruitment and retainment.

IE users can still access your website

We live in a multi-device world. Some users will be lucky enough to have a computer provided by their employer, a personal laptop and a tablet. Smartphones are ubiquitous. If an IE user runs into problems using your site, they can complete the transaction on another device. Or they could open a different browser, as Microsoft Edge comes preinstalled on Windows 10.

The reality of cross-browser testing

If you have a thorough and rigorous cross-browser testing process that always gets followed, congratulations! This is rare in my experience. Plenty of companies only test in Chrome. By making cross-browser testing less onerous, it can be made more likely that developers and stakeholders will actually do it. Eliminating all bugs in browsers that are popular is far more worthwhile monetarily than catering to IE.

When do you plan to drop IE support?

Inevitably, your own analytics will be the determining factor in whether dropping IE support is sensible for you. Browser usage varies massively around the world — from almost 10% in South Korea to well below one percent in many parts of the world. Even if you deem today as being too soon for your particular site, be sure to reassess your analytics after the new Microsoft Edge lands.

The post A Business Case for Dropping Internet Explorer appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]

A Practical Use Case for Vue Render Functions: Building a Design System Typography Grid

This post covers how I built a typography grid for a design system using Vue render functions. Here’s the demo and the code. I used render functions because they allow you to create HTML with a greater level of control than regular Vue templates, yet surprisingly I couldn’t find very much when I web searched around for real-life, non-tutorial applications of them. I’m hoping this post will fill that void and provide a helpful and practical use case on using Vue render functions.

I’ve always found render functions to be a little out-of-character for Vue. While the rest of the framework emphasizes simplicity and separation of concerns, render functions are a strange and often difficult-to-read mix of HTML and JavaScript.

For example, to display:

<div class="container">   <p class="my-awesome-class">Some cool text</p> </div>

…you need:

render(createElement) {   return createElement("div", { class: "container" }, [     createElement("p", { class: "my-awesome-class" }, "Some cool text")   ]) }

I suspect that this syntax turns some people off, since ease-of-use is a key reason to reach for Vue in the first place. This is a shame because render functions and functional components are capable of some pretty cool, powerful stuff. In the spirit of demonstrating their value, here’s how they solved an actual business problem for me.

Quick disclaimer: It will be super helpful to have the demo open in another tab to reference throughout this post.

Defining criteria for a design system

My team wanted to include a page in our VuePress-powered design system showcasing different typography options. This is part of a mockup that I got from our designer.

A screenshot of the typographic design system. There are four columns where the first shows the style name with the rendered style, second is the element or class, third shows the properties that make the styles, and fourth is the defined usage.

And here’s a sample of some of the corresponding CSS:

h1, h2, h3, h4, h5, h6 {   font-family: "balboa", sans-serif;   font-weight: 300;   margin: 0; }  h4 {   font-size: calc(1rem - 2px); }  .body-text {   font-family: "proxima-nova", sans-serif; }  .body-text--lg {   font-size: calc(1rem + 4px); }  .body-text--md {   font-size: 1rem; }  .body-text--bold {   font-weight: 700; }  .body-text--semibold {   font-weight: 600; }

Headings are targeted with tag names. Other items use class names, and there are separate classes for weight and size.

Before writing any code, I created some ground rules:

  • Since this is really a data visualization, the data should be stored in a separate file.
  • Headings should use semantic heading tags (e.g. <h1>, <h2>, etc.) instead of having to rely on a class.
  • Body content should use paragraph (<p>) tags with the class name (e.g. <p class="body-text--lg">).
  • Content types that have variations should be grouped together by wrapping them in the root paragraph tag, or corresponding root element, without a styling class. Children should be wrapped with <span> and the class name.
<p>   <span class="body-text--lg">Thing 1</span>   <span class="body-text--lg">Thing 2</span> </p>
  • Any content that’s not demonstrating special styling should use a paragraph tag with the correct class name and <span> for any child nodes.
<p class="body-text--semibold">   <span>Thing 1</span>   <span>Thing 2</span> </p>
  • Class names should only need to be written once for each cell that’s demonstrating styling.

Why render functions make sense

I considered a few options before starting:

Hardcoding

I like hardcoding when appropriate, but writing my HTML by hand would have meant typing out different combinations of the markup, which seemed unpleasant and repetitive. It also meant that data couldn’t be kept in a separate file, so I ruled out this approach.

Here’s what I mean:

<div class="row">   <h1>Heading 1</h1>   <p class="body-text body-text--md body-text--semibold">h1</p>   <p class="body-text body-text--md body-text--semibold">Balboa Light, 30px</p>   <p class="group body-text body-text--md body-text--semibold">     <span>Product title (once on a page)</span>     <span>Illustration headline</span>   </p> </div>

Using a traditional Vue template

This would normally be the go-to option. However, consider the following:

See the Pen
Different Styles Example
by Salomone Baquis (@soluhmin)
on CodePen.

In the first column, we have:

– An <h1>> tag rendered as-is.
– A <p> tag that groups some <span> children with text, each with a class (but no special class on the <p> tag).
– A <p> tag with a class and no children.

The result would have meant many instances of v-if and v-if-else, which I knew would get confusing fast. I also disliked all of that conditional logic inside the markup.

Because of these reasons, I chose render functions. Render functions use JavaScript to conditionally create child nodes based on all of the criteria that’s been defined, which seemed perfect for this situation.

Data model

As I mentioned earlier, I’d like to keep typography data in a separate JSON file so I can easily make changes later without touching markup. Here’s the raw data.

Each object in the file represents a different row.

{   "text": "Heading 1",   "element": "h1", // Root wrapping element.   "properties": "Balboa Light, 30px", // Third column text.   "usage": ["Product title (once on a page)", "Illustration headline"] // Fourth column text. Each item is a child node.  }

The object above renders the following HTML:

<div class="row">   <h1>Heading 1</h1>   <p class="body-text body-text--md body-text--semibold">h1</p>   <p class="body-text body-text--md body-text--semibold">Balboa Light, 30px</p>   <p class="group body-text body-text--md body-text--semibold">     <span>Product title (once on a page)</span>     <span>Illustration headline</span>   </p> </div>

Let’s look at a more involved example. Arrays represent groups of children. A classes object can store classes. The base property contains classes that are common to every node in the cell grouping. Each class in variants is applied to a different item in the grouping.

{   "text": "Body Text - Large",   "element": "p",   "classes": {     "base": "body-text body-text--lg", // Applied to every child node     "variants": ["body-text--bold", "body-text--regular"] // Looped through, one class applied to each example. Each item in the array is its own node.    },   "properties": "Proxima Nova Bold and Regular, 20px",   "usage": ["Large button title", "Form label", "Large modal text"] }

Here’s how that renders:

<div class="row">   <!-- Column 1 -->   <p class="group">     <span class="body-text body-text--lg body-text--bold">Body Text - Large</span>     <span class="body-text body-text--lg body-text--regular">Body Text - Large</span>   </p>   <!-- Column 2 -->   <p class="group body-text body-text--md body-text--semibold">     <span>body-text body-text--lg body-text--bold</span>     <span>body-text body-text--lg body-text--regular</span>   </p>   <!-- Column 3 -->   <p class="body-text body-text--md body-text--semibold">Proxima Nova Bold and Regular, 20px</p>   <!-- Column 4 -->   <p class="group body-text body-text--md body-text--semibold">     <span>Large button title</span>     <span>Form label</span>     <span>Large modal text</span>   </p> </div>

The basic setup

We have a parent component, TypographyTable.vue, which contains the markup for the wrapper table element, and a child component, TypographyRow.vue, which creates a row and contains our render function.

I loop through the row component, passing the row data as props.

<template>   <section>     <!-- Headers hardcoded for simplicity -->     <div class="row">       <p class="body-text body-text--lg-bold heading">Hierarchy</p>       <p class="body-text body-text--lg-bold heading">Element/Class</p>       <p class="body-text body-text--lg-bold heading">Properties</p>       <p class="body-text body-text--lg-bold heading">Usage</p>     </div>       <!-- Loop and pass our data as props to each row -->     <typography-row       v-for="(rowData, index) in $ options.typographyData"       :key="index"       :row-data="rowData"     />   </section> </template> <script> import TypographyData from "@/data/typography.json"; import TypographyRow from "./TypographyRow"; export default {   // Our data is static so we don't need to make it reactive   typographyData: TypographyData,   name: "TypographyTable",   components: {     TypographyRow   } }; </script>

One neat thing to point out: the typography data can be a property on the Vue instance and be accessed using $ options.typographyData since it doesn’t change and doesn’t need to be reactive. (Hat tip to Anton Kosykh.)

Making a functional component

The TypographyRow component that passes data is a functional component. Functional components are stateless and instanceless, which means that they have no this and don’t have access to any Vue lifecycle methods.

The empty starting component looks like this:

// No <template> <script> export default {   name: "TypographyRow",   functional: true, // This property makes the component functional   props: {     rowData: { // A prop with row data       type: Object     }   },   render(createElement, { props }) {     // Markup gets rendered here   } } </script>

The render method takes a context argument, which has a props property that’s de-structured and used as the second argument.

The first argument is createElement, which is a function that tells Vue what nodes to create. For brevity and convention, I’ll be abbreviating createElement as h. You can read about why I do that in Sarah’s post.

h takes three arguments:

  1. An HTML tag (e.g. div)
  2. A data object with template attributes (e.g. { class: 'something'})
  3. Text strings (if we’re just adding text) or child nodes built using h
render(h, { props }) {   return h("div", { class: "example-class }, "Here's my example text") }

OK, so to recap where we are at this point, we’ve covered creating:

  • a file with the data that’s going to be used in my visualization;
  • a regular Vue component where I’m importing the full data file; and
  • the beginning of a functional component that will display each row.

To create each row, the data from the JSON file needs to be passed into arguments for h. This could be done all at once, but that involves a lot of conditional logic and is confusing.

Instead, I decided to do it in two parts:

  1. Transform the data into a predictable format.
  2. Render the transformed data.

Transforming the common data

I wanted my data in a format that would match the arguments for h, but before doing this, I wrote out how I wanted things structured:

// One cell {   tag: "", // HTML tag of current level   cellClass: "", // Class of current level, null if no class exists for that level   text: "", // Text to be displayed    children: [] // Children each follow this data model, empty array if no child nodes }

Each object represents one cell, with four cells making up each row (an array).

// One row [ { cell1 }, { cell2 }, { cell3 }, { cell4 } ]

The entry point would be a function like:

function createRow(data) { // Pass in the full row data and construct each cell   let { text, element, classes = null, properties, usage } = data;   let row = [];   row[0] = createCellData(data) // Transform our data using some shared function   row[1] = createCellData(data)   row[2] = createCellData(data)   row[3] = createCellData(data)    return row; }

Let’s take another look at our mockup.

The first column has styling variations, but the rest seem to follow the same pattern, so let’s start with those.

Again, the desired model for each cell is:

{   tag: "",   cellClass: "",    text: "",    children: [] }

This gives us a tree-like structure for each cell since some cells have groups of children. Let’s use two functions to create the cells.

  • createNode takes each of our desired properties as arguments.
  • createCell wraps around createNode so that we can check if the text that we’re passing in is an array. If it is, we build up an array of child nodes.
// Model for each cell function createCellData(tag, text) {   let children;   // Base classes that get applied to every root cell tag   const nodeClass = "body-text body-text--md body-text--semibold";   // If the text that we're passing in as an array, create child elements that are wrapped in spans.    if (Array.isArray(text)) {     children = text.map(child => createNode("span", null, child, children));   }   return createNode(tag, nodeClass, text, children); } // Model for each node function createNode(tag, nodeClass, text, children = []) {   return {     tag: tag,     cellClass: nodeClass,     text: children.length ? null : text,     children: children   }; }

Now, we can do something like:

function createRow(data) {   let { text, element, classes = null, properties, usage } = data;   let row = [];   row[0] = ""   row[1] = createCellData("p", ?????) // Need to pass in class names as text    row[2] = createCellData("p", properties) // Third column   row[3] = createCellData("p", usage) // Fourth column    return row; }

We pass properties and usage to the third and fourth columns as text arguments. However, the second column is a little different; there, we’re displaying the class names, which are stored in the data file like:

"classes": {   "base": "body-text body-text--lg",   "variants": ["body-text--bold", "body-text--regular"] },

Additionally, remember that headings don’t have classes, so we want to show the heading tag names for those rows (e.g. h1, h2, etc.).

Let’s create some helper functions to parse this data into a format that we can use for our text argument.

// Pass in the base tag and class names as arguments function displayClasses(element, classes) {   // If there are no classes, return the base tag (appropriate for headings)   return getClasses(classes) ? getClasses(classes) : element; }  // Return the node class as a string (if there's one class), an array (if there are multiple classes), or null (if there are none.)  // Ex. "body-text body-text--sm" or ["body-text body-text--sm body-text--bold", "body-text body-text--sm body-text--italic"] function getClasses(classes) {   if (classes) {     const { base, variants = null } = classes;     if (variants) {       // Concatenate each variant with the base classes       return variants.map(variant => base.concat(`$ {variant}`));     }     return base;   }   return classes; }

Now we can do this:

function createRow(data) {   let { text, element, classes = null, properties, usage } = data;   let row = [];   row[0] = ""   row[1] = createCellData("p", displayClasses(element, classes)) // Second column   row[2] = createCellData("p", properties) // Third column   row[3] = createCellData("p", usage) // Fourth column    return row; }

Transforming the demo data

This leaves the first column that demonstrates the styles. This column is different because we’re applying new tags and classes to each cell instead of using the class combination used by the rest of the columns:

<p class="body-text body-text--md body-text--semibold">

Rather than try to do this in createCellData or createNodeData, let’s make another function to sit on top of these base transformation functions and handle some of the new logic.

function createDemoCellData(data) {   let children;   const classes = getClasses(data.classes);   // In cases where we're showing off multiple classes, we need to create children and apply each class to each child.   if (Array.isArray(classes)) {     children = classes.map(child =>       // We can use "data.text" since each node in a cell grouping has the same text       createNode("span", child, data.text, children)     );   }   // Handle cases where we only have one class   if (typeof classes === "string") {     return createNode("p", classes, data.text, children);   }   // Handle cases where we have no classes (ie. headings)   return createNode(data.element, null, data.text, children); }

Now we have the row data in a normalized format that we can pass to our render function:

function createRow(data) {   let { text, element, classes = null, properties, usage } = data   let row = []   row[0] = createDemoCellData(data)   row[1] = createCellData("p", displayClasses(element, classes))   row[2] = createCellData("p", properties)   row[3] = createCellData("p", usage)    return row }

Rendering the data

Here’s how we actually render the data to display:

// Access our data in the "props" object const rowData = props.rowData;  // Pass it into our entry transformation function const row = createRow(rowData);  // Create a root "div" node and handle each cell return h("div", { class: "row" }, row.map(cell => renderCells(cell)));  // Traverse cell values function renderCells(data) {    // Handle cells with multiple child nodes   if (data.children.length) {     return renderCell(       data.tag, // Use the base cell tag       { // Attributes in here         class: {           group: true, // Add a class of "group" since there are multiple nodes           [data.cellClass]: data.cellClass // If the cell class isn't null, apply it to the node         }       },       // The node content       data.children.map(child => {         return renderCell(           child.tag,           { class: child.cellClass },           child.text         );       })     );   }    // If there are no children, render the base cell   return renderCell(data.tag, { class: data.cellClass }, data.text); }  // A wrapper function around "h" to improve readability function renderCell(tag, classArgs, text) {   return h(tag, classArgs, text); }

And we get our final product! Here’s the source code again.

Wrapping up

It’s worth pointing out that this approach represents an experimental way of addressing a relatively trivial problem. I’m sure many people will argue that this solution is needlessly complicated and over-engineered. I’d probably agree.

Despite the up-front cost, however, the data is now fully separated from the presentation. Now, if my design team adds or removes rows, I don’t have to dig into messy HTML — I just update a couple of properties in the JSON file.

Is it worth it? Like everything else in programming, I guess it depends. I will say that this comic strip was in the back of my mind as I worked on this:

A three-panel comic strip. First panel is a stick figure at a dinner table asking to pass the salt. Second panel is the same figure with no dialogue. Third panel is another figure saying he's building a system to pass the condiments and that it will save time in the long run. First figure says it's already been 20 minutes.
Source: https://xkcd.com/974

Maybe that’s an answer. I’d love to hear all of your (constructive) thoughts and suggestions, or if you’ve tried other ways of going about a similar task.

The post A Practical Use Case for Vue Render Functions: Building a Design System Typography Grid appeared first on CSS-Tricks.

CSS-Tricks

, , , , , , , ,
[Top]