It all started with this question Keith Clark recently asked on twitter.
CSS repeating-linear-gradients, do we need these? Can't the same thing be achieved with a linear-gradient and background-size?
That's a good question.
Their cousin, repeating radial gradients, definitely come in handy. They have saved me from having to write tens of stops inside a regular radial-gradient when I wanted to reproduce the look of a vinyl record with CSS.
Until this year, I was also convinced they're useless. But in early February, while working on a canvas demo, I fell into the trap of styling the control panel for that demo and ended up reproducing over 100 slider designs I found online, each with the (self-imposed) restriction that I should use just one range input per slider.
Warning: while I did learn a lot creating them, I never went back to apply what I learned along the way to the sliders I had already coded, so, with the exception of the last few, I wouldn't really recommend them as a good resource. Also: if I never have to style another range input again, it will be too soon.
Some of these styled range inputs had striped fills or rulers, so they required using gradients, particularly repeating linear gradients, quite a bit. I used repeating linear gradients precisely because it turned out normal linear gradients were not enough. Let's see why!
We start by recreating the following pattern:
It has black and blue stripes, blue stripes being twice as wide as the black ones. If the black stripes have a width of
.25em, the code to get this effect with
background: repeating-linear-gradient(135deg, #000, #000 .25em /* black stripe */, #0092b7 0, #0092b7 .75em /* blue stripe */ );
The gradient starts from the top left corner at an angle of
135°. If you need a refresher on how linear gradients and their angles work, check out this explanation (ignore the part about the old syntax, we have now left that behind). There's a black stripe from
.25em along the gradient line, then a blue stripe from
.75em - .25em = .5em = the blue stripe width = 2*.25em = twice the black stripe width). We can see it in action in this Pen:
Now let's try to achieve the same with plain
background-size. We'll make the background a small square with a diagonal adding up to the width of a black stripe (
.25em) and the width of a blue stripe (
.5em). The diagonal of a square is the square edge times
√2 and, if we know the diagonal is
.25em + .5em = .75em, then the edge of the
background-size square is going to be
.75em/sqrt(2). This brings us to the following code:
background: linear-gradient(135deg, #000 .25em, #0092b7 0); background-size: .75em/sqrt(2) .75em/sqrt(2);
Hmm, this doesn't look like what we wanted to achieve...
What if we put the
.25em wide black stripe in the middle? Let's try that!
background: linear-gradient(135deg, #0092b7 calc(50% - .125em) /* blue corner */, #000 0, #000 calc(50% + .125em) /* black stripe */, #0092b7 0 /* blue corner */ ); background-size: .75em/sqrt(2) .75em/sqrt(2);
It's a little closer now, but it still doesn't look good.
If we zoom in, it becomes more obvious what the problem is and what we need to do about it. We have to fill out those corners with black.
This means adding a small black stripe at the beginning of the gradient line and one at the end. Each of these will be half the width of the initial black stripe in the middle (so half of
.25em, which is
background: linear-gradient(135deg, #000 .125em /* black corner */, #0092b7 0, #0092b7 calc(50% - .125em) /* blue stripe */, #000 0, #000 calc(50% + .125em) /* black stripe */, #0092b7 0, #0092b7 calc(100% - .125em) /* blue stripe */, #000 0 /* black corner */ ); background-size: .75em/sqrt(2) .75em/sqrt(2);
It looks... slightly better?
It's still not what we want because now the blue stripes are too narrow. This is because the diagonal computations don't work out right anymore. Going from the top left corner to the bottom right corner, we now have half a black stripe (
.125em), a blue stripe (
.5em), a black stripe (
.25em), another blue stripe (
.5em) and another half a black stripe (
.125em). This all adds up to a diagonal of
1.5em, which means we need to change the
Perfect! It's visually the same result as in the
repeating-linear-gradient version. But in this case, it's too much work, too many computations, too much code, too much risk for rounding errors (the demo above will break if we set the
font-size to some weird value like
1.734em which doesn't compute to an integer pixel value).
And in case you're not yet convinced that
repeating-linear-gradient is the better solution in this case, let's imagine the gradient angle isn't
135°, but something different... let's say
repeating-linear-gradient, the code is almost the same as before, we only need to replace
All seems fine in Firefox and Edge/IE, but the transition from blue to black doesn't look good in Chrome. We can fix that by not making the transition sharp, leaving a
1px distance in between.
background: repeating-linear-gradient(120deg, #0092b7 0, #000 1px /* transition from previous blue stripe */, #000 .25em, #0092b7 calc(.25em + 1px) /* from black to blue */, #0092b7 .75em );
We use this Pen to test that this fixes the Chrome issue:
See the Pen simple
repeating-linear-gradient pattern v2 (Chrome fix) by Ana Tudor (@thebabydino) on CodePen.
With plain old
linear-gradient, we also need to change the angle from
120deg. But it's not enough this time.
We also need to change the dimensions of the
background-size, as this is not a square anymore. The
x dimension will be
1.5em*abs(cos(120deg)), while the
y dimension will be
Again, the edges don't look good in Chrome, so we need to use the
1px spacing trick.
linear-gradient alternative isn't pretty, but it exists.
Is there something
repeating-linear-gradient can do that plain
linear-gradient and the proper
background-size can't do at all?
Well... there is!
If you've paid attention to the slider examples shown at the beginning, the striped gradient reproduced above is used to fill the slider track before the thumb (on the left of the thumb).
IE and Firefox have dedicated pseudo-elements for this (
-moz-progress respectively) and all we need to do is add the background created earlier on these pseudos.
But for WebKit browsers, the only way to get that striped fill is to add another background on top of the regular one used for the slider track and then update how much of the track it covers as the slider thumb gets adjusted. Now this creates a big problem with the plain
linear-gradient. We cannot control how much of the track it covers via
background-size because we are already using
background-size to create the striped look. In addition to that, gradients either repeat indefinitely or they don't repeat at all.
It would probably be cool if we could specify number values for
background-repeat in the same way we can for
animation-iteration-count. Something like
background-repeat: .5 1.5 would make half of the background to be displayed horizontally and have one tile and a half displayed vertically. But we cannot do this. Maybe we have some other way of making a
linear-gradient repeat just within certain limits?
Well, the only way we could create something that gets close to what we want with plain old
background-size is to stack up identical gradients and position them one after the other while not allowing them to repeat in at least one direction.
For example, if we wanted them to only cover a part of the element horizontally, we'd have to set
background-repeat: repeat-y. The first gradient background would start horizontally at at
0*$size-x, the second at
1*$size-x and so on... where
$size-x is the
x component of the
background-size. The following image illustrates how this would work:
But this only works for changing how much of the track the striped part covers in increments of
$size-x, it's useless if we want finer control. Plus, it's really ugly to stack up maybe 20 identical gradients.
background-image: linear-gradient(135deg, #000 0.125em, #0092b7 0, #0092b7 calc(50% - .125em), #000 0, #000 calc(50% + .125em), #0092b7 0, #0092b7 calc(100% - .125em), #000 0 ), linear-gradient(135deg, #000 0.125em, #0092b7 0, #0092b7 calc(50% - .125em), #000 0, #000 calc(50% + .125em), #0092b7 0, #0092b7 calc(100% - .125em), #000 0 ) /* repeat as many times as needed */; ; background-position: 0*1.5em/sqrt(2) 0, 1*1.5em/sqrt(2) /* repeat for 2, 3, 4 and so on... */; background-repeat: repeat-y;
Ugh. Repeating the exact same gradient 20 times is awful. We could make the Sass better, but the generated CSS will still look ugly:
$grad: linear-gradient(135deg, #000 0.125em, #0092b7 0, #0092b7 calc(50% - .125em), #000 0, #000 calc(50% + .125em), #0092b7 0, #0092b7 calc(100% - .125em), #000 0 ); background-image: $grad, $grad /* repeat as many times as needed */; background-position: 0*1.5em/sqrt(2) 0, 1*1.5em/sqrt(2) /* repeat for 2, 3, 4 and so on... */; background-repeat: repeat-y;
The following Pen shows this method in action:
In short, my advice is: don't do this! The code is horrible and breakable, we could easily end up with gaps between the gradients for certain background sizes or on zoom.
We have a much better method available. We can limit how much of the element a
repeating-linear-gradient covers (no matter what the dimensions of the repeating part are) by using
background-size and setting
See the Pen constrain a
repeating-linear-gradient within certain limits by Ana Tudor (@thebabydino) on CodePen.
This is how I was able to control how much of the slider track was filled with the striped gradient every time the slider thumb value got updated - by changing the
x component of the
The very same idea helps limit how much the rulers would extend in situations when we don't want them to extend across the whole length of the slider. Let's say we also want them to be horizontally in the middle, which makes the
x component of
50%. This is how a very simple ruler would get created:
width: 19em; background: repeating-linear-gradient(90deg, #c8c8c8, #c8c8c8 0.125em /* lines */, transparent 0.125em, transparent 1.25em /* space between */ ) 50% no-repeat; background-size: 12.625em /* = 10*1.25em + .125em */ .5em;
And we can see the result in this pen:
In order to have both major and minor lines, we need to add a second repeating gradient, one that repeats at twice the interval for the first one and is twice as tall:
background: repeating-linear-gradient(90deg, /* major */ #c8c8c8, #c8c8c8 .125em /* lines */, transparent , transparent 2.5em /* space between */ ) 50% no-repeat, repeating-linear-gradient(90deg, /* minor */ #c8c8c8, #c8c8c8 .125em /* lines */, transparent , transparent 1.25em /* space between */ ) 50% no-repeat; background-size: 12.625em 1em, 12.625em .5em;
The result can be seen in this Pen:
We cold also tweak the
y component of the
background-position so that they align to top or to bottom. For example, we could do something like this:
background-position: right 50% bottom 2.25em;
This is exactly the kind of ruler that was used in one of the examples at the beginning.
The slider thumb grips in the following example were created in exactly the same manner.
There are two identical repeating linear gradients, both limited horizontally to twice the size of the repeating part plus the width of the non-transparent part. Horizontally, the first one is positioned at
25% and the second one at
background: repeating-linear-gradient(90deg, #6b4c1e, #6b4c1e 1px, #e1ba75 0, #e1ba75 2px, transparent 0, transparent 4px ) 25% 50% /* left */, repeating-linear-gradient(90deg, #6b4c1e, #6b4c1e 1px, #e1ba75 0, #e1ba75 2px, transparent 0, transparent 4px ) 75% 50% /* right */ orange; background-repeat: no-repeat; background-size: 10px 10px;
But this is a very WET style of writing code. We could make it more maintainable using Sass:
$grip: repeating-linear-gradient(90deg, #6b4c1e, #6b4c1e 1px, #e1ba75 0, #e1ba75 2px, transparent 0, transparent 4px); /* we use this twice */ background: $grip 25% 50% /* left */, $grip 75% 50% /* right */ orange; background-repeat: no-repeat; background-size: 10px 10px;
This gives us the following result:
Repeating gradients used to be a real headache and we can still run into issues with thin gradient lines, especially when they cover larger areas. They won't always have the same width in WebKit browsers and Firefox on OS X isn't very precise when rendering them (though Firefox on Windows rarely ever fails here).
But things have improved a lot and I believe they'll continue to improve. Even as I was creating those sliders at the beginning of this year, I could see pretty big differences between what were then the stable (41) and the canary (43) versions of Chrome.
Repeating gradients are a lot safer to use now and I think they deserve a bit more love because they can make our lives much easier and that makes them pretty cool in my eyes.