conic gradients – CSS-Tricks https://css-tricks.com Tips, Tricks, and Techniques on using Cascading Style Sheets. Fri, 18 Nov 2022 14:11:27 +0000 en-US hourly 1 https://wordpress.org/?v=6.1.1 https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1 conic gradients – CSS-Tricks https://css-tricks.com 32 32 45537868 Making Static Noise From a Weird CSS Gradient Bug https://css-tricks.com/making-static-noise-from-a-weird-css-gradient-bug/ https://css-tricks.com/making-static-noise-from-a-weird-css-gradient-bug/#comments Fri, 18 Nov 2022 13:55:24 +0000 https://css-tricks.com/?p=374993 What I will be doing here is kind of an experiment to explore tricks that leverage a bug with the way CSS gradients handle sub-pixel rendering to create a static noise effect — like you might see on a TV with no signal.


Making Static Noise From a Weird CSS Gradient Bug originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
👋 The demos in this article experiment with a non-standard bug related to CSS gradients and sub-pixel rendering. Their behavior may change at any time in the future. They’re also heavy as heck. We’re serving them async where you click to load, but still want to give you a heads-up in case your laptop fan starts spinning.

Do you remember that static noise on old TVs with no signal? Or when the signal is bad and the picture is distorted? In case the concept of a TV signal predates you, here’s a GIF that shows exactly what I mean.

View image (contains auto-playing media)
Animated image showing static noise from a TV screen.

Yes, we are going to do something like this using only CSS. Here is what we’re making:

Before we start digging into the code, I want to say that there are better ways to create a static noise effect than the method I am going to show you. We can use SVG, <canvas>, the filter property, etc. In fact, Jimmy Chion wrote a good article showing how to do it with SVG.

What I will be doing here is kind of a CSS experiment to explore some tricks leveraging a bug with gradients. You can use it on your side projects for fun but using SVG is cleaner and more suitable for a real project. Plus, the effect behaves differently across browsers, so if you’re checking these out, it’s best to view them in Chrome, Edge, or Firefox.

Let’s make some noise!

To make this noise effect we are going to use… gradients! No, there is no secret ingredient or new property that makes it happen. We are going to use stuff that’s already in our CSS toolbox!

The “trick” relies on the fact that gradients are bad at anti-aliasing. You know those kind of jagged edges we get when using hard stop colors? Yes, I talk about them in most of my articles because they are a bit annoying and we always need to add or remove a few pixels to smooth things out:

As you can see, the second circle renders better than the first one because there is a tiny difference (0.5%) between the two colors in the gradient rather than using a straight-up hard color stop using whole number values like the first circle.

Here’s another look, this time using a conic-gradient where the result is more obvious:

An interesting idea struck me while I was making these demos. Instead of fixing the distortion all the time, why not trying to do the opposite? I had no idea what would happen but it was a fun surprise! I took the conic gradient values and started to decrease them to make the poor anti-aliasing results look even worse.

Do you see how bad the last one is? It’s a kind of scrambled in the middle and nothing is smooth. Let’s make it full-screen with smaller values:

I suppose you see where this is going. We get a strange distorted visual when we use very small decimal values for the hard colors stops in a gradient. Our noise is born!

We are still far from the grainy noise we want because we can still see the actual conic gradient. But we can decrease the values to very, very small ones — like 0.0001% — and suddenly there’s no more gradient but pure graininess:

Tada! We have a noise effect and all it takes is one CSS gradient. I bet if I was to show this to you before explaining it, you’d never realize you’re looking at a gradient. You have to look very carefully at center of the gradient to see it.

We can increase the randomness by making the size of the gradient very big while adjusting its position:

The gradient is applied to a fixed 3000px square and placed at the 60% 60% coordinates. We can hardly notice its center in this case. The same can be done with radial gradient as well:

And to make things even more random (and closer to a real noise effect) we can combine both gradients and use background-blend-mode to smooth things out:

Our noise effect is perfect! Even if we look closely at each example, there’s no trace of either gradient in there, but rather beautiful grainy static noise. We just turned that anti-aliasing bug into a slick feature!

Now that we have this, let’s see a few interesting examples where we might use it.

Animated no TV signal

Getting back to the demo we started with:

If you check the code, you will see that I am using a CSS animation on one of the gradients. It’s really as simple as that! All we’re doing is moving the conic gradient’s position at a lightning fast duration (.1s) and this is what we get!

I used this same technique on a one-div CSS art challenge:

Grainy image filter

Another idea is to apply the noise to an image to get an old-time-y look. Hover each image to see them without the noise.

I am using only one gradient on a pseudo-element and blending it with the image, thanks to mix-blend-mode: overlay.

We can get an even funnier effect if we use the CSS filter property

And if we add a mask to the mix, we can make even more effects!

Grainy text treatment

We can apply this same effect to text, too. Again, all we need is a couple of chained gradients on a background-image and then blend the backgrounds. The only difference is that we’re also reaching for background-clip so the effect is only applied to the bounds of each character.

Generative art

If you keep playing with the gradient values, you may get more surprising results than a simple noise effect. We can get some random shapes that look a lot like generative art!

Of course, we are far from real generative art, which requires a lot of work. But it’s still satisfying to see what can be achieved with something that is technically considered a bug!

Monster face

One last example I made for CodePen’s divtober 2022 collection:

Wrapping up

I hope you enjoyed this little CSS experiment. We didn’t exactly learn something “new” but we took a little quirk with gradients and turned it into something fun. I’ll say it again: this isn’t something I would consider using on a real project because who knows if or when anti-aliasing will be addressed at some point in time. Instead, this was a very random, and pleasant, surprise when I stumbled into it. It’s also not that easy to control and it behaves inconsistently across browsers.

This said, I am curious to see what you can do with it! You can play with the values, combine different layers, use a filter, or mix-blend-mode, or whatever, and you will for sure get something really cool. Share your creations in the comment section — there are no prizes but we can get a nice collection going!


Making Static Noise From a Weird CSS Gradient Bug originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/making-static-noise-from-a-weird-css-gradient-bug/feed/ 9 374993
Some Links About CSS Gradients https://css-tricks.com/some-links-about-css-gradients/ https://css-tricks.com/some-links-about-css-gradients/#comments Wed, 02 Nov 2022 12:59:02 +0000 https://css-tricks.com/?p=374697 Every once in a while, the blogging zeitgiest seems to coalesce around a certain topic and it’s like the saved articles in my bookmarks folder are having a conversation. The conversation sitting in there now is all about CSS Gradients …


Some Links About CSS Gradients originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Every once in a while, the blogging zeitgiest seems to coalesce around a certain topic and it’s like the saved articles in my bookmarks folder are having a conversation. The conversation sitting in there now is all about CSS Gradients and I thought I’d link some of the more interesting pieces.


Some Links About CSS Gradients originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/some-links-about-css-gradients/feed/ 4 374697
Fancy Image Decorations: Single Element Magic https://css-tricks.com/fancy-image-decorations-single-element-magic/ https://css-tricks.com/fancy-image-decorations-single-element-magic/#comments Fri, 14 Oct 2022 15:37:28 +0000 https://css-tricks.com/?p=373965 As the title says, we are going to decorate images! There’s a bunch of other articles out there that talk about this, but what we’re covering here is quite a bit different because it’s more of a challenge. The challenge? …


Fancy Image Decorations: Single Element Magic originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
As the title says, we are going to decorate images! There’s a bunch of other articles out there that talk about this, but what we’re covering here is quite a bit different because it’s more of a challenge. The challenge? Decorate an image using only the <img> tag and nothing more.

That right, no extra markup, no divs, and no pseudo-elements. Just the one tag.

Sounds difficult, right? But by the end of this article — and the others that make up this little series — I’ll prove that CSS is powerful enough to give us great and stunning results despite the limitation of working with a single element.

Fancy Image Decorations series

Let’s start with our first example

Before digging into the code let’s enumerate the possibilities for styling an <img> without any extra elements or pseudo-elements. We can use border, box-shadow, outline, and, of course, background. It may look strange to add a background to an image because we cannot see it as it will be behind the image — but the trick is to create space around the image using padding and/or border and then draw our background inside that space.

I think you know what comes next since I talked about background, right? Yes, gradients! All the decorations we are going to make rely on a lot of gradients. If you’ve followed me for a while, I think this probably comes as no surprise to you at all. 😁

Let’s get back to our first example:

img {
  --s: 10px; /* control the size */
  padding: var(--s);
  border: calc(2 * var(--s)) solid #0000;
  outline: 1px solid #000;
  outline-offset: calc(-1 * var(--s));
  background: conic-gradient(from 90deg at 1px 1px, #0000 25%, #000 0);
}

We are defining padding and a transparent border using the variable --s to create a space around our image equal to three times that variable.

Why are we using both padding and border instead of one or the other? We can get by using only one of them but I need this combination for my gradient because, by default, the initial value of background-clip is border-box and background-origin is equal to padding-box.

Here is a step-by-step illustration to understand the logic:

Initially, we don’t have any borders on the image, so our gradient will create two segments with 1px of thickness. (I am using 3px in this specific demo so it’s easier to see.) We add a colored border and the gradient still gives us the same result inside the padding area (due to background-origin) but it repeats behind the border. If we make the color of the border transparent, we can use the repetition and we get the frame we want.

The outline in the demo has a negative offset. That creates a square shape at the top of the gradient. That’s it! We added a nice decoration to our image using one gradient and an outline. We could have used more gradients! But I always try to keep my code as simple as possible and I found that adding an outline is better that way.

Here is a gradient-only solution where I am using only padding to define the space. Still the same result but with a more complex syntax.

Let’s try another idea:

For this one, I took the previous example removed the outline, and applied a clip-path to cut the gradient on each side. The clip-path value is a bit verbose and confusing but here is an illustration to better see its points:

Side-by-side comparison of the image with and without using clip-path.

I think you get the main idea. We are going to combine backgrounds, outlines, clipping, and some masking to achieve different kinds of decorations. We are also going to consider some cool hover animations as an added bonus! What we’ve looked at so far is merely a small overview of what’s coming!

The Corner-Only Frame

This one takes four gradients. Each gradient covers one corner and, on hover, we expand them to create a full frame around the image. Let’s dissect the code for one of the gradients:

--b: 5px; /* border thickness */
background: conic-gradient(from 90deg at top var(--b) left var(--b), #0000 90deg, darkblue 0) 0 0;
background-size: 50px 50px; 
background-repeat: no-repeat;

We are going to draw a gradient with a size equal to 50px 50px and place it at the top-left corner (0 0). For the gradient’s configuration, here’s a step-by-step illustration showing how I reached that result.

We tend to think that gradients are only good for transitioning between two colors. But in reality, we can do so much more with them! They are especially useful when it comes to creating different shapes. The trick is to make sure we have hard stops between colors — like in the example above — rather than smooth transitions:

#0000 25%, darkblue 0

This is basically saying: “fill the gradient with a transparent color until 25% of the area, then fill the remaining area with darkblue.

You might be scratching your head over the 0 value. It’s a little hack to simplify the syntax. In reality, we should use this to make a hard stop between colors:

#0000 25%, darkblue 25%

That is more logical! The transparent color ends at 25% and darkblue starts exactly where the transparency ends, making a hard stop. If we replace the second one with 0, the browser will do the job for us, so it is a slightly more efficient way to go about it.

Somewhere in the specification, it says:

if a color stop or transition hint has a position that is less than the specified position of any color stop or transition hint before it in the list, set its position to be equal to the largest specified position of any color stop or transition hint before it.

0 is always smaller than any other value, so the browser will always convert it to the largest value that comes before it in the declaration. In our case, that number is 25%.

Now, we apply the same logic to all the corners and we end with the following code:

img {
  --b: 5px; /* border thickness */
  --c: #0000 90deg, darkblue 0; /* define the color here */
  padding: 10px;
  background:
    conic-gradient(from 90deg  at top    var(--b) left  var(--b), var(--c)) 0 0,
    conic-gradient(from 180deg at top    var(--b) right var(--b), var(--c)) 100% 0,
    conic-gradient(from 0deg   at bottom var(--b) left  var(--b), var(--c)) 0 100%,
    conic-gradient(from -90deg at bottom var(--b) right var(--b), var(--c)) 100% 100%;
  background-size: 50px 50px; /* adjust border length here */
  background-repeat: no-repeat;
}

I have introduced CSS variables to avoid some redundancy as all the gradients use the same color configuration.

For the hover effect, all I’m doing is increasing the size of the gradients to create the full frame:

img:hover {
  background-size: 51% 51%;
}

Yes, it’s 51% instead of 50% — that creates a small overlap and avoids possible gaps.

Let’s try another idea using the same technique:

This time we are using only two gradients, but with a more complex animation. First, we update the position of each gradient, then increase their sizes to create the full frame. I also introduced more variables for better control over the color, size, thickness, and even the gap between the image and the frame.

img {
  --b: 8px;  /* border thickness*/
  --s: 60px; /* size of the corner*/
  --g: 14px; /* the gap*/
  --c: #EDC951; 

  padding: calc(var(--b) + var(--g));
  background-image:
    conic-gradient(from  90deg at top    var(--b) left  var(--b), #0000 25%, var(--c) 0),
    conic-gradient(from -90deg at bottom var(--b) right var(--b), #0000 25%, var(--c) 0);
  background-position:
    var(--_p, 0%) var(--_p, 0%),
    calc(100% - var(--_p, 0%)) calc(100% - var(--_p, 0%));
  background-size: var(--s) var(--s);
  background-repeat: no-repeat;
  transition: 
    background-position .3s var(--_i,.3s), 
    background-size .3s calc(.3s - var(--_i, .3s));
}
img:hover {
  background-size: calc(100% - var(--g)) calc(100% - var(--g));
  --_p: calc(var(--g) / 2);
  --_i: 0s;
}

Why do the --_i and --_p variables have an underscore in their name? The underscores are part of a naming convention I use to consider “internal” variables used to optimize the code. They are nothing special but I want to make a difference between the variables we adjust to control the frame (like --b, --c, etc.) and the ones I use to make the code shorter.

The code may look confusing and not easy to grasp but I wrote a three-part series where I detail such technique. I highly recommend reading at least the first article to understand how I reached the above code.

Here is an illustration to better understand the different values:

Showing the same image of two classic cars three times to illustrate the CSS variables used in the code.

The Frame Reveal

Let’s try another type of animation where we reveal the full frame on hover:

Cool, right? And you if you look closely, you will notice that the lines disappear in the opposite direction on mouse out which makes the effect even more fancy! I used a similar effect in a previous article.

But this time, instead of covering all the element, I cover only a small portion by defining a height to get something like this:

This is the top border of our frame. We repeat the same process on each side of the image and we have our hover effect:

img {
  --b: 10px; /* the border thickness*/
  --g: 5px; /* the gap on hover */
  --c: #8A9B0F; 

  padding: calc(var(--g) + var(--b));
  --_g: no-repeat linear-gradient(var(--c) 0 0);
  background: 
    var(--_g) var(--_i, 0%) 0,
    var(--_g) 100% var(--_i, 0%),
    var(--_g) calc(100% - var(--_i, 0%)) 100%,
    var(--_g) 0 calc(100% - var(--_i, 0%));
  background-size: var(--_i, 0%) var(--b),var(--b) var(--_i, 0%);
  transition: .4s, background-position 0s;
  cursor: pointer;
}
img:hover {
  --_i: 100%;
}

As you can see, I am applying the same gradient four times and each one has a different position to cover only one side at a time.

Another one? Let’s go!

This one looks a bit tricky and it indeed does require some imagination to understand how two conic gradients are pulling off this kind of magic. Here is a demo to illustrate one of the gradients:

The pseudo-element simulates the gradient. It’s initially out of sight and, on hover, we first change its position to get the top edge of the frame. Then we increase the height to get the right edge. The gradient shape is similar to the ones we used in the last section: two segments to cover two sides.

But why did I make the gradient’s width 200%? You’d think 100% would be enough, right?

100% should be enough but I won’t be able to move the gradient like I want if I keep its width equal to 100%. That’s another little quirk related to how background-position works. I cover this in a previous article. I also posted an answer over at Stack Overflow dealing with this. I know it’s a lot of reading, but it’s really worth your time.

Now that we have explained the logic for one gradient, the second one is easy because it’s doing exactly the same thing, but covering the left and bottom edges instead. All we have to do is to swap a few values and we are done:

img {
  --c: #8A9B0F; /* the border color */
  --b: 10px; /* the border thickness*/
  --g: 5px;  /* the gap */

  padding: calc(var(--g) + var(--b));
  --_g: #0000 25%, var(--c) 0;
  background: 
    conic-gradient(from 180deg at top    var(--b) right var(--b), var(--_g))
     var(--_i, 200%) 0 / 200% var(--_i, var(--b))  no-repeat,
    conic-gradient(            at bottom var(--b) left  var(--b), var(--_g))
     0 var(--_i, 200%) / var(--_i, var(--b)) 200%  no-repeat;
  transition: .3s, background-position .3s .3s;
  cursor: pointer;
}
img:hover {
  --_i: 100%;
  transition: .3s, background-size .3s .3s;
}

As you can see, both gradients are almost identical. I am simply swapping the values of the size and position.

The Frame Rotation

This time we are not going to draw a frame around our image, but rather adjust the look of an existing one.

You are probably asking how the heck I am able to transform a straight line into an angled line. No, the magic is different than that. That’s just the illusion we get after combining simple animations for four gradients.

Let’s see how the animation for the top gradient is made:

I am simply updating the position of a repeating gradient. Nothing fancy yet! Let’s do the same for the right side:

Are you starting to see the trick? Both gradients intersect at the corner to create the illusion where the straight line is changed to an angled one. Let’s remove the outline and hide the overflow to better see it:

Now, we add two more gradients to cover the remaining edges and we are done:

img {
  --g: 4px; /* the gap */
  --b: 12px; /* border thickness*/
  --c: #669706; /* the color */

  padding: calc(var(--g) + var(--b));
  --_c: #0000 0 25%, var(--c) 0 50%;
  --_g1: repeating-linear-gradient(90deg ,var(--_c)) repeat-x;
  --_g2: repeating-linear-gradient(180deg,var(--_c)) repeat-y;
  background:
    var(--_g1) var(--_p, 25%) 0, 
    var(--_g2) 0 var(--_p, 125%),
    var(--_g1) var(--_p, 125%) 100%, 
    var(--_g2) 100% var(--_p, 25%);
  background-size: 200% var(--b), var(--b) 200%;
  transition: .3s;
}
img:hover {
  --_p: 75%;
}

If we take this code and slightly adjust it, we can get another cool animation:

Can you figure out the logic in this example? That’s your homework! The code may look scary but it uses the same logic as the previous examples we looked at. Try to isolate each gradient and imagine how it animates.

Wrapping up

That’s a lot of gradients in one article!

It sure is and I warned you! But if the challenge is to decorate an image without an extra elements and pseudo-elements, we are left with only a few possibilities and gradients are the most powerful option.

Don’t worry if you are a bit lost in some of the explanations. I always recommend some of my old articles where I go into greater detail with some of the concepts we recycled for this challenge.

I am gonna leave with one last demo to hold you over until the next article in this series. This time, I am using radial-gradient() to create another funny hover effect. I’ll let you dissect the code to grok how it works. Ask me questions in the comments if you get stuck!

Fancy Image Decorations series


Fancy Image Decorations: Single Element Magic originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/fancy-image-decorations-single-element-magic/feed/ 3 373965
Making a Real-Time Clock With a Conic Gradient Face https://css-tricks.com/making-a-real-time-clock-with-a-conic-gradient-face/ https://css-tricks.com/making-a-real-time-clock-with-a-conic-gradient-face/#comments Mon, 19 Sep 2022 12:58:09 +0000 https://css-tricks.com/?p=373184 Gradients have been a part of the CSS spectrum for quite some time now. We see a lot of radial and linear gradients in a lot of projects, but there is one type of gradient that seems to be a …


Making a Real-Time Clock With a Conic Gradient Face originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Gradients have been a part of the CSS spectrum for quite some time now. We see a lot of radial and linear gradients in a lot of projects, but there is one type of gradient that seems to be a bit lonely: the conic gradient. We’re going to make a watch face using this type of gradient.

Working with conic gradients

What we’re making consists of a gradient with color transitions rotated around a center point and can have multiple color values. For this clock to work, we will also be using the angle value of a conic gradient which defines the rotation or starting point. The angle is defined by using a from value.

background-image: conic-gradient(from 45deg, #6e7dab, #5762d5);

What is interesting about this, is that a starting angle can have a negative value in CSS, which will come in handy later.

A simple elegant example of a conical gradient:

Building our basic clock

Let’s start by adding some HTML for the clock and the hands:

Let’s create some default styling for our clock. For this to work properly, we will update CSS variables with JavaScript later on, so let’s scope these variables inside our .clock selector. For easy tweaking, let’s add the colors of the hands as well.

.clock {
  /* general clock vars */
  --hour-hand-color: #000;
  --hour-hand-degrees: 0deg;
  --minute-hand-color: #000;
  --minute-hand-degrees: 0deg;
  --second-hand-color: hotpink;
  --second-hand-degrees: 0deg;

  position: relative;
  min-width: 320px;
  width: 25vw;
  height: 25vw;
  min-height: 320px;
  border-radius: 50%;
  margin: 0 auto;
  border: 7px solid #000;
}

/* clock hands */
.hand {
  position: absolute;
  left: 50%;
  bottom: 50%;
  height: 45%;
  width: 4px;
  margin-left: -2px;
  background: var(--second-hand-color);
  border-radius: 6px;
  transform-origin: bottom center;
  transition-timing-function: cubic-bezier(0.1, 2.7, 0.58, 1);
}
.second-hand {
  transform: rotate(var(--second-hand-degrees));
}
.hour-hand {
  height: 35%;
  border-radius: 40px;
  background-color: var(--hour-hand-color);
  transform: rotate(var(--hour-hand-degrees));
}
.minute-hand {
  height: 50%;
  background: var(--minute-hand-color);
  transform: rotate(var(--minute-hand-degrees));
}

This sets us up with the general styling we need for the clock. We’ve set transform-origin on the hands so that they properly rotate around the face of the clock. There are also a few custom properties in there to set angles on the hands that we’ll update with JavaScript to get the timing just right so that each hand maps to seconds, minutes, and hours accordingly.

Here’s what we have so far:

Alright, let’s move on to updating those custom properties!

Adding the JavaScript for our basic clock

First off, we’re going to target our clock and create a function:

const clock = document.getElementById("clock");
function setDate() {
  // Code to set the current time and hand angles.
}
setDate();

Inside of our function we’re going to fetch the current time using the Date() function to calculate the correct angle of the hands:

const now = new Date();
const secondsAngle = now.getSeconds() * 6; 
const minsAngle = now.getMinutes() * 6 + secondsAngle / 60;
const hourAngle = ((now.getHours() % 12) / 12) * 360 + minsAngle / 12;

Here is how this calculation works:

  • Seconds: We take 60 seconds and multiply it by 6, which happens to be 360, the perfect number of angles in a full circle.
  • Minutes: Same as seconds, but now we add the seconds angle and divide it by 60 to increase the angle just a little bit within the minute for a more accurate result.
  • Hours: First, we calculate the remainder of the hour and divide it by 12. Then we divide that remainder by 12 again to get a decimal value we can multiply by 360. For example, when we’re at the 23rd hour, 23 / 12 = remain 11. Divide this by 12 and we get 0.916 which then gets multiplied by 360 for a grand total of 330. Here, we will do the same thing we did with the minutes and add the minutes angle, divided by 12, for a more accurate result.

Now that we have our angles, the only thing left to do is to update the variables of our clock by adding the following at the end of our function:

clock.style.setProperty("--second-hand-degrees", secondsAngle + "deg");
clock.style.setProperty("--minute-hand-degrees", minsAngle + "deg");
clock.style.setProperty("--hour-hand-degrees", hourAngle + "deg");

Last, but not least, we will trigger the function with an interval of a second to get a working clock:

const clock = document.getElementById("clock");
function setDate() {
  // etc.
}
// Tick tick tick
setInterval(setDate, 1000);
setDate();

See the working demo of our basic clock:

Applying this to a conical gradient

OK, so the hands of our clock are working. What we really want is to map them to a conical gradient that updates as the time changes. You may have seen the same effect if you have an Apple Watch with the “Gradient” face active:

Black Apple Watch on a person's wrist showing a deep purple conic gradient face.
Credit: Macworld

To do this, let’s start by updating our .clock element with a conic gradient and two custom properties that control the starting and ending angles :

.clock {
  /* same as before */

  /* conic gradient vars */
  --start: 0deg;
  --end: 0deg;

  /* same as before */

  background: 
    conic-gradient(
      from var(--start),
      rgb(255 255 255) 2deg,
      rgb(0 0 0 / 0.5) var(--end),
      rgb(255 255 255) 2deg,
      rgb(0 0 0 / 0.7)
  );
}

You can play around with this a bit to style it just the way you like it. I added some extra colors in the gradient to my liking, but as long as you have a starting point and an ending point, you’re good to go.

Next up, we will update our setDate() function so that it updates the variables for our starting and ending points on the conic gradient. The starting point will be our seconds hand, which is easy to find because it will be the same as the angle of our minutes. To make this end at the hours hand, we should make our ending point the same as the hourAngle variable in the script, but subtract our starting point from it.

let startPosition = minsAngle;
let endPosition = hourAngle - minsAngle;

Now we can update our variables with JavaScript again:

clock.style.setProperty("--start", startPosition + "deg");
clock.style.setProperty("--end", endPosition + "deg");

It looks like we could be done at this point, but there is a catch! This calculation works fine as long as the minutes hand has a smaller angle than the hours hand. Our conic gradient will get messy the moment when the minutes hand has moved past it. To fix this, we will use a negative value as a starting point. Luckily, it’s easy to spot when this happens. Before updating our variables we’ll add the following:

if (minsAngle > hourAngle) {
  startPosition = minsAngle - 360;
  endPosition = hourAngle - startPosition;
}

By subtracting 360 from our minutes angle, we are able to set a negative value for our startposition variable. Because of this negative starting point, our end position should be updated by the hour angle, subtracted by the starting position.

There we go — now the hour and minute hands are set to gradient angles:

That’s it! But don’t let that stop you from taking this even further. Create your own styles and share them with me in the comments so I can check them out.. Here is a little inspiration to get you going:


Making a Real-Time Clock With a Conic Gradient Face originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/making-a-real-time-clock-with-a-conic-gradient-face/feed/ 8 373184
My Struggle to Use and Animate a Conic Gradient in SVG https://css-tricks.com/my-struggle-to-use-and-animate-a-conic-gradient-in-svg/ https://css-tricks.com/my-struggle-to-use-and-animate-a-conic-gradient-in-svg/#comments Thu, 08 Jul 2021 14:35:38 +0000 https://css-tricks.com/?p=343623 The wonderful company I work for, Payoneer, has a new logo, and my job was to recreate it and animate it for a loader component in our app. I’ll explain exactly how I did it, share the problems I …


My Struggle to Use and Animate a Conic Gradient in SVG originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
The wonderful company I work for, Payoneer, has a new logo, and my job was to recreate it and animate it for a loader component in our app. I’ll explain exactly how I did it, share the problems I had, and walk you through the solution I came up with. And, as a bonus, we’ll look at animating it!

But first, I guess some of you are asking yourselves… Recreate it? Why?

The branding agency that designed our logo sent us a full set of assets categorized by themes. They came in all sizes and in every available format. We had everything, including SVGs, for the logo and the loader animation. But we couldn’t use them.

Here’s why. Let’s take a look at the logo:

We call the new logo “The Halo.”

The logo is a ring with a conic gradient that consists of five colors, and… that’s it. The problem is that SVG doesn’t support angled gradients (for now, at least), so when we export a design that has a conic gradient as an SVG, we need some sort of hack to get the desired result.

Now, I’m no expert when it comes to working with vector graphic software, so there might be a different (and perhaps better) way to do this, but I know that the most common way to export conic gradients to SVG is to convert the gradient element to an image and insert that image into the SVG as a base64 string. That’s also what we got from the branding agency, and I trust them to know the best way to export an SVG.

But, since the final SVG file now contains a PNG base64 string, the file size jumped to nearly 1MB, which might not be a total disaster, but it’s much higher than the 2KB that it should be. Multiply that difference by three themes (no text, light text, and dark text variations), and we’re looking at 3MB worth of images instead of 3KB worth of code. That’s a big difference, so we’ve decided to recreate the logo with SVG.

But how?!

Even though CSS fully supports conic gradients, SVG does not. So the first question I asked myself was how to create a conic gradient in SVG. Actually, I asked Google. And what I found was a lot of cool, unique, creative ways to add a conic gradients to SVG, most of them relying on some sort of clip-path implementation. I first created a short <path> that represents the shape of the ring and used it as a clip-path on a simple <rect> element.

Next, I needed to fill the <rect> with conic gradients, but first, I had to find all the correct color stops to recreate the look. That took a while, but after a lot of fine tuning, I got a result I’m happy with:

div.gradient {
  background-image: conic-gradient(from 270deg, #ff4800 10%, #dfd902 35%, #20dc68, #0092f4, #da54d8 72% 75%, #ff4800 95%);
}

The last step was to replace the <rect> with something else that supports conic gradients, and the simplest way I’ve found is to use an SVG <foreignObject> element with a regular <div> inside it, and a conic-gradient as a background-image. Then all I needed to do was to set the clip-path on the <foreignObject> element, and that’s it.

So, that’s how I used a conic gradient in an SVG to keep the design fully vector and scalable with less than 20 lines of code, and less than 2KB in file size.

But that was the easy part. Now let’s talk animation.

The loader

Our app shows a loading animation every time a user logs in. We had been using a GIF file for it, but I had been meaning to update it to a pure CSS/SVG animation for months. The benefits are obvious: faster render means a more seamless loading experience, and a smaller file size means even faster loading. We simply get more for less, which is especially ideal for a loading animation.

Here’s the animation I was aiming for:

This type of animation is actually fairly easy with SVG. All we really need is a trick using stroke-dasharray and stroke-dashoffset. That was my starting point. I created a new <path> in the center of the ring, removed the fill, added a stroke with the right stroke-width, and then worked on the animation.

It took me some playing around to get the movement just like the designers wanted it. I ended up using two animations, actually: one controls the stroke-dashoffset, and the second rotates the entire <path> a full turn.

But, since the clip-path property refers to the fill of the shape, animating the stroke meant I had to solve one of two problems: I could either find a different way to animate the movement, or find a different way to add the colors to the stroke.

So I went back to Google and all of the creative ideas I found before, but most of them were pretty much un-animatable, so I started looking for a good non-clip-path way to add colors to the stroke. I looked at a few “out-of-the-box” solutions, checked out masking, and ended up with the simplest perfect solution:

.logoBlend {
  mix-blend-mode: lighten;
}

A lighten blend mode looks at the RGB colors of each pixel of the rendered element, compares it to the RGB value of the background pixel that’s behind it, and keeps whichever is highest. That means that the parts of the element that are white will remain white, and the dark parts will get the values of the background pixel.

By adding a white <rect> to the black path, I essentially blocked anything that’s behind it. Meanwhile, everything that’s behind the animated black stroke is visible. That meant I could bring back the <foreignObject> with the conic-gradient, put it behind the mix-blend-mode layer, and give it a simple rotate animation to match the design.

Note that the end result of this method will have a white background, not transparent like the static logo, but I was fine with that. If you need to, you can flip it around, use black background, and hide the light parts of your element by setting the blend mode to darken.

Final touches

I was pretty much done at this point, and quite happy with the end result. But a couple of days later, I got a Lottie-based JSON file from the branding agency with the exact same animation. In retrospect, maybe I could spare my work and use their file, it would have worked just fine. Even the file size was surprisingly small, but it was still 8✕ bigger than the SVG, so we ended up using my animation anyway.

But, that meant I had one last thing to do. The Lottie animation had a “start animation” where the small orange dot grows into view, and I had to add it to my animation as well. I added a short 0.5s delay to all three animations as well as a scaling animation in the beginning.

Click on “Rerun” on the Pen to see the animation again from the initial dot.

That’s it! Now my company has a new logo and a set of lightweight, fully scalable assets to use across our web platforms.

And for those of you wondering, yes, I did end up creating a nice little Logo component in React since we’re using it. It even renders the SVG according to a theme passed to it as a prop, making the implementation easier, and keeping all future changes in a single location.

What about you?

Do you think there’s a better way to get the same result? Share your thoughts in the comments! And thank you for reading.


My Struggle to Use and Animate a Conic Gradient in SVG originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/my-struggle-to-use-and-animate-a-conic-gradient-in-svg/feed/ 14 343623
Variable Aspect Ratio Card With Conic Gradients Meeting Along the Diagonal https://css-tricks.com/variable-aspect-ratio-card-with-conic-gradients-meeting-along-the-diagonal/ https://css-tricks.com/variable-aspect-ratio-card-with-conic-gradients-meeting-along-the-diagonal/#comments Mon, 10 May 2021 14:13:12 +0000 https://css-tricks.com/?p=339203 I recently came across an interesting problem. I had to implement a grid of cards with a variable (user-set) aspect ratio that was stored in a --ratio custom property. Boxes with a certain aspect ratio are a classic problem in …


Variable Aspect Ratio Card With Conic Gradients Meeting Along the Diagonal originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I recently came across an interesting problem. I had to implement a grid of cards with a variable (user-set) aspect ratio that was stored in a --ratio custom property. Boxes with a certain aspect ratio are a classic problem in CSS and one that got easier to solve in recent years, especially since we got aspect-ratio, but the tricky part here was that each of the cards needed to have two conic gradients at opposite corners meeting along the diagonal. Something like this:

A 3 by 3 grid of square cards with color backgrounds made from conic gradients. The gradients appear like stripes that extend from opposite corners of the card. Each card reads Hello Gorgeous in a fancy script font.
User set aspect ratio cards.

The challenge here is that, while it’s easy to make an abrupt change in a linear-gradient() along the diagonal of a variable aspect ratio box using for example a direction like to top left which changes with the aspect ratio, a conic-gradient() needs either an angle or a percentage representing how far it has gone around a full circle.

Check out this guide for a refresher on how conic gradients work.

The simple solution

The spec now includes trigonometric and inverse trigonometric functions, which could help us here — the angle of the diagonal with the vertical is the arctangent of the aspect ratio atan(var(--ratio)) (the left and top edges of the rectangle and the diagonal form a right triangle where the tangent of the angle formed by the diagonal with the vertical is the width over the height — precisely our aspect ratio).

Illustration. Shows a rectangle of width w and height h with a diagonal drawn from the bottom left corner to the top right one. This diagonal is the hypotenuse of the right triangle whose catheti are the left and top edges of the rectangle. In this right triangle, the tangent of the angle between the hypotenuse (diagonal of original rectangle) with the vertical (left edge of original rectangle) is the top edge (w) over the left edge (h).
The angle of the diagonal with the vertical (edge).

Putting it into code, we have:

--ratio: 3/ 2;
aspect-ratio: var(--ratio);
--angle: atan(var(--ratio));
background: 
  /* below the diagonal */
  conic-gradient(from var(--angle) at 0 100%, 
      #319197, #ff7a18, #af002d  calc(90deg - var(--angle)), transparent 0%), 
  /* above the diagonal */
  conic-gradient(from calc(.5turn + var(--angle)) at 100% 0, 
      #ff7a18, #af002d, #319197 calc(90deg - var(--angle)));

However, no browser currently implements trigonometric and inverse trigonometric functions, so the simple solution is just a future one and not one that would actually work anywhere today.

The JavaScript solution

We can of course compute the --angle in the JavaScript from the --ratio value.

let angle = Math.atan(1/ratio.split('/').map(c => +c.trim()).reduce((a, c) => c/a, 1));
document.body.style.setProperty('--angle', `${+(180*angle/Math.PI).toFixed(2)}deg`)

But what if using JavaScript won’t do? What if we really need a pure CSS solution? Well, it’s a bit hacky, but it can be done!

The hacky CSS solution

This is an idea I got from a peculiarity of SVG gradients that I honestly found very frustrating when I first encountered.

Let’s say we have a gradient with a sharp transition at 50% going from bottom to top since in CSS, that’s a gradient at a angle. Now let’s say we have the same gradient in SVG and we change the angle of both gradients to the same value.

In CSS, that’s:

linear-gradient(45deg, var(--stop-list));

In SVG, we have:

<linearGradient id='g' y1='100%' x2='0%' y2='0%' 
                gradientTransform='rotate(45 .5 .5)'>
  <!-- the gradient stops -->
</linearGradient>

As it can be seen below, these two don’t give us the same result. While the CSS gradient really is at 45°, the SVG gradient rotated by the same 45° has that sharp transition between orange and red along the diagonal, even though our box isn’t square, so the diagonal isn’t at 45°!

Screenshot. Shows a rectangle with a CSS gradient at 45° (left) vs. a rectangle with a bottom to top SVG gradient rotated by 45° (right). This angle is adjustable via the slider at the bottom. The CSS gradient is really at 45°, but the line of the SVG gradient is perpendicular onto the rectangle's diagonal.
45° CSS vs. SVG gradient (DEMO).

This is because our SVG gradient gets drawn within a 1x1 square box, rotated by 45°, which puts the abrupt change from orange to red along the square diagonal. Then this square is stretched to fit the rectangle, which basically changes the diagonal angle.

Note that this SVG gradient distortion happens only if we don’t change the gradientUnits attribute of the linearGradient from its default value of objectBoundingBox to userSpaceOnUse.

Basic idea

We cannot use SVG here since it only has linear and radial gradients, but not conic ones. However, we can put our CSS conic gradients in a square box and use the 45° angle to make them meet along the diagonal:

aspect-ratio: 1/ 1;
width: 19em;
background: 
  /* below the diagonal */
  conic-gradient(from 45deg at 0 100%, 
      #319197, #ff7a18, #af002d 45deg, transparent 0%), 
  /* above the diagonal */
  conic-gradient(from calc(.5turn + 45deg) at 100% 0, 
      #ff7a18, #af002d, #319197 45deg);

Then we can stretch this square box using a scaling transform – the trick is that the ‘/’ in the 3/ 2 is a separator when used as an aspect-ratio value, but gets parsed as division inside a calc():

--ratio: 3/ 2;
transform: scaley(calc(1/(var(--ratio))));

You can play with changing the value of --ratio in the editable code embed below to see that, this way, the two conic gradients always meet along the diagonal:

Note that this demo will only work in a browser that supports aspect-ratio. This property is supported out of the box in Chrome 88+ (current version is 90), but Firefox still needs the layout.css.aspect-ratio.enabled flag to be set to true in about:config. And if you’re using Safari… well, I’m sorry!

Screenshot showing how to enable the Firefox flag. Go to about:config (type that in the address bar - you may be asked if you're sure you want to mess with that stuff before you're allowed to enter). Use the search bar to look for 'aspect' - this should be enough to bring up the flag. Set its value to true.
Enabling the flag in Firefox.

Issues with this approach and how to get around them

Scaling the actual .card element would rarely be a good idea though. For my use case, the cards are on a grid and setting a directional scale on them messes up the layout (the grid cells are still square, even though we’ve scaled the .card elements in them). They also have text content which gets weirdly stretched by the scaley() function.

Screenshot. Shows how the card elements are scaled down vertically, yet the grid cells they're occupying have remained square, just like the cards before the directional scaling.
The problem with scaling the actual cards (DEMO)

The solution is to give the actual cards the desired aspect-ratio and use an absolutely positioned ::before placed behind the text content (using z-index: -1) in order to create our background. This pseudo-element gets the width of its .card parent and is initially square. We also set the directional scaling and conic gradients from earlier on it. Note that since our absolutely positioned ::before is top-aligned with the top edge of its .card parent, we should also scale it relative to this edge as well (the transform-origin needs to have a value of 0 along the y axis, while the x axis value doesn’t matter and can be anything).

body {
  --ratio: 3/ 2;
  /* other layout and prettifying styles */
}

.card {
  position: relative;
  aspect-ratio: var(--ratio);

  &::before {
    position: absolute;
    z-index: -1; /* place it behind text content */

    aspect-ratio: 1/ 1; /* make card square */
    width: 100%;
    	
    /* make it scale relative to the top edge it's aligned to */
    transform-origin: 0 0;
    /* give it desired aspect ratio with transforms */
    transform: scaley(calc(1/(var(--ratio))));
    /* set background */
    background: 
      /* below the diagonal */
      conic-gradient(from 45deg at 0 100%, 
      #319197, #af002d, #ff7a18 45deg, transparent 0%), 
      /* above the diagonal */
      conic-gradient(from calc(.5turn + 45deg) at 100% 0, 
      #ff7a18, #af002d, #319197 45deg);
    content: '';
  }
}

Note that we’ve moved from CSS to SCSS in this example.

This is much better, as it can be seen in the embed below, which is also editable so you can play with the --ratio and see how everything adapts nicely as you change its value.

Padding problems

Since we haven’t set a padding on the card, the text may go all the way to the edge and even slightly out of bounds given it’s a bit slanted.

Screenshot. Shows a case where the text goes all the way to the edge of the card and even goes out a tiny little bit creating an ugly result.
Lack of padding causing problems.

That shouldn’t be too difficult to fix, right? We just add a padding, right? Well, when we do that, we discover the layout breaks!

Animated gif. Shows the dev tools grid overlay t highlight that, while the background (created with the scaled pseudo) still has the desired aspect ratio, the grid cell and the actual card in it are taller.
Adding a padding breaks the layout. (DEMO)

This is because the aspect-ratio we’ve set on our .card elements is that of the .card box specified by box-sizing. Since we haven’t explicitly set any box-sizing value, its current value is the default one, content-box. Adding a padding of the same value around this box gives us a padding-box of a different aspect ratio that doesn’t coincide with that of its ::before pseudo-element anymore.

In order to better understand this, let’s say our aspect-ratio is 4/ 1 and the width of the content-box is 16rem (256px). This means the height of the content-box is a quarter of this width, which computes to 4rem (64px). So the content-box is a 16rem×4rem (256px×64px) rectangle.

Now let’s say we add a padding of 1rem (16px) along every edge. The width of the padding-box is therefore 18rem (288px, as it can be seen in the animated GIF above) — computed as the width of the content-box, which is 16rem (256px) plus 1rem (16px) on the left and 1rem on the right from the padding. Similarly, the height of the padding-box is 6rem (96px) — computed as the height of the content-box, which is 4rem (64px), plus 1rem (16px) at the top and 1rem at the bottom from the padding).

This means the padding-box is a 18rem×6rem (288px×96px) rectangle and, since 18 = 3⋅6, it has a 3/ 1 aspect ratio which is different from the 4/ 1 value we’ve set for the aspect-ratio property! At the same time, the ::before pseudo-element has a width equal to that of its parent’s padding-box (which we’ve computed to be 18rem or 288px) and its aspect ratio (set by scaling) is still 4/ 1, so its visual height computes to 4.5rem (72px). This explains why the background created with this pseudo — scaled down vertically to a 18rem×4.5rem (288px×72px) rectangle — is now shorter than the actual card — a 18rem×6rem (288px×96px) rectangle now with the padding.

So, it looks like the solution is pretty straightforward — we need to set box-sizing to border-box to fix our problem as this applied the aspect-ratio on this box (identical to the padding-box when we don’t have a border).

Sure enough, this fixes things… but only in Firefox!

Screenshot collage. Shows how the text is not middle aligned in Chromium browsers (top), while Firefox (bottom) gets this right.
Showing the difference between Chromium (top) and Firefox (bottom).

The text should be middle-aligned vertically as we’ve given our .card elements a grid layout and set place-content: center on them. However, this doesn’t happen in Chromium browsers and it becomes a bit more obvious why when we take out this last declaration — somehow, the cell in the card’s grid gets the 3/ 1 aspect ratio too and overflows the card’s content-box:

Animated gif. For some reason, the grid cell inside the card gets the set aspect ratio and overflows the card's content-box.
Checking the card’s grid with vs. without place-content: center (DEMO).

Fortunately, this is a known Chromium bug that should probably get fixed in the coming months.

In the meantime, what we can do to get around this is remove the box-sizing, padding and place-content declarations from the .card element, move the text in a child element (or in the ::after pseudo if it’s just a one-liner and we’re lazy, though an actual child is the better idea if we want the text to stay selectable) and make that a grid with a padding.

.card {
  /* same as before, 
     minus the box-sizing, place-content and padding declarations 
     the last two of which which we move on the child element */
  
  &__content {
    place-content: center;
    padding: 1em
  }
}

Rounded corners

Let’s say we also want our cards to have rounded corners. Since a directional transform like the scaley on the ::before pseudo-element that creates our background also distorts corner rounding, it results that the simplest way to achieve this is to set a border-radius on the actual .card element and cut out everything outside that rounding with overflow: hidden.

Screenshot. Shows an element that's not scaled at all on the left. This has a perfectly circular border-radius. In the right, there's a non-uniform scaled element - its border-radius is not perfectly circular anymore, but instead distorted by the scaling.
Non-uniform scaling distorts corner rounding. (DEMO)

However, this becomes problematic if at some point we want some other descendant of our .card to be visible outside of it. So, what we’re going to do is set the border-radius directly on the ::before pseudo that creates the card background and reverse the directional scaling transform along the y axis on the y component of this border-radius:

$r: .5rem;

.card {
  /* same as before */
  
  &::before {
    border-radius: #{$r}/ calc(#{$r}*var(--ratio));
    transform: scaley(calc(1/(var(--ratio))));
    /* same as before */
  }
}

Final result

Putting it all together, here’s an interactive demo that allows changing the aspect ratio by dragging a slider – every time the slider value changes, the --ratio variable is updated:


Variable Aspect Ratio Card With Conic Gradients Meeting Along the Diagonal originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/variable-aspect-ratio-card-with-conic-gradients-meeting-along-the-diagonal/feed/ 4 339203
The “Gray Dead Zone” of Gradients https://css-tricks.com/the-gray-dead-zone-of-gradients/ https://css-tricks.com/the-gray-dead-zone-of-gradients/#comments Mon, 01 Mar 2021 19:16:50 +0000 https://css-tricks.com/?p=335427 Erik D. Kennedy notes an interesting phenomenon of color gradients. If you have a gradient between two colors where the line between them in the color space goes through the zero-saturation middle, you get this “gray dead zone” in …


The “Gray Dead Zone” of Gradients originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Erik D. Kennedy notes an interesting phenomenon of color gradients. If you have a gradient between two colors where the line between them in the color space goes through the zero-saturation middle, you get this “gray dead zone” in the middle.

It’s a real thing. See the gray middle here:

You can also see how colors might not do that, like red and blue here shooting right through purple instead, which you can visualize on that color circle above.

Erik says one solution can be taking a little detour instead of going straight through the gray zone:

His updated gradient tool deals with this by using different “interpolation modes” and easing the gradient with a choice of precision stops. Don’t miss the radial and conic options as well, with the ability to place the centers “offscreen” which can yield pretty cool looks you can’t do any other way.


Oh and speaking of conic gradients, Adam Argyle has a little gallery of possibilities that is pretty unique.


The “Gray Dead Zone” of Gradients originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/the-gray-dead-zone-of-gradients/feed/ 7 335427
Firefox 83 https://css-tricks.com/firefox-83/ https://css-tricks.com/firefox-83/#respond Wed, 18 Nov 2020 23:26:03 +0000 https://css-tricks.com/?p=326021 There’s a small line in the changelog that is is big news for CSS:

We’ve added support for CSS Conic Gradients (bug 1632351) and (bug 1175958).

🎉🎉🎉

Conic gradients are circular, just like their radial counterpart, but place color …


Firefox 83 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
There’s a small line in the changelog that is is big news for CSS:

We’ve added support for CSS Conic Gradients (bug 1632351) and (bug 1175958).

🎉🎉🎉

Conic gradients are circular, just like their radial counterpart, but place color stops around the circle instead of from the center of the circle.

Adding more color stops gives it a “cone-like” appearance that’s fitting of the name:

Prior to Firefox 83, cross-browser support for conic gradients meant using a polyfill, like this one from Lea Verou. But browser support is much nicer with Firefox in the mix.

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

ChromeFirefoxIEEdgeSafari
6983No7912.1

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
10810710812.2-12.5

And wouldn’t you know it! We just so happen to have a brand-spankin’ new CSS Gradients guide that not only covers conic gradients, but linear, radial, and repeating gradients as well, including explanations, examples and, of course, plenty of CSS tricks sprinkled in along the way.

One of those tricks? Using hard color stops to create a pie chart.

To Shared LinkPermalink on CSS-Tricks


Firefox 83 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/firefox-83/feed/ 0 326021
Background Patterns, Simplified by Conic Gradients https://css-tricks.com/background-patterns-simplified-by-conic-gradients/ https://css-tricks.com/background-patterns-simplified-by-conic-gradients/#comments Thu, 28 May 2020 14:37:00 +0000 https://css-tricks.com/?p=307270 For those who have missed the big news, Firefox now supports conic gradients!

Starting with Firefox 75, released on the April 7, we can go to about:config, look for the layout.css.conic-gradient.enabled flag and set its value to true


Background Patterns, Simplified by Conic Gradients originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
For those who have missed the big news, Firefox now supports conic gradients!

Starting with Firefox 75, released on the April 7, we can go to about:config, look for the layout.css.conic-gradient.enabled flag and set its value to true (it’s false by default and all it takes to switch is double-clicking it).

Screenshot. Shows the Firefox URL bar at `about:config`, a search for 'conic' giving the `layout.css.conic-gradient.enabled` flag as the sole result and its value set to `true`.
Enabling conic gradients in Firefox 75+

With that enabled, now we can test our CSS including conic gradients in Firefox as well.

While some of the demos in this article work just fine when using a polyfill, some use CSS variables inside the conic gradient and therefore require native support for this feature.

One thing I particularly like about conic gradients is just how much they can simplify background patterns. So let’s take a few linear-gradient() patterns from the gallery created by Lea Verou about a decade ago and see how we can now simplify them with conic-gradient!

Pyramid

Screenshot. Shows the original pyramid pattern with the code that was used to create it.
The pyramid pattern

The pattern above uses four linear gradients:

background:
  linear-gradient(315deg, transparent 75%, #d45d55 0) -10px 0,
  linear-gradient(45deg, transparent 75%, #d45d55 0) -10px 0,
  linear-gradient(135deg, #a7332b 50%, transparent 0) 0 0,
  linear-gradient(45deg, #6a201b 50%, #561a16 0) 0 0 #561a16;
background-size: 20px 20px;

That’s quite a bit of CSS and perhaps even a bit intimidating. It’s not easy to just look at this and understand how it all adds up to give us the pyramid pattern. I certainly couldn’t do it. It took me a while to get it, even though gradients are one of the CSS features I’m most comfortable with. So don’t worry if you don’t understand how those gradients manage to create the pyramid pattern because, one, it is complicated and, two, you don’t even need to understand that!

Using conic-gradient(), we can now get the same result in a much simpler manner, with a single background layer instead of four!

What I like to do when coding repeating patterns is draw equidistant vertical and horizontal lines delimiting the rectangular boxes defined by the background-size. In this case, it’s pretty obvious we have square boxes and where their limits are, but it’s a really useful technique for more complex patterns.

Annotated screenshot. Shows the rectangles (squares in this case) defined by the `background-size`.
Highlighting the pattern’s cells

By default, conic gradients start from 12 o’clock and go clockwise. However, in our case, we want to offset the start of the gradient by 45° in the clockwise direction and afterwards make every one of the four shades occupy a quarter (25%) of the available space around the midpoint of our square box.

SVG illustration. Shows how we place a conic gradient into a single pattern cell by rotating the gradient start point 45° in the clockwise (positive) direction.
A pattern cell with a conic gradient’s hard stops at every 25% starting from 45° w.r.t. the vertical axis (live).

This means our pyramid pattern can be reduced to:

$s: 20px;
background:
  conic-gradient(from 45deg, 
    #561a16 25%, 
    #6a201b 0% 50%, 
    #a7332b 0% 75%, 
    #d45d55 0%) 
    50%/ #{$s $s};

Not only does the code look simpler, but we’ve also gone from 260 bytes to 103 bytes, reducing the code needed to get this pattern by more than half.

We’re using the double position syntax as that’s also well supported these days.

We can see it in action in the Pen below:

Checkerboard

Screenshot. Shows the original checkerboard pattern with the code that was used to create it.
The checkerboard pattern

This pattern above is created with two linear gradients:

background-color: #eee;
background-image:
  linear-gradient(45deg, black 25%, transparent 25%, 
    transparent 75%, black 75%, black),
  linear-gradient(45deg, black 25%, transparent 25%, 
    transparent 75%, black 75%, black);
background-size: 60px 60px;
background-position: 0 0, 30px 30px;

Let’s see how we can simplify this CSS when replacing these linear gradients with a conic one!

Just like in the previous case, we draw vertical and horizontal lines in order to better see the rectangles defined by the background-size.

Annotated screenshot. Shows the rectangles (squares in this case) defined by the `background-size`.
Highlighting the pattern’s cells

Looking at the square highlighted in deeppink in the illustration above, we see that, in this case, our conic gradient starts from the default position at 12 o’clock. A quarter of it is black, the next quarter is dirty white and then we have repetition (the same black and then dirty white quarter slices once more).

SVG illustration. Shows how we place a conic gradient into a single pattern cell and then make it repeat after the 50% point.
A pattern cell with a conic gradient’s hard stops at every 25%, starting from the default at 12 o’clock and repeating after 50% (demo).

This repetition in the second half of the [0%, 100%] interval means we can use a repeating-conic-gradient(), which gives us the following code (bringing the compiled CSS from 263 bytes down to only 73 bytes – that’s reducing it by over 70%):

$s: 60px;
background:
  repeating-conic-gradient(#000 0% 25%, #eee 0% 50%) 
    50%/ #{$s $s};

The Pen below shows it in action:

Diagonal checkerboard

Screenshot. Shows the original diagonal checkerboard pattern with the code that was used to create it.
The diagonal checkerboard pattern

Again, we have a pattern created with two linear gradients:

background-color: #eee;
background-image: 
  linear-gradient(45deg, black 25%, transparent 25%, 
    transparent 75%, black 75%, black),
  linear-gradient(-45deg, black 25%, transparent 25%, 
    transparent 75%, black 75%, black);
background-size: 60px 60px;

We draw horizontal and vertical lines to split this pattern into identical rectangles:

Annotated screenshot. Shows the rectangles (squares in this case) defined by the `background-size`.
Highlighting the pattern’s cells

What we now have is pretty much the same checkerbox pattern as before, with the sole difference that we don’t start from the default position at 12 o’clock, but from 45° in the clockwise direction.

If you’re having trouble visualising how simply changing the start angle can make us go from the previous pattern to this one, you can play with it in the interactive demo below:

Note that this demo does not work in browsers that have no native support for conic gradients.

This means our code looks as follows:

$s: 60px;
background:
  repeating-conic-gradient(from 45deg, 
    #000 0% 25%, #eee 0% 50%) 
  50%/ #{$s $s};

We can see it in action below:

Again, not only is the code simpler to understand, but we’ve also gone from 229 bytes to only 83 bytes in the compiled CSS, reducing it by almost two-thirds!

Half-Rombes

Screenshot. Shows the original Half-Rombes pattern with the code that was used to create it.
The half-rombes pattern

This pattern was created with four linear gradients:

background: #36c;
background:
  linear-gradient(115deg, transparent 75%, rgba(255,255,255,.8) 75%) 0 0,
  linear-gradient(245deg, transparent 75%, rgba(255,255,255,.8) 75%) 0 0,
  linear-gradient(115deg, transparent 75%, rgba(255,255,255,.8) 75%) 7px -15px,
  linear-gradient(245deg, transparent 75%, rgba(255,255,255,.8) 75%) 7px -15px,
  #36c;
background-size: 15px 30px;

Just like in the previous cases, we draw equidistant vertical and horizontal lines in order to better see the repeating unit:

Annotated screenshot. Shows the rectangles (squares in this case) defined by the `background-size`.
Highlighting the pattern’s cells.

What we have here is a pattern that’s made up of congruent isosceles triangles (the angled edges are equal and the dark blue triangles are a reflection of the light blue ones) formed by the intersection of equidistant parallel lines that are either horizontal, angled clockwise, or the other way. Each of these three types of parallel lines is highlighted in the illustration below:

Illustration. Shows the equidistant parallel lines which create the pattern of isosceles triangles.
Parallel guides

Every pattern cell contains a full triangle and two adjacent triangle halves in the upper part, then a reflection of this upper part in the lower part. This means we can identify a bunch of congruent right triangles that will help us get the angles we need for our conic-gradient():

SVG illustration. Shows how we place a conic gradient into a single pattern cell by rotating the gradient start point by an angle β in the clockwise (positive) direction such that the 0% line goes through the top right corner and then all the other hard stops are either horizontal or going through the cell corners.
A pattern cell with a conic gradient’s hard stops such that they’re either horizontal or go through the cell corners, all starting from β w.r.t. the vertical axis (demo)

This illustration shows us that the gradient starts from an angle, β, away from the default conic gradient start point at 12 o’clock. The first conic slice (the top right half triangle) goes up to α, the second one (the bottom right dark triangle) up to 2·α, and the third one (the bottom light triangle) goes halfway around the circle from the start (that’s 180°, or 50%). The fourth one (the bottom left dark triangle) goes to 180° + α and the fifth one (the top left light triangle) goes to 180° + 2·α, while the sixth one covers the rest.

SVG illustration. Highlights the right triangle from where we can get α knowing the catheti and shows how we can then compute β.
Getting α and β (demo)

From the highlighted right triangle we get that:

tan(α) = (.5·h)/(.5·w) = h/w

Knowing the width (w) and height (h) of a pattern cell, we can get the angles α and β:

α = atan(h/w)
β = 90° - α

It results in the pattern that’s generated by the following code:

$w: 15px;
$h: 30px;
$a: atan($h/$w)*180deg/pi();
$b: 90deg - $a;
$c0: #36c;
$c1: #d6e0f5;

html {
  background: 
    conic-gradient(from $b, 
      $c1 0% $a, 
      $c0 0% 2*$a, 
      $c1 0% 50%, 
      $c0 0% 180deg + $a, 
      $c1 0% 180deg + 2*$a, 
      $c0 0%) 
    0 0/ #{$w $h};
}

This means going from 343 bytes to only 157 bytes in the compiled CSS. The result can be seen below:

You can tweak the pattern width ($w) and height ($h) in the Sass code in order to see how the pattern gets squished and stretched for different aspect ratios.

In the particular case where the angle between 2*$a and 50% (or 180deg) is also $a, it results that $a is 60deg, our isosceles triangles are equilateral, and our gradient can be reduced to a repeating one (and under 100 bytes in the compiled CSS):

$a: 60deg;
$b: 90deg - $a;
$w: 15px;
$h: $w*tan($a);
$c0: #36c;
$c1: #d6e0f5;

html {
  background: 
    repeating-conic-gradient(from $b, 
      $c1 0% $a, $c0 0% 2*$a) 
    0 0/ #{$w $h}
}

The live result can be seen below:

Bonus: Intersecting line backgrounds!

Screenshot. Shows the original intersecting lines pattern with the code that was used to create it.
Intersecting line background examples

While these are not repeating patterns, they’re examples of a situation where a single conic gradient achieves an effect that would have previously needed a bunch of linear ones.

What we have here is a conic-gradient() created starting from two straight lines intersecting within the rectangular box where we set the background.

SVG illustration. Shows a rectangular box and two random lines intersecting inside it. This intersection point (x,y) is the point the conic gradient goes around, while the gradient's start is from the angle β formed by the line segment closest to the top right corner with the vertical. The hard stops are at α, the angle between the start segment and the next one in clockwise order, at 50% and at 180° + α.
Bonus pattern structure (ldemo)

The gradient goes around the point of coordinates, x,y, where the two straight lines intersect. It starts from an angle, β, which is the angle of the line segment that’s closest to the top-right corner, then has hard stops at α, 50% (or 180°) and 180° + α.

If we want to have multiple elements with similar such patterns created with the help of different intersecting lines and different palettes, we have the perfect use case for CSS variables:

.panel {
  background: 
    conic-gradient(from var(--b) at var(--xy), 
      var(--c0) var(--a), var(--c1) 0% 50%, 
      var(--c2) 0% calc(180deg + var(--a)), var(--c3) 0%);
}

All we have to do is set the position (--xy), the start angle (--b), the first angle (--a) and the palette (--c0 through --c3).

.panel {
  /* same as before */
  
  &:nth-child(1) {
    --xy: 80% 65%; 
    --b: 31deg;
    --a: 121deg; 
    --c0: #be5128;
    --c1: #ce9248;
    --c2: #e4c060;
    --c3: #db9c4e
  }
  
  /* similarly for the other panels */
}

Instead of hardcoding, we could also generate these values randomly or extract them from a data object with the help of a CSS or HTML preprocessor. In this second case, we’d set these custom properties inline, which is precisely what I did in the Pen below:

Since we’re using custom properties inside the conic gradients, this demo does not work in browsers that don’t support them natively.

Well, that’s it! I hope you’ve enjoyed this article and that it gives you some ideas about how conic gradients can make your life easier.


Background Patterns, Simplified by Conic Gradients originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/background-patterns-simplified-by-conic-gradients/feed/ 4 307270
Simple Interactive Pie Chart with CSS Variables and Houdini Magic https://css-tricks.com/simple-interactive-pie-chart-with-css-variables-and-houdini-magic/ https://css-tricks.com/simple-interactive-pie-chart-with-css-variables-and-houdini-magic/#comments Mon, 13 Aug 2018 13:57:13 +0000 http://css-tricks.com/?p=273668 I got the idea for doing something of the kind when I stumbled across this interactive SVG pie chart. While the SVG code is as compact as it gets (a single <circle> element!), using strokes for creating pie chart …


Simple Interactive Pie Chart with CSS Variables and Houdini Magic originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I got the idea for doing something of the kind when I stumbled across this interactive SVG pie chart. While the SVG code is as compact as it gets (a single <circle> element!), using strokes for creating pie chart slices is problematic as we run into rendering issues on Windows for Firefox and Edge. Plus, in 2018, we can accomplish a lot more with a lot less JavaScript!

I managed to get the following result using a single HTML element for the chart and very little JavaScript. The future should completely eliminate the need for any JavaScript, but more on that later.

Animated gif. Shows how the final pie chart behaves. We have a value label over our pie chart and three radio buttons below it. Each radio button selects a year and sets the pie chart to show the value for the selected year. When changing the year and, consequently the value, the pie chart animates from its previous state to the current one.
The final pie chart result.

Some of you may remember Lea Verou’s Missing Slice talk—my solution is based on her technique. This article dissects how it all works, showing what we can do in terms of graceful degradation and other ways this technique can be put to use.

The HTML

We use Pug to generate the HTML from a data object that contains unitless percentage values for the past three years:

- var data = { 2016: 20, 2017: 26, 2018: 29 }

We make all our elements reside in a .wrap element. Then, we loop through our data object and, for each of its properties, we create a radio input with a corresponding label. After these, we add a .pie element to the mix.

- var darr = [], val;

.wrap
  - for(var p in data) {
    - if(!val) val = data[p];
    input(id=`o${p}` name='o' type='radio' checked=val == data[p])
    label(for=`o${p}`) #{p}
    - darr.push(`${data[p]}% for year ${p}`)
  - }
  .pie(aria-label=`Value as pie chart. ${darr.join(', ')}.` 
       role='graphics-document group')

The above Pug code compiles to the following HTML:

<style><div class="wrap">
  <input id="o2016" name="o" type="radio" checked="checked"/>
  <label for="o2016">2016</label>
  <input id="o2017" name="o" type="radio"/>
  <label for="o2017">2017</label>
  <input id="o2018" name="o" type="radio"/>
  <label for="o2018">2018</label>
  <div class="pie" aria-label="Value as pie chart. 20% for year 2016, 26% for year 2017, 29% for year 2018." role="graphics-document group"></div>
</div>

Note that we also made sure only the first radio input is checked.

Passing custom properties to the CSS

I normally don’t like putting styles in HTML but, in this particular case, it’s a very useful way to pass custom property values to the CSS and ensure that we only need to update things in one place if we need to change any of our data points—the Pug code. The CSS remains the same.

The trick is to set a unitless percentage --p on the .pie element for every radio input that might be checked:

style
  - for(var p in data) {
    | #o#{p}:checked ~ .pie { --p: #{data[p]} }
  - }

We use this percentage for a conic-gradient() on the .pie element after making sure neither of its dimensions (including border and padding) are 0:

$d: 20rem;

.wrap { width: $d; }

.pie {
  padding: 50%;
  background: conic-gradient(#ab3e5b calc(var(--p)*1%), #ef746f 0%);
}

Note that this requires native conic-gradient() support since the polyfill doesn’t work with CSS variables. At the moment, this limits support to Blink browsers with the Experimental Web Platform features flag enabled, though things are bound to get better.

Screenshot showing the Experimental Web Platform Features flag being enabled in Chrome.
The Experimental Web Platform features flag enabled in Chrome.

Update: Chrome 69+ now supports conic-gradient() natively without the flag as well.

We now have a working skeleton of our demo—picking a different year via the radio buttons results in a different conic-gradient()!

Animated gif. Shows how picking a different year via the radio buttons changes the chart to show the value for the selected year.
The basic functionality we’ve been after (live demo, Blink browsers only).

Displaying the value

The next step is to actually display the current value and we do this via a pseudo-element. Unfortunately, number-valued CSS variables cannot be used for the value of the content property, so we get around this by using the counter() hack.

.pie:after {
  counter-reset: p var(--p);
  content: counter(p) '%';
}

We’ve also tweaked the color and font-size properties so that our pseudo-element is a bit more visible:

Screenshot. Shows the chart with the value for the current year (the one selected by the checked radio button) overlayed on top.
Displaying the value on the chart (live demo, Blink browsers only).

Smoothing things out

We don’t want abrupt changes between values, so we smooth things out with the help of a CSS transition. Before we can transition or animate the --p variable, we need to register it in JavaScript:

CSS.registerProperty({
  name: '--p', 
  syntax: '<integer>',
  initialValue: 0, 
  inherits: true
});

Note that using <number> instead of <integer> causes the displayed value to go to 0 during the transition as our counter needs an integer. Thanks to Lea Verou for helping me figure this out!

Also note that explicitly setting inherits is mandatory. This wasn’t the case until recently.

This is all the JavaScript we need for this demo and, in the future, we shouldn’t even need this much as we’ll be able to register custom properties from the CSS.

With that out of the way, we can add a transition on our .pie element.

.pie {
  /* same styles as before */
  transition: --p .5s;
}

And that’s it for the functionality! All done with one element, one custom variable, and a sprinkle of Houdini magic!

Animated gif. Shows how picking a different year via the radio buttons smoothly changes both the actual chart and the number value displayed on top of it to represent the value for the newly selected year.
Interactive pie chart (live demo, Blink browsers with flag only).

Prettifying touches

While our demo is functional, it looks anything but pretty at this point. So, let’s take care of that while we’re at it!

Making the pie… a pie!

Since the presence of :after has increased the height of its .pie parent, we absolutely position it. And since we want our .pie element to look more like an actual pie, we make it round with border-radius: 50%.

Screenshot. Shows the chart now being perfectly round like a pie.
Rounding up our pie (live demo, Blink browsers only, transition needs flag).

We also want to display the percentage value in the middle of the dark pie slice.

In order to do this, we first position it dead in the middle of the .pie element. By default, the :after pseudo-element is displayed after its parent’s content. Since .pie has no content in this case, the top-left corner of the :after pseudo-element is in the top-left corner of the parent’s content-box. Here, the content-box is a 0x0 box in the center of the padding-box. Remember that we’ve set the padding of .pie to 50%—a value that’s relative to the wrapper width for both the horizontal and the vertical direction!

This means the top-left corner of :after is in the middle of its parent, so a translate(-50%, -50%) on it shifts it to the left by half its own width and up by half its own height, making its own mid-point coincide with that of .pie.

Remember that %-valued translations are relative to the dimensions of the element they’re applied on along the corresponding axis. In other words, a %-valued translation along the x-axis is relative to the element’s width, a %-valued translation along the y-axis is relative to its height and a %-valued translation along the z-axis is relative to its depth, which is always 0 because all elements are flat two-dimensional boxes with 0 depth along the third axis.

Screenshot. Shows the chart now being perfectly round like a pie, with the percentage as text positioned right in the middle.
Positioning the value label in the middle of the pie (live demo, Blink browsers only, transition needs flag).

Next, we rotate the value such that the positive half of its x-axis splits the dark slice into two equal halves and then translate it by half a pie radius along this now-rotated x-axis.

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

What we need to figure out is how much to rotate the :after pseudo-element so that its x-axis splits the dark slice into two equal halves. Let’s break that down!

Initially, the x-axis is horizontal, pointing towards the right, so in order to have it in the desired direction, we first need to rotate it so that it points up and going along the starting edge of the slice. Then it needs to rotate clockwise by half a slice.

In order to get the axis to point up, we need to rotate it by -90deg. The minus sign is due to the fact that positive values follow a clockwise direction and we’re going the other way.

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

Next, we need to rotate it by half a slice.

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

But how much is half a slice?

Well, we already know what percentage of the pie this slice represents: it’s our custom property, --p. If we divide that value by 100 and then multiply it by 360deg (or 1turn, it doesn’t matter what unit is used), we get the central angle of our dark slice.

After the -90deg rotation, we need to rotate :after by half this central angle in the clockwise (positive) direction.

This means we apply the following transform chain:

translate(-50%, -50%) rotate(calc(.5*var(--p)/100*1turn - 90deg)) translate(.25*$d);

The last translation is by a quarter of $d, which is the wrapper width and gives us the .pie diameter as well. (Since the content-box of .pie is a 0x0 box, it has no border and both its left and right padding are 50% of its wrapper parent width.) The .pie radius is half its diameter, meaning that half the radius is a quarter of the diameter ($d).

Now the value label is positioned where we want it to be:

Screenshot. Shows the chart now being perfectly round like a pie, with the percentage as text positioned right in the middle of the slice of pie it represents. It is however rotated such that its x axis is along the radial line splitting this slice into two equal halves.
Positioning the value label in the middle of the slice (live demo, Blink browsers only, transition needs flag).

However, there’s still one problem: we don’t want it to be rotated, as that can look really awkward and neck-bending at certain angles. In order to fix this, we revert the rotation at the end. To make things easier for ourselves, we store the rotation angle in a CSS variable that we call --a:

--a: calc(.5*var(--p)/100*1turn - 90deg);
transform: 
  translate(-50%, -50%) 
  rotate(var(--a)) 
  translate(.25*$d) 
  rotate(calc(-1*var(--a)));

Much better!

Screenshot. Shows the chart now being perfectly round like a pie, with the percentage as text positioned horizontally right in the middle of the slice of pie it represents.
Positioning the value label in the middle of the slice, now horizontal (live demo, Blink browsers only, transition needs flag).

Layout

We want to have the whole assembly in the middle of the screen, so we solve this with a neat little grid trick:

body {
  display: grid;
  place-items: center center;
  margin: 0;
  min-height: 100vh
}

Alright, this puts the entire .wrap element in the middle:

Screenshot. Shows the whole assembly in the middle of the page.
Positioning the whole assembly in the middle (live demo, Blink browsers only, transition needs flag).

The next step is to place the pie chart above the radio buttons. We do this with a flexbox layout on the .wrap element:

.wrap {
  display: flex;
  flex-wrap: wrap-reverse;
  justify-content: center;
  width: $d;
}
Screenshot. Shows the whole assembly in the middle of the page, with the pie chart now above the radio buttons.
Placing the pie chart above the radio buttons (live demo, Blink browsers only, transition needs flag).

Styling the radio buttons

…or more accurately, we’re styling the radio button labels because the first thing that we do is hide the radio inputs:

[type='radio'] {
  position: absolute;
  left: -100vw;
}
Screenshot. Shows the whole assembly in the middle of the page, with the pie chart above the radio button labels. The radio buttons are now hidden and it's difficult to distinguish the labels from one another.
After hiding the radio buttons (live demo, Blink browsers only, transition needs flag).

Since this leaves us with some very ugly labels that are very hard to distinguish from one another, let’s give each one some margin and padding so they don’t look so crammed together, plus backgrounds so that their clickable areas are clearly highlighted. We can even add box and text shadows for some 3D effects. And, of course, we can create a separate case for when their corresponding inputs are :checked.

$c: #ecf081 #b3cc57;

[type='radio'] {
   /* same as before */

  + label {
    margin: 3em .5em .5em;
    padding: .25em .5em;
    border-radius: 5px;
    box-shadow: 1px 1px nth($c, 2);
    background: nth($c, 1);
    font-size: 1.25em;
    text-shadow: 1px 1px #fff;
    cursor: pointer;
  }
	
  &:checked {
    + label {
      box-shadow: inset -1px -1px nth($c, 1);
      background: nth($c, 2);
      color: #fff;
      text-shadow: 1px 1px #000;
    }
  }
}

We’ve also blown up the font-size a bit and set a border-radius to smooth out the corners:

Screenshot. Shows the whole assembly in the middle of the page, with the pie chart above the radio button labels. The radio buttons are now hidden, but the labels have now been styled to be clearly distinguishable from one another.
After styling the radio button labels (live demo, Blink browsers only, transition needs flag).

Final prettifying touches

Let’s set a background on the body, tweak the font of the whole thing and add a transition for the radio labels:

Screenshot. Shows the final result with the pie chart above the radio button labels, styled to be clearly distinguishable from one another and a sans-serif font and vivid background.
The final pie chart result (live demo, Blink browsers only, transition needs flag).

Graceful degradation

While our demo now looks good in Blink browsers, it looks awful in all other browsers… and that’s most browsers!

First off, let’s put our work inside a @supports block that checks for native conic-gradient() support so the browsers that support it will render the pie chart. This includes our conic-gradient(), the padding that gives the pie equal horizontal and vertical dimensions, the border-radius that makes the pie circular, and the transform chain that positions the value label in the middle of the pie slice.

.pie {	
  @supports (background: conic-gradient(tan, red)) {
    padding: 50%;
    border-radius: 50%;
    background: conic-gradient(var(--stop-list));
    --a: calc(.5*var(--p)/100*1turn - 90deg);
    --pos: rotate(var(--a)) 
           translate(#{.25*$d}) 
           rotate(calc(-1*var(--a)));
    }
  }
}

Now, let’s construct a bar chart as a fallback for all other browsers using linear-gradient(). We want our bar to stretch across the .wrap element so that the horizontal padding is still 50%, but vertically as a narrow bar. We still want the chart to be tall enough to fit the value label. This means we go for a smaller vertical padding. We also decrease the border-radius, since 50% would give us an ellipse and what we need is a rectangle with slightly rounded corners.

The fallback also replaces conic-gradient() with a left-to-right linear-gradient(). Since both the linear-gradient() creating the fallback bar chart and the conic-gradient() creating the pie chart use the same stop list, we can store it in a CSS variable (--stop-list) so that we don’t even have it repeated in the compiled CSS.

Finally, we want the value label in the middle of the bar for the fallback since we don’t have pie slices anymore. This means we store all the post-centering positioning into a CSS variable (--pos) whose value is nothing in the no conic-gradient() support case and the previous transform chain otherwise:

.pie {
  padding: 1.5em 50%;
  border-radius: 5px;
  --stop-list: #ab3e5b calc(var(--p)*1%), #ef746f 0%;
  background: linear-gradient(90deg, var(--stop-list));
  /* same as before */
	
  &:after {
    transform: translate(-50%, -50%) var(--pos, #{unquote(' ')});
    /* same as before */
  }
	
  @supports (background: conic-gradient(tan, red)) {
    padding: 50%;
    border-radius: 50%;
    background: conic-gradient(var(--stop-list));
    --a: calc(.5*var(--p)/100*1turn - 90deg);
    --pos: rotate(var(--a)) 
           translate(#{.25*$d}) 
           rotate(calc(-1*var(--a)));
    }
  }
}

We also switch to using a flexbox layout on the body (since, as clever as it may be, the grid one is messed up in Edge).

body {
  display: flex;
  align-items: center;
  justify-content: center;
  /* same as before */
}

This all gives us a bar chart fallback for the browsers not supporting conic-gradient().

Screenshot. Shows the fallback for the final result in browsers not supporting conic-gradient().
The fallback for the final pie chart result (live demo).

Responsifying it all

The one problem we still have is that, if the viewport is narrower than the pie diameter, things don’t look so good anymore.

CSS variables and media queries to the rescue!

We set the diameter to a CSS variable (--d) that gets used to set the pie dimensions and the position of the value label in the middle of our slice.

.wrap {
  --d: #{$d};
  width: var(--d);
  /* same as before */

  @media (max-width: $d) { --d: 95vw }
}

Below certain viewport widths, we also decrease the font-size, the margin for our <label> elements, and we don’t position the value label in the middle of the dark pie slice anymore, but rather in the middle of the pie itself:

.wrap {
 /* same as before */

@media (max-width: 265px) { font-size: .75em; }
}

[type='radio'] {
   /* same as before */

  + label {
    /* same as before */
    
    @media (max-width: 195px) { margin-top: .25em; }
  }
}

.pie{
   /* same as before */

  @media (max-width: 160px) { --pos: #{unquote(' ')} } 
}

This gives us our final result: a responsive pie chart in browsers supporting conic-gradient() natively. And, even though that’s sadly just Blink browsers for now, we have a solid fallback that renders a responsive bar chart for all other browsers. We also animate between values—this is even more limited at this point, to just Blink browsers with the Experimental Web Platform features flag enabled.

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

Bonus: radial progress!

We can also apply this concept to build a radial progress indicator like the one below (inspired by this Pen):

Animated gif. Shows how the final radial progress behaves and its fallback. We have a value label inside/ over our radial/ linear progress and a button below this progress. Clicking the button generates a new random percentage to indicate the progress. When changing the percentage value, the radial progress animates from its previous state to the current one.
The radial progress and its fallback.

The technique is pretty much the same, except we leave the value label dead in the middle and set the conic-gradient() on the :before pseudo-element. This is because we use a mask to get rid of everything, except for a thin outer ring and, if we were to set the conic-gradient() and the mask on the element itself, then the mask would also hide the value label inside and we want that visible.

On clicking the <button>, a new value for our unitless percentage (--p) is randomly generated and we transition smoothly between values. Setting a fixed transition-duration would create a really slow transition between two close values (e.g. 47% to 49%) and a really fast transition when moving between values with a larger gap in between (e.g. 3% to 98%). We get around this by making the transition-duration depend on the absolute value of the difference between the previous value of --p and its newly generated value.

[id='out'] { /* radial progress element */
  transition: --p calc(var(--dp, 1)*1s) ease-out;
}
const _GEN = document.getElementById('gen'), 
             _OUT = document.getElementById('out');

_GEN.addEventListener('click', e => {
  let old_perc = ~~_OUT.style.getPropertyValue('--p'), 
      new_perc = Math.round(100*Math.random());
	
  _OUT.style.setProperty('--p', new_perc);
  _OUT.style.setProperty('--dp', .01*Math.abs(old_perc - new_perc));
  _OUT.setAttribute('aria-label', `Graphical representation of generated percentage: ${new_perc}% of 100%.`)
}, false);

This gives us a nice animated radial progress indicator for browsers supporting all the new and shiny features. We have a linear fallback for browsers not supporting conic-gradient() natively and no transition in the case of no Houdini support:

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


Simple Interactive Pie Chart with CSS Variables and Houdini Magic originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/simple-interactive-pie-chart-with-css-variables-and-houdini-magic/feed/ 8 273668
Chrome 69 https://css-tricks.com/chrome-69/ Wed, 08 Aug 2018 21:10:40 +0000 http://css-tricks.com/?p=275090 Chrome 69 is notable for us CSS developers:


Chrome 69 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Chrome 69 is notable for us CSS developers:

  • Conic gradients (i.e. background: conic-gradient(red, green, blue);): We’ve got lots of interesting articles about conic gradients here, and here’s some use cases and a polyfill from Lea Verou.
  • Logical box model properties: margin, padding, and border all get an upgrade for more use cases. Think of how we have margin-left now — the “left” part doesn’t make much sense when we switch directions. Now, we’ll have margin-inline-start for that. The full list is margin-{block,inline}-{start,end}, padding-{block,inline}-{start,end} and border-{block,inline}-{start,end}-{width,style,color}. Here’s Rachel Andrew with Understanding Logical Properties And Values.
  • Scroll snap points (i.e. scroll-snap-type: x mandatory;): What once required JavaScript intervention is now happily in CSS. We’ve been covering this for years. Goes a long way in making carousels way less complicated.
  • Environment variables (i.e. env(safe-area-inset-top);): Apple introduced “the notch” with the iPhone X and dropped some proprietary CSS for dealing with it. The community quickly stepped in and now we have env() for browsers to ship stuff like this.

I guess we can give this version number a well-deserved nice.

To Shared LinkPermalink on CSS-Tricks


Chrome 69 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
275090
1 Element CSS Rainbow Gradient Infinity https://css-tricks.com/1-element-css-rainbow-gradient-infinity/ https://css-tricks.com/1-element-css-rainbow-gradient-infinity/#comments Tue, 05 Jun 2018 14:04:31 +0000 http://css-tricks.com/?p=271321 I first got the idea to CSS something of the kind when I saw this gradient infinity logo by Infographic Paradise. The gradient doesn't look like in the original illustration, as I chose to generate the rainbow logically instead of using the Dev Tools picker or something like that, but other than that, I think I got pretty close—let's see how I did that!


1 Element CSS Rainbow Gradient Infinity originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I first got the idea to CSS something of the kind when I saw this gradient infinity logo by Infographic Paradise:

Original illustration. Shows a thick infinity symbol with a rainbow gradient filling its two loops and some highlights over this gradient.
The original gradient infinity.

After four hours and some twenty minutes, of which over four hours were spent on tweaking positioning, edges, and highlights… I finally had the result below:

Screenshot of my version. Shows a thick infinity symbol with a rainbow gradient filling its two loops and some highlights over this gradient.
My version of the rainbow gradient infinity.

The gradient doesn’t look like in the original illustration, as I chose to generate the rainbow logically instead of using the Dev Tools picker or something like that, but other than that, I think I got pretty close—so let’s see how I did it!

Markup

As you’ve probably already guessed from the title, the HTML is just one element:

<div class='∞'></div>

Styling

Deciding on the approach

The first idea that might come to mind when seeing the above would be using conic gradients as border images. Unfortunately, border-image and border-radius don’t play well together, as illustrated by the interactive demo below:

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

Whenever we set a border-image, border-radius just gets ignored, so using the two together is sadly not an option.

So the approach we take here is using conic-gradient() backgrounds and then getting rid of the part in the middle with the help of a mask. Let’s see how that works!

Creating the two ∞ halves

We first decide on an outer diameter.

$do: 12.5em;

We create the two halves of the infinity symbol using the ::before and ::after pseudo-elements of our .∞ element. In order to place these two pseudo-elements next to one another, we use a flex layout on their parent (the infinity element .∞). Each of these has both the width and the height equal to the outer diameter $do. We also round them with a border-radius of 50% and we give them a dummy background so we can see them.

.∞ {
  display: flex;
	
  &:before, &:after {
    width: $do; height: $do;
    border-radius: 50%;
    background: #000;
    content: '';
  }
}

We’ve also placed the .∞ element in the middle of its parent (the body in this case) both vertically and horizontally by using the flexbox approach.

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

How conic-gradient() works

In order to create the conic-gradient() backgrounds for the two halves, we must first understand how the conic-gradient() function works.

If inside the conic-gradient() function we have a list of stops without explicit positions, then the first is taken to be at 0% (or 0deg, same thing), the last is taken to be at 100% (or 360deg), while all those left are distributed evenly in the [0%, 100%] interval.

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

If we have just 2 stops, it’s simple. The first is at 0%, the second (and last) at 100% and there are no other stops in between.

If we have 3 stops, the first is at 0%, the last (third) at 100%, while the second is dead in the middle of the [0%, 100%] interval, at 50%.

If we have 4 stops, the first is at 0%, the last (fourth) at 100%, while the second and third split the [0%, 100%] interval into 3 equal intervals, being positioned at 33.(3)% and 66.(6)% respectively.

If we have 5 stops, the first is at 0%, the last (fifth) at 100%, while the second, third and fourth split the [0%, 100%] interval into 4 equal intervals being positioned at 25%, 50% and 75% respectively.

If we have 6 stops, the first is at 0%, the last (sixth) at 100%, while the second, third, fourth and fifth split the [0%, 100%] interval into 5 equal intervals being positioned at 20%, 40%, 60% and 80% respectively.

In general, if we have n stops, the first is at 0%, the last at 100%, while the ones in between split the [0%, 100%] interval into n-1 equal intervals spanning 100%/(n-1) each. If we give the stops 0-based indices, then each one of them is positioned at i*100%/(n-1).

For the first one, i is 0, which gives us 0*100%/(n-1) = 0%.

For the last (n-th) one, i is n-1, which gives us (n-1)*100%/(n-1) = 100%.

Here, we choose to use 9 stops which means we split the [0%, 100%] interval into 8 equal intervals.

Alright, but how do we get the stop list?

The hsl() stops

Well, for simplicity, we choose to generate it as a list of HSL values. We keep the saturation and the lightness fixed and we vary the hue. The hue is an angle value that goes from 0 to 360, as we can see here:

Hue scale from 0 to 360 in the HSB/HSL models.
Visual representation of the hue scale from 0 to 360 (saturation and lightness being kept constant).

With this in mind, we can construct a list of hsl() stops with fixed saturation and lightness and varying hue if we know the start hue $hue-start, the hue range $hue-range (this is the end hue minus the start hue) and the number of stops $num-stops.

Let’s say we keep the saturation and the lightness fixed at 85% and 57%, respectively (arbitrary values that can probably be tweaked for better results) and, for example, we might go from a start hue of 240 to an end hue of 300 and use 4 stops.

In order to generate this list of stops, we use a get-stops() function that takes these three things as arguments:

@function get-stops($hue-start, $hue-range, $num-stops) {}

We create the list of stops $list which is originally empty (and which we’ll return at the end after we populate it). We also compute the span of one of the equal intervals our stops split the full start to end interval into ($unit).

@function get-stops($hue-start, $hue-range, $num-stops) {
  $list: ();
  $unit: $hue-range/($num-stops - 1);
	
  /* populate the list of stops $list */
	
  @return $list
}

In order to populate our $list, we loop through the stops, compute the current hue, use the current hue to generate the hsl() value at that stop and then then add it to the list of stops:

@for $i from 0 to $num-stops {
  $hue-curr: $hue-start + $i*$unit;
  $list: $list, hsl($hue-curr, 85%, 57%);
}

We can now use the stop list this function returns for any kind of gradient, as it can be seen from the usage examples for this function shown in the interactive demo below (navigation works both by using the previous/next buttons on the sides as well as the arrow keys and the PgDn/ PgUp keys):

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

Note how, when our range passes one end of the [0, 360] interval, it continues from the other end. For example, when the start hue is 30 and the range is -210 (the fourth example), we can only go down to 0, so then we continue going down from 360.

Conic gradients for our two halves

Alright, but how do we determine the $hue-start and the $hue-range for our particular case?

In the original image, we draw a line in between the central points of the two halves of the loop and, starting from this line, going clockwise in both cases, we see where we start from and where we end up in the [0, 360] hue interval and what other hues we pass through.

Original illustration, annotated. We've marked out the central points of the two halves, connected them with a line and used this line as the start for going around each of the two halves in the clockwise direction.
We start from the line connecting the central points of the two halves and we go around them in the clockwise direction.

To simplify things, we consider we pass through the whole [0, 360] hue scale going along our infinity symbol. This means the range for each half is 180 (half of 360) in absolute value.

Hue scale from 0 to 360 in the HSB/HSL models, with saturation and lightness fixed at 100% and 50% respectively. Red corresponds to a hue of 0/ 360, yellow to a hue of 60, lime to a hue of 120, cyan to a hue of 180, blue to a hue of 240, magenta to a hue of 300.
Keywords to hue values correspondence for saturation and lightness fixed at 100% and 50% respectively.

On the left half, we start from something that looks like it’s in between some kind of cyan (hue 180) and some kind of lime (hue 120), so we take the start hue to be the average of the hues of these two (180 + 120)/2 = 150.

Original illustration, annotated. For the left half, our start hue is 150 (something between a kind of cyan and a kind of lime), we pass through yellows, which are around 60 in hue and end up at a kind of red, 180 away from the start, so at 330.
The plan for the left half.

We get to some kind of red, which is 180 away from the start value, so at 330, whether we subtract or add 180:

(150 - 180 + 360)%360 = (150 + 180 + 360)%360 = 330

So… do we go up or down? Well, we pass through yellows which are around 60 on the hue scale, so that’s going down from 150, not up. Going down means our range is negative (-180).

Original illustration, annotated. For the right half, our start hue is 150 (something between a kind of cyan and a kind of lime), we pass through blues, which are around 240 in hue and end up at a kind of red, 180 away from the start, so at 330.
The plan for the right half.

On the right half, we also start from the same hue in between cyan and lime (150) and we also end at the same kind of red (330), but this time we pass through blues, which are around 240, meaning we go up from our start hue of 150, so our range is positive in this case (180).

As far as the number of stops goes, 9 should suffice.

Now update our code using the values for the left half as the defaults for our function:

@function get-stops($hue-start: 150, $hue-range: -180, $num-stops: 9) {
  /* same as before */
}

.∞ {
  display: flex;
	
  &:before, &:after {
    /* same as before */
    background: conic-gradient(get-stops());
  }
  
  &:after {
    background: conic-gradient(get-stops(150, 180));
  }
}

And now our two discs have conic-gradient() backgrounds:

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

However, we don’t want these conic gradients to start from the top.

For the first disc, we want it to start from the right—that’s at 90° from the top in the clockwise (positive) direction. For the second disc, we want it to start from the left—that’s at 90° from the top in the other (negative) direction, which is equivalent to 270° from the top in the clockwise direction (because negative angles don’t appear to work from some reason).

The conic gradient for the first (left) half starts from the right, which means an offset of 90° in the clockwise (positive) direction from the top. The conic gradient for the second (right) half starts from the left, which means an offset of 270° in the clockwise (positive) direction (and of 90° in the negative direction) from the top.
Angular offsets from the top for our two halves.

Let’s modify our code to achieve this:

.∞ {
  display: flex;
	
  &:before, &:after {
    /* same as before */
    background: conic-gradient(from 90deg, get-stops());
  }
  
  &:after {
    background: conic-gradient(from 270deg, get-stops(150, 180));
  }
}

So far, so good!

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

From 🥧 to 🍩

The next step is to cut holes out of our two halves. We do this with a mask or, more precisely, with a radial-gradient() one. This cuts out Edge support for now, but since it’s something that’s in development, it’s probably going to be a cross-browser solution at some point in the not too far future.

Remember that CSS gradient masks are alpha masks by default (and only Firefox currently allows changing this via mask-mode), meaning that only the alpha channel matters. Overlaying the mask over our element makes every pixel of this element use the alpha channel of the corresponding pixel of the mask. If the mask pixel is completely transparent (its alpha value is 0), then so will the corresponding pixel of the element.

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

In order to create the mask, we compute the outer radius $ro (half the outer diameter $do) and the inner radius $ri (a fraction of the outer radius $ro).

$ro: .5*$do;
$ri: .52*$ro;
$m: radial-gradient(transparent $ri, red 0);

We then set the mask on our two halves:

.∞ {
  /* same as before */
	
  &:before, &:after {
    /* same as before */
    mask: $m;
  }
}

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

This looks perfect in Firefox, but the edges of radial gradients with abrupt transitions from one stop to another look ugly in Chrome and, consequently, so do the inner edges of our rings.

Screenshot. Shows a close-up of the inner edge of the right half in Chrome. These inner edges look jagged and ugly in Chrome.
Close-up of the inner edge of the right half in Chrome.

The fix here would be not to have an abrupt transition between stops, but spread it out over a small distance, let’s say half a pixel:

$m: radial-gradient(transparent calc(#{$ri} - .5px), red $ri);

We now got rid of the jagged edges in Chrome:

Screenshot. Shows a close-up of the inner edge of the right half in Chrome after spreading out the transition between stops over half a pixel. These inner edges now look blurry and smoother in Chrome.
Close-up of the inner edge of the right half in Chrome after spreading out the transition between stops over half a pixel.

The following step is to offset the two halves such that they actually form an infinity symbol. The visible circular strips both have the same width, the difference between the outer radius $ro and the inner radius $ri. This means we need to shift each laterally by half this difference $ri - $ri.

.∞ {
  /* same as before */
	
  &:before, &:after {
    /* same as before */
    margin: 0 (-.5*($ro - $ri));
  }
}

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

Intersecting halves

We’re getting closer, but we still have a very big problem here. We don’t want the right part of the loop to be completely over the left one. Instead, we want the top half of the right part to be over that of the left part and the bottom half of the left part to be over that of the right part.

So how do we achieve that?

We take a similar approach to that presented in an older article: using 3D!

In order to better understand how this works, consider the two card example below. When we rotate them around their x axes, they’re not in the plane of the screen anymore. A positive rotation brings the bottom forward and pushes the top back. A negative rotation brings the top forward and pushes the bottom back.

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

Note that the demo above doesn’t work in Edge.

So if we give the left one a positive rotation and the right one a negative rotation, then the top half of the right one appears in front of the top half of the left one and the other way around for the bottom halves.

Adding perspective makes what’s closer to our eyes appears bigger and what’s further away appears smaller and we use way smaller angles. Without it, we have the 3D plane intersection without the 3D appearance.

Note that both our halves need to be in the same 3D context, something that’s achieved by setting transform-style: preserve-3d on the .∞ element.

.∞ {
  /* same as before */
  transform-style: preserve-3d;
	
  &:before, &:after {
    /* same as before */
    transform: rotatex(1deg);
  }
  
  &:after {
    /* same as before */
    transform: rotatex(-1deg);
  }
}

And now we’re almost there, but not quite:

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

Fine tuning

We have a little reddish strip in the middle because the gradient ends and the intersection line don’t quite match:

Screenshot. Shows a close-up of the intersection of the two halves. In theory, the intersection line should match the start/ end line of the conic gradients, but this isn't the case in practice, so we're still seeing a strip of red along it, even though the red side should be behind the plane of the screen and not visible.
Close-up of small issue at the intersection of the two halves.

A pretty ugly, but efficient fix is to add a 1px translation before the rotation on the right part (the ::after pseudo-element):

.∞:after { transform: translate(1px) rotatex(-1deg) }

Much better!

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

This still isn’t perfect though. Since the inner edges of our two rings are a bit blurry, the transition in between them and the crisp outer ones looks a bit odd, so maybe we can do better there:

Screenshot. Shows a close-up of the area around the intersection of the two halves, where the crisp outer edges meet the blurry inner ones, which looks odd.
Close-up of continuity issue (crisp outer edges meeting blurry inner ones).

A quick fix here would be to add a radial-gradient() cover on each of the two halves. This cover is transparent white (rgba(#fff, 0)) for most of the unmasked part of the two halves and goes to solid white (rgba(#fff, 1)) along both their inner and outer edges such that we have nice continuity:

$gc: radial-gradient(#fff $ri, rgba(#fff, 0) calc(#{$ri} + 1px), 
  rgba(#fff, 0) calc(#{$ro} - 1px), #fff calc(#{$ro} - .5px));

.∞ {
  /* same as before */
	
  &:before, &:after {
    /* same as before */
    background: $gc, conic-gradient(from 90deg, get-stops());
  }
  
  &:after {
    /* same as before */
    background: $gc, conic-gradient(from 270deg, get-stops(150, 180));
  }
}

The benefit becomes more obvious once we add a dark background to the body:

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

Now it looks better even when zooming in:

Screenshot. Shows a close-up of the area around the intersection of the two halves, we don't have the same sharp contrast between inner and outer edges, not even when zooming in.
No more sharp contrast between inner and outer edges.

The final result

Finally, we add some prettifying touches by layering some more subtle radial gradient highlights over the two halves. This was the part that took me the most because it involved the least amount of logic and the most amount of trial and error. At this point, I just layered the original image underneath the .∞ element made the two halves semi-transparent and started adding gradients and tweaking them until they pretty much matched the highlights. And you can see when I got sick of it because that’s when the position values become rougher approximations with fewer decimals.

Another cool touch would be drop shadows on the whole thing using a filter on the body. Sadly, this breaks the 3D intersection effect in Firefox, which means we cannot add it there, too.

@supports not (-moz-transform: scale(2)) {
  filter: drop-shadow(.25em .25em .25em #000) 
          drop-shadow(.25em .25em .5em #000);
}

We now have the final static result!

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

Spicing it up with animation!

When I first shared this demo, I got asked about animating it. I initially thought this would be complicated, but then it hit me that, thanks to Houdini, it doesn’t have to be!

As mentioned in my previous article, we can animate in between stops, let’s say from a red to a blue. In our case, the saturation and lightness components of the hsl() values used to generate the rainbow gradient stay constant, all that changes is the hue.

For each and every stop, the hue goes from its initial value to its initial value plus 360, thus passing through the whole hue scale in the process. This is equivalent to keeping the initial hue constant and varying an offset. This offset --off is the custom property we animate.

Sadly, this means support is limited to Blink browsers with the Experimental Web Platform features flag enabled. While conic-gradient() is supported natively without the flag starting with Chrome 69+, Houdini isn’t, so no real gain so far in this particular case.

Screenshot showing the Experimental Web Platform features flag being enabled in Chrome.
The Experimental Web Platform features flag enabled in Chrome.

Still, let’s see how we put it all into code!

For starters, we modify the get-stops() function such that the current hue at any time is the initial hue of the current stop $hue-curr plus our offset --off:

$list: $list, hsl(calc(#{$hue-curr} + var(--off, 0)), 85%, 57%);

Next, we register this custom property:

CSS.registerProperty({
  name: '--off', 
  syntax: '<number>', 
  initialValue: 0, 
  inherits: true
})

Note that inherits is now required, even though it was optional in earlier versions of the spec.

And finally, we animate it to 360:

.∞ {
  /* same as before */
	
  &:before, &:after {
    /* same as before */
    animation: shift 2s linear infinite;
  }
}

@keyframes shift { to { --off: 360 } }

This gives us our animated gradient infinity!

Animated ∞ logo (live demo, Blink only with flag enabled).

That’s it! I hope you’ve enjoyed this dive into what can be done with CSS these days!


1 Element CSS Rainbow Gradient Infinity originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/1-element-css-rainbow-gradient-infinity/feed/ 4 271321
1 HTML Element + 5 CSS Properties = Magic! https://css-tricks.com/1-html-element-5-css-properties-magic/ https://css-tricks.com/1-html-element-5-css-properties-magic/#comments Mon, 16 Apr 2018 13:26:36 +0000 http://css-tricks.com/?p=269088 Let’s say I told you we can get the results below with just one HTML element and five CSS properties for each. No SVG, no images (save for the background on the root that’s there just to make clear that …


1 HTML Element + 5 CSS Properties = Magic! originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Let’s say I told you we can get the results below with just one HTML element and five CSS properties for each. No SVG, no images (save for the background on the root that’s there just to make clear that our one HTML element has some transparent parts), no JavaScript. What would you think that involves?

Screenshots. On the left, a screenshot of equal radial slices of a pie with transparent slices (gaps) in between them. The whole assembly has a top to bottom gradient (orange to purple). On the right, the XOR operation between what we have on the left and a bunch of concentric ripples. Again, the whole assembly has the same top to bottom gradient.
The desired results.

Well, this article is going to explain just how to do this and then also show how to make things fun by adding in some animation.

CSS-ing the Gradient Rays

The HTML is just one <div>.

<div class='rays'></div>

In the CSS, we need to set the dimensions of this element and we need to give it a background so that we can see it. We also make it circular using border-radius:

.rays {
  width: 80vmin; height: 80vmin;
  border-radius: 50%;
  background: linear-gradient(#b53, #f90);
}

And… we’ve already used up four out of five properties to get the result below:

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

So what’s the fifth? mask with a repeating-conic-gradient() value!

Let’s say we want to have 20 rays. This means we need to allocate $p: 100%/20 of the full circle for a ray and the gap after it.

Illustration. Shows how we slice the disc to divide it into equal rays and gaps.
Dividing the disc into rays and gaps (live).

Here we keep the gaps in between rays equal to the rays (so that’s .5*$p for either a ray or a space), but we can make either of them wider or narrower. We want an abrupt change after the ending stop position of the opaque part (the ray), so the starting stop position for the transparent part (the gap) should be equal to or smaller than it. So if the ending stop position for the ray is .5*$p, then the starting stop position for the gap can’t be bigger. However, it can be smaller and that helps us keep things simple because it means we can simply zero it.

SVG illustration. Connects the stop positions from the code to the actual corresponding points on the circle defining the repeating conic gradient.
How repeating-conic-gradient() works (live).
$nr: 20; // number of rays
$p: 100%/$nr; // percent of circle allocated to a ray and gap after

.rays {
  /* same as before */
  mask: repeating-conic-gradient(#000 0% .5*$p, transparent 0% $p);
}

Note that, unlike for linear and radial gradients, stop positions for conic gradients cannot be unitless. They need to be either percentages or angular values. This means using something like transparent 0 $p doesn’t work, we need transparent 0% $p (or 0deg instead of 0%, it doesn’t matter which we pick, it just can’t be unitless).

Screenshot of equal radial slices of a pie with transparent slices (gaps) in between them. The whole assembly has a top to bottom gradient (orange to purple).
Gradient rays (live demo, no Edge support).

There are a few things to note here when it comes to support:

  • Edge doesn’t support masking on HTML elements at this point, though this is listed as In Development and a flag for it (that doesn’t do anything for now) has already shown up in about:flags.
    Screenshot showing the about:flags page in Edge, with the 'Enable CSS Masking' flag highlighted.
    The Enable CSS Masking flag in Edge.
  • conic-gradient() is only supported natively by Blink browsers behind the Experimental Web Platform features flag (which can be enabled from chrome://flags or opera://flags). Support is coming to Safari as well, but, until that happens, Safari still relies on the polyfill, just like Firefox (or Edge when it also supports masking in the next version). Update: starting with Chrome 69, conic-gradient() isn’t behind a flag anymore – it now works in any up to date Blink browser, regardless of the flag being enabled or not.
    Screenshot showing the Experimental Web Platform Features flag being enabled in Chrome.
    The Experimental Web Platform features flag enabled in Chrome.
  • WebKit browsers still need the -webkit- prefix for mask properties on HTML elements. You’d think that’s no problem since we’re using the polyfill which relies on -prefix-free anyway, so, if we use the polyfill, we need to include -prefix-free before that anyway. Sadly, it’s a bit more complicated than that. That’s because -prefix-free works via feature detection, which fails in this case because all browsers do support mask unprefixed… on SVG elements! But we’re using mask on an HTML element here, so we’re in the situation where WebKit browsers need the -webkit- prefix, but -prefix-free won’t add it. So I guess that means we need to add it manually:
    $nr: 20; // number of rays
    $p: 100%/$nr; // percent of circle allocated to a ray and gap after
    $m: repeating-conic-gradient(#000 0% .5*$p, transparent 0% $p); // mask
    
    .rays {
      /* same as before */
      -webkit-mask: $m;
              mask: $m;
    }

    I guess we could also use Autoprefixer, even if we need to include -prefix-free anyway, but using both just for this feels a bit like using a shotgun to kill a fly.

Adding in Animation

One cool thing about conic-gradient() being supported natively in Blink browsers is that we can use CSS variables inside (we cannot do that when using the polyfill). And CSS variables can now also be animated in Blink browsers with a bit of Houdini magic (we need the Experimental Web Platform features flag to be enabled for that, even though we don’t need it for native conic-gradient() support anymore starting with Chrome 69+).

In order to prepare our code for the animation, we change our masking gradient so that it uses variable alpha values:

$m: repeating-conic-gradient(
      rgba(#000, var(--a)) 0% .5*$p, 
      rgba(#000, calc(1 - var(--a))) 0% $p);

We then register the alpha --a custom property:

CSS.registerProperty({
  name: '--a', 
  syntax: '<number>', 
  initialValue: 1, 
  inherits: true
})

Note that the spec now requires that inherits should be explicitly specified, even though it was optional before. So if any Houdini demos that don’t specify it are broken, this is at least one of the reasons why.

And finally, we add in an animation in the CSS:

.rays {
  /* same as before */
  animation: a 2s linear infinite alternate;
}

@keyframes a { to { --a: 0 } }

This gives us the following result:

Animated gif. We animate the alpha of the gradient stops, such that the rays go from fully opaque to fully transparent, effectively becoming gaps, while the opposite happens for the initial gaps, they go from fully transparent to fully opaque, thus becoming rays. At any moment, the alpha of either of them is  1 minus the alpha of the other, so they complement each other.
Ray alpha animation (live demo, only works in Blink browsers with the Experimental Web Platform features flag enabled).

Meh. Doesn’t look that great. We could however make things more interesting by using multiple alpha values:

$m: repeating-conic-gradient(
      rgba(#000, var(--a0)) 0%, rgba(#000, var(--a1)) .5*$p, 
      rgba(#000, var(--a2)) 0%, rgba(#000, var(--a3)) $p);

The next step is to register each of these custom properties:

for(let i = 0; i < 4; i++) {
  CSS.registerProperty({
    name: `--a${i}`, 
    syntax: '<number>', 
    initialValue: 1 - ~~(i/2), 
    inherits: true
  })
}

And finally, add the animations in the CSS:

.rays {
  /* same as before */
  animation: a 2s infinite alternate;
  animation-name: a0, a1, a2, a3;
  animation-timing-function: 
    /* easings from easings.net */
    cubic-bezier(.57, .05, .67, .19) /* easeInCubic */, 
    cubic-bezier(.21, .61, .35, 1); /* easeOutCubic */
}

@for $i from 0 to 4 {
  @keyframes a#{$i} { to { --a#{$i}: #{floor($i/2)} } }
}

Note that since we’re setting values to custom properties, we need to interpolate the floor() function.

Animated gif. This time, the alpha of each and every stop (start and end of ray, start and end of gap) is animated independently via its own CSS variable. The alphas at the start and end of the ray both go from 1 to 0, but using different timing functions. The alphas at the start and end of the gap both go from 0 to 1, but, again, using different timing functions.
Multiple ray alpha animations (live demo, only works in Blink browsers with the Experimental Web Platform features flag enabled).

It now looks a bit more interesting, but surely we can do better?

Let’s try using a CSS variable for the stop position between the ray and the gap:

$m: repeating-conic-gradient(#000 0% var(--p), transparent 0% $p);

We then register this variable:

CSS.registerProperty({
  name: '--p', 
  syntax: '<percentage>', 
  initialValue: '0%', 
  inherits: true
})

And we animate it from the CSS using a keyframe animation:

.rays {
  /* same as before */
  animation: p .5s linear infinite alternate
}

@keyframes p { to { --p: #{$p} } }

The result is more interesting in this case:

Animated gif. The stop position between the ray an the gap animates from 0 (when the ray is basically reduced to nothing) to the whole percentage $p allocated for a ray and the gap following it (which basically means we don't have a gap anymore) and then back to 0 again.
Alternating ray size animation (live demo, only works in Blink browsers with the Experimental Web Platform features flag enabled).

But we can still spice it up a bit more by flipping the whole thing horizontally in between every iteration, so that it’s always flipped for the reverse ones. This means not flipped when --p goes from 0% to $p and flipped when --p goes back from $p to 0%.

The way we flip an element horizontally is by applying a transform: scalex(-1) to it. Since we want this flip to be applied at the end of the first iteration and then removed at the end of the second (reverse) one, we apply it in a keyframe animation as well—in one with a steps() timing function and double the animation-duration.

 $t: .5s;

.rays {
  /* same as before */
  animation: p $t linear infinite alternate, 
    s 2*$t steps(1) infinite;
}

@keyframes p { to { --p: #{$p} } }

@keyframes s { 50% { transform: scalex(-1); } }

Now we finally have a result that actually looks pretty cool:

Animated gif. We have the same animation as before, plus a horizontal flip at the end of every iteration which creates the illusion of a circular sweep instead of just increasing and then decreasing rays, as the rays seems to now decrease from the start after they got to their maximum size incresing from the end.
Alternating ray size animation with horizontal flip in between iterations (live demo, only works in Blink browsers with the Experimental Web Platform features flag enabled).

CSS-ing Gradient Rays and Ripples

To get the rays and ripples result, we need to add a second gradient to the mask, this time a repeating-radial-gradient().

SVG illustration. Connects the stop positions from the code to the actual corresponding points on the circle defining the repeating radial gradient.
How repeating-radial-gradient() works (live).
$nr: 20;
$p: 100%/$nr;
$stop-list: #000 0% .5*$p, transparent 0% $p;
$m: repeating-conic-gradient($stop-list), 
    repeating-radial-gradient(closest-side, $stop-list);

.rays-ripples {
  /* same as before */
  mask: $m;
}

Sadly, using multiple stop positions only works in Blink browsers with the same Experimental Web Platform features flag enabled. And while the conic-gradient() polyfill covers this for the repeating-conic-gradient() part in browsers supporting CSS masking on HTML elements, but not supporting conic gradients natively (Firefox, Safari, Blink browsers without the flag enabled), nothing fixes the problem for the repeating-radial-gradient() part in these browsers.

This means we’re forced to have some repetition in our code:

$nr: 20;
$p: 100%/$nr;
$stop-list: #000, #000 .5*$p, transparent 0%, transparent $p;
$m: repeating-conic-gradient($stop-list), 
    repeating-radial-gradient(closest-side, $stop-list);

.rays-ripples {
  /* same as before */
  mask: $m;
}

We’re obviously getting closer, but we’re not quite there yet:

Screenshot. We have the same radial slices with equal gaps in between, and over them, a layer of ripples - concentric rings with gaps equal to their width in between them. The whole thing has a top to bottom gradient (orange to purple) with transparent parts where the gaps of the two layers intersect.
Intermediary result with the two mask layers (live demo, no Edge support).

To get the result we want, we need to use the mask-composite property and set it to exclude.

mask-composite is only supported in Firefox 53+ for now, though WebKit browsers have very good support (since Chrome 1.0 and Safari 4.0) for a similar non-standard property, -webkit-mask-composite, that helps us get the same result for a value of xor and Edge should join in when it finally supports CSS masking on HTML elements. Do note however that Edge is going to support both mask-composite and -webkit-mask-composite with the standard values, even though anyone using -webkit-mask-composite to target WebKit browsers is probably going to use it with the non-standard ones as it doesn’t even work in WebKit browsers otherwise.

$lyr1: repeating-conic-gradient($stop-list); 
$lyr0: repeating-radial-gradient(closest-side, $stop-list);

.xor {
  /* same as before */
  -webkit-mask: $lyr1, $lyr0;
  -webkit-mask-composite: xor;
          mask: $lyr1 exclude, $lyr0
}

Note that the non-standard -webkit-mask-composite cannot be used within the -webkit-mask shorthand in the same way we use the standard mask-composite within the mask shorthand for Firefox.

Screenshot. We have the same result as before, except now we have performed a XOR operation between the two layers (rays and ripples).
XOR rays and ripples (live demo, Firefox 53+ with standard mask-composite and Chrome 1.0+/ Safari 4.0+ with non-standard -webkit-mask-composite).

If you think it looks like the rays and the gaps between the rays are not equal in browsers not supporting conic-gradient() natively, you’re right. This is due to a polyfill issue.

Adding in Animation

Since the standard mask-composite only works in Firefox for now and Firefox doesn’t yet support conic-gradient() natively, we cannot put CSS variables inside the repeating-conic-gradient() (because Firefox still falls back on the polyfill for it and the polyfill doesn’t support CSS variable usage). But we can put them inside the repeating-radial-gradient() and even if we cannot animate them with CSS keyframe animations, we can do so with JavaScript!

Because we’re now putting CSS variables inside the repeating-radial-gradient(), but not inside the repeating-conic-gradient() (as we want better browser support and Firefox doesn’t support conic gradients natively, so it falls back on the polyfill, which doesn’t support CSS variable usage), we cannot use the same $stop-list for both gradient layers of our mask anymore.

But if we have to rewrite our mask without a common $stop-list anyway, we can take this opportunity to use different stop positions for the two gradients:

// for conic gradient
$nc: 20;
$pc: 100%/$nc;
// for radial gradient
$nr: 10;
$pr: 100%/$nr;

The CSS variable we animate is an alpha --a one, just like for the first animation in the rays case. We also introduce the --c0 and --c1 variables because here we cannot have multiple positions per stop and we want to avoid repetition as much as possible:

$lyr1: repeating-conic-gradient(#000 0% .5*$pc, transparent 0% $pc);
$lyr0: repeating-radial-gradient(closest-side, 
          var(--c0), var(--c0) .5*$pr, 
          var(--c1) 0, var(--c1) $pr);

body {
  --a: 0;
  /* layout, backgrounds and other irrelevant stuff */
}

.xor {
  /* same as before */
  --c0: #{rgba(#000, var(--a))};
  --c1: #{rgba(#000, calc(1 - var(--a)))};
  -webkit-mask: $lyr1, $lyr0;
  -webkit-mask-composite: xor;
          mask: $lyr1 exclude, $lyr0
}

The alpha variable --a is the one we animate back and forth (from 0 to 1 and then back to 0 again) with a little bit of vanilla JavaScript. We start by setting a total number of frames NF the animation happens over, a current frame index f and a current animation direction dir:

const NF = 50;

let f = 0, dir = 1;

Within an update() function, we update the current frame index f and then we set the current progress value (f/NF) to the current alpha --a. If f has reached either 0 of NF, we change the direction. Then the update() function gets called again on the next refresh.

(function update() {
  f += dir;
  
  document.body.style.setProperty('--a', (f/NF).toFixed(2));
	  
  if(!(f%NF)) dir *= -1;
  
  requestAnimationFrame(update)
})();

And that’s all for the JavaScript! We now have an animated result:

Animated gif. We animate the alpha of the gradient stops, such that the ripples go from fully opaque to fully transparent, effectively becoming gaps, while the opposite happens for the initial gaps, they go from fully transparent to fully opaque, thus becoming ripples. At any moment, the alpha of either of them is  1 minus the alpha of the other, so they complement each other. In this case, the animation is linear, the alpha changing at the same rate from start to finish.
Ripple alpha animation, linear (live demo, Firefox 53+ with standard mask-composite and Chrome 1.0+/ Safari 4.0+ with non-standard -webkit-mask-composite).

This is a linear animation, the alpha value --a being set to the progress f/NF. But we can change the timing function to something else, as explained in an earlier article I wrote on emulating CSS timing functions with JavaScript.

For example, if we want an ease-in kind of timing function, we set the alpha value to easeIn(f/NF) instead of just f/NF, where we have that easeIn() is:

function easeIn(k, e = 1.675) {
  return Math.pow(k, e)
}

The result when using an ease-in timing function can be seen in this Pen (Firefox 53+ with standard mask-composite and Chrome 1.0+/ Safari 4.0+ with non-standard -webkit-mask-composite). If you’re interested in how we got this function, it’s all explained in a lot of detail in the previously linked article on timing functions.

The exact same approach works for easeOut() or easeInOut():

function easeOut(k, e = 1.675) {
  return 1 - Math.pow(1 - k, e)
};

function easeInOut(k) {
  return .5*(Math.sin((k - .5)*Math.PI) + 1)
}

Since we’re using JavaScript anyway, we can make the whole thing interactive, so that the animation only happens on click/tap, for example.

In order to do so, we add a request ID variable (rID), which is initially null, but then takes the value returned by requestAnimationFrame() in the update() function. This enables us to stop the animation with a stopAni() function whenever we want to:

 /* same as before */

let rID = null;

function stopAni() {
  cancelAnimationFrame(rID);
  rID = null
};

function update() {
  /* same as before */
  
  if(!(f%NF)) {
    stopAni();
    return
  }
  
  rID = requestAnimationFrame(update)
};

On click, we stop any animation that may be running, reverse the animation direction dir and call the update() function:

addEventListener('click', e => {
  if(rID) stopAni();
  dir *= -1;
  update()
}, false);

Since we start with the current frame index f being 0, we want to go in the positive direction, towards NF on the first click. And since we’re reversing the direction on every click, it results that the initial value for the direction must be -1 now so that it gets reversed to +1 on the first click.

The result of all the above can be seen in this interactive Pen (working only in Firefox 53+ with standard mask-composite and Chrome 1.0+/ Safari 4.0+ with non-standard -webkit-mask-composite).

We could also use a different alpha variable for each stop, just like we did in the case of the rays:

$lyr1: repeating-conic-gradient(#000 0% .5*$pc, transparent 0% $pc);
$lyr0: repeating-radial-gradient(closest-side, 
           rgba(#000, var(--a0)), rgba(#000, var(--a1)) .5*$pr, 
           rgba(#000, var(--a2)) 0, rgba(#000, var(--a3)) $pr);

In the JavaScript, we have the ease-in and ease-out timing functions:

const TFN = {
  'ease-in': function(k, e = 1.675) {
    return Math.pow(k, e)
  }, 
  'ease-out': function(k, e = 1.675) {
    return 1 - Math.pow(1 - k, e)
  }
};

In the update() function, the only difference from the first animated demo is that we don’t change the value of just one CSS variable—we now have four to take care of: --a0, --a1, --a2, --a3. We do this within a loop, using the ease-in function for the ones at even indices and the ease-out function for the others. For the first two, the progress is given by f/NF, while for the last two, the progress is given by 1 - f/NF. Putting all of this into one formula, we have:

(function update() {
  f += dir;
  
  for(var i = 0; i < 4; i++) {
    let j = ~~(i/2);
		
    document.body.style.setProperty(
      `--a${i}`, 
      TFN[i%2 ? 'ease-out' : 'ease-in'](j + Math.pow(-1, j)*f/NF).toFixed(2)
    )
  }
	  
  if(!(f%NF)) dir *= -1;
  
  requestAnimationFrame(update)
})();

The result can be seen below:

Animated gif. This time, the alpha of each and every stop (start and end of ripple, start and end of gap) is animated independently via its own CSS variable. The alphas at the start and end of the ripple both go from 1 to 0, but using different timing functions. The alphas at the start and end of the gap both go from 0 to 1, but, again, using different timing functions.
Multiple ripple alpha animations (live demo, only works in Firefox 53+ with standard mask-composite and Chrome 1.0+/ Safari 4.0+ with non-standard -webkit-mask-composite).

Just like for conic gradients, we can also animate the stop position between the opaque and the transparent part of the masking radial gradient. To do so, we use a CSS variable --p for the progress of this stop position:

$lyr1: repeating-conic-gradient(#000 0% .5*$pc, transparent 0% $pc); 
$lyr0: repeating-radial-gradient(closest-side, 
           #000, #000 calc(var(--p)*#{$pr}), 
           transparent 0, transparent $pr);

The JavaScript is almost identical to that for the first alpha animation, except we don’t update an alpha --a variable, but a stop progress --p variable and we use an ease-in-out kind of function:

/* same as before */

function easeInOut(k) {
  return .5*(Math.sin((k - .5)*Math.PI) + 1)
};

(function update() {
  f += dir;
  
  document.body.style.setProperty('--p', easeInOut(f/NF).toFixed(2));
	  
  /* same as before */
})();
Animated gif. The stop position between the ripple an the gap animates from 0 (when the ripple is basically reduced to nothing) to the whole percentage $pr allocated for a ripple and the gap following it (which basically means we don't have a gap anymore) and then back to 0 again.
Alternating ripple size animation (live demo, only works in Firefox 53+ with standard mask-composite and Chrome 1.0+/ Safari 4.0+ with non-standard -webkit-mask-composite).

We can make the effect more interesting if we add a transparent strip before the opaque one and we also animate the progress of the stop position --p0 where we go from this transparent strip to the opaque one:

$lyr1: repeating-conic-gradient(#000 0% .5*$pc, transparent 0% $pc); 
$lyr0: repeating-radial-gradient(closest-side, 
           transparent, transparent calc(var(--p0)*#{$pr}), 
           #000, #000 calc(var(--p1)*#{$pr}), 
           transparent 0, transparent $pr);

In the JavaScript, we now need to animate two CSS variables: --p0 and --p1. We use an ease-in timing function for the first and an ease-out for the second one. We also don’t reverse the animation direction anymore:

const NF = 120, 
      TFN = {
        'ease-in': function(k, e = 1.675) {
          return Math.pow(k, e)
        }, 
        'ease-out': function(k, e = 1.675) {
          return 1 - Math.pow(1 - k, e)
        }
      };

let f = 0;

(function update() {
  f = (f + 1)%NF;
	
  for(var i = 0; i < 2; i++)
    document.body.style.setProperty(`--p${i}`, TFN[i ? 'ease-out' : 'ease-in'](f/NF);
  
  requestAnimationFrame(update)
})();

This gives us a pretty interesting result:

Animated gif. We now have one extra transparent circular strip before the opaque and transparent ones we previously had. Initially, both the start and end stop positions of this first strip and the following opaque one are 0, so they're both reduced to nothing and the whole space is occupied by the last transparent strip. The end stop positions of both strips then animate from 0 to the whole percentage $pr allocated for one repetition of our radial gradient, but with different timing functions. The end stop position of the first opaque strip animates slowly at first and faster towards the end (ease-in), while the end stop position of the opaque strip animates faster at first and slower towards the end (ease-out). This makes the opaque strip in the middle grow from nothing at first as its end stop position increases faster than that of the first transparent strip (which determines the start stop position of the opaque strip), then shrink back to nothing as its end stop position ends up being equal to $pr, just like the end stop position of the first transparent strip. The whole cycle then repeats itself.
Double ripple size animation (live demo, only works in Firefox 53+ with standard mask-composite and Chrome 1.0+/ Safari 4.0+ with non-standard -webkit-mask-composite).

1 HTML Element + 5 CSS Properties = Magic! originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/1-html-element-5-css-properties-magic/feed/ 6 269088
Using Conic Gradients and CSS Variables to Create a Doughnut Chart Output for a Range Input https://css-tricks.com/using-conic-gradients-css-variables-create-doughnut-chart-output-range-input/ https://css-tricks.com/using-conic-gradients-css-variables-create-doughnut-chart-output-range-input/#comments Fri, 02 Feb 2018 15:00:25 +0000 http://css-tricks.com/?p=265472 I recently came across this Pen and my first thought was that it could all be done with just three elements: a wrapper, a range input and an output. On the CSS side, this involves using a conic-gradient() with …


Using Conic Gradients and CSS Variables to Create a Doughnut Chart Output for a Range Input originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I recently came across this Pen and my first thought was that it could all be done with just three elements: a wrapper, a range input and an output. On the CSS side, this involves using a conic-gradient() with a stop set to a CSS variable.

Animated gif. Shows the result we want to get: a vertical slider from a minimum of 0 to a maximum of 100, which we can drag to update a doughnut chart alongside it.
The result we want to reproduce.

In mid 2015, Lea Verou unveiled a polyfill for conic-gradient() during a conference talk where she demoed how they can be used for creating pie charts. This polyfill is great for getting started to play with conic-gradient(), as it allows us to use them to build stuff that works across the board. Sadly, it doesn’t work with CSS variables and CSS variables have become a key component of writing efficient code these days.

The good news is that things have moved a bit over the past two years and a half. Chrome and, in general, browsers using Blink that expose flags (like Opera for example) now support conic-gradient() natively (yay!), which means it has become possible to experiment with CSS variables as conic-gradient() stops. All we need to do is have the Experimental Web Platform Features flag enabled in chrome://flags (or, if you’re using Opera, opera://flags):

Screenshot showing the Experimental Web Platform Features flag being enabled in Chrome.
The Experimental Web Platform Features flag enabled in Chrome.

Alright, now we can get started!

The Initial Structure

We start with a wrapper element and a range input:

<div class="wrap">
  <input id="r" type="range"/>
</div>

Note that we don’t have an output element there. This is because we need JavaScript to update the value of the output element anyway and we don’t want to see an ugly useless non-updating element if the JavaScript is disabled or fails for some reason. So we add this element via JavaScript and also, based on whether the current browser supports conic-gradient() or not, we add a class on the wrapper to signal that.

If our browser supports conic-gradient(), the wrapper gets a class of .full and we style the output into a chart. Otherwise, we just have a simple slider without a chart, the output being on the slider thumb.

Screenshots. The top screenshot shows the result in browsers supporting conic-gradient: a chart output alongside the vertical slider. Dragging the slider updates the chart and the value on the slider thumb. The bottom screenshot shows the fallback case (when there is no native support for conic-gradient): a regular horizontal slider that only shows the value on the thumb and has no chart output.
The result in browsers supporting conic-gradient() (top) and the fallback in browsers not supporting it (bottom).

Basic Styles

Before anything else, we want to show a nice-looking slider on the screen in all browsers.

We start with the most basic reset possible and set a background on the body:

$bg: #3d3d4a;

* { margin: 0 }

body { background: $bg }

The second step is to prepare the slider for styling in WebKit browsers by setting -webkit-appearance: none on it and on its thumb (because the track already has it set by default for some reason) and we make sure we level the field by explicitly setting the properties that are inconsistent across browsers like padding, background or font:

[type='range'] {
  &, &::-webkit-slider-thumb { -webkit-appearance: none }
  
  display: block;	
  padding: 0;
  background: transparent;
  font: inherit
}

If you need a refresher on how sliders and their components work in various browsers, check out my detailed article on understanding the range input.

We can now move on to the more interesting part. We decide upon the dimensions of the track and thumb and set these on the slider components via the corresponding mixins. We’ll also include some background values so that we have something visible on the screen as well as a border-radius to prettify things. For both components, we also reset the border to none so that we have consistent results across the board.

$k: .1;
$track-w: 25em;
$track-h: .02*$track-w;
$thumb-d: $k*$track-w;

@mixin track() {
  border: none;
  width: $track-w; height: $track-h;
  border-radius: .5*$track-h;
  background: #343440
}

@mixin thumb() {
  border: none;
  width: $thumb-d; height: $thumb-d;
  border-radius: 50%;
  background: #e6323e
}

[type='range'] {
  /* same styles as before */
  width: $track-w; height: $thumb-d;
	
  &::-webkit-slider-runnable-track { @include track }
  &::-moz-range-track { @include track }
  &::-ms-track { @include track }
	
  &::-webkit-slider-thumb {
    margin-top: .5*($track-h - $thumb-d);
    @include thumb
  }
  &::-moz-range-thumb { @include thumb }
  &::-ms-thumb {
    margin-top: 0;
    @include thumb
  }
}

We add a few more touches like setting a margin, an explicit width and a font on the wrapper:

.wrap {
  margin: 2em auto;
  width: $track-w;
  font: 2vmin trebuchet ms, arial, sans-serif
}

We don’t want to let this get too small or too big, so we limit the font-size:

.wrap {
  @media (max-width: 500px), (max-height: 500px) { font-size: 10px }
  @media (min-width: 1600px), (min-height: 1600px) { font-size: 32px }
}

And we now have a nice cross-browser slider:

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

The JavaScript

We start by getting the slider and the wrapper and creating the output element.

const _R = document.getElementById('r'), 
      _W = _R.parentNode, 
      _O = document.createElement('output');

We create a variable val where we store the current value of our range input:

let val = null;

Next, we have an update() function that checks whether the current slider value is equal to the one we have already stored. If that’s not the case, we update the JavaScript val variable, the text content of the output and the CSS variable --val on the wrapper.

function update() {
  let newval = +_R.value;

  if(val !== newval)
    _W.style.setProperty('--val', _O.value = val = newval)
};

Before we move further with the JavaScript, we set a conic-gradient() on the output from the CSS:

output {
  background: conic-gradient(#e64c65 calc(var(--val)*1%), #41a8ab 0%)
}

We put things in motion by calling the update() function, adding the output to the DOM as a child of the wrapper and then testing whether the computed background-image of the output is the conic-gradient() we have set or not (note that we need to add it to the DOM before we do this).

If the computed background-image is not "none" (as it is the case if we have no native conic-gradient() support), then we add a full class on the wrapper. We also connect the output to the range input via a for attribute.

Via event listeners, we ensure the update() function is called every time we move the slider thumb.

_O.setAttribute('for', _R.id);
update();
_W.appendChild(_O);

if(getComputedStyle(_O).backgroundImage !== 'none')
  _W.classList.add('full');

_R.addEventListener('input', update, false);
_R.addEventListener('change', update, false);

We now have a slider and an output (that shows its value on a variable conic-gradient() background if we’re viewing it in a browser with native conic-gradient() support). Still ugly at this stage, but it’s functional—the output updates when we drag the slider:

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

We’ve also given the output a light color value so that we can see it better and added a % at the end via the ::after pseudo-element. We’ve also hidden the tooltip (::-ms-tooltip) in Edge by setting its display to none.

The no chart case

This is the case when we don’t have conic-gradient() support so we don’t have a chart. The result we’re aiming for can be seen below:

Animated gif. Shows the result we want to get when conic-gradient is not supported natively: a horizontal slider from a minimum of 0 to a maximum of 100, which we can drag to update the value displayed on its thumb.
The result we want to reproduce.

Prettifying the Output

In this case, we absolutely position the output element, make it take the dimensions of the thumb and put its text right in the middle:

.wrap:not(.full) {
  position: relative;
		
  output {    
    position: absolute;
    
    /* ensure it starts from the top */
    top: 0;
    
    /* set dimensions */
    width: $thumb-d; height: $thumb-d
  }
}

/* we'll be using this for the chart case too */
output {
  /* place text in the middle */
  display: flex;
  align-items: center;
  justify-content: center;
}

If you need a refresher on how align-items and justify-content work, check out this comprehensive article on CSS alignment by Patrick Brosset.

The result can be seen in the following Pen, where we’ve also set an outline in order to clearly see the boundaries of the output:

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

This is starting to look like something, but our output isn’t moving with the slider thumb.

Making the Output Move

In order to fix this problem, let’s first remember how the motion of a slider thumb works. In Chrome, the border-box of the thumb moves within the limits of the track’s content-box, while in Firefox and Edge, the thumb’s border-box moves within the limits of the actual slider’s content-box.

While this inconsistency may cause problems in some situations, our use case here is a simple one. We don’t have margins, paddings or borders on the slider or on its components, so the three boxes (content-box, padding-box and border-box) coincide with both the slider itself and its track and thumb components. Furthermore, the width of the three boxes of the actual input coincides with the width of the three boxes of its track.

This means that when the slider value is at its minimum (which we haven’t set explicitly, so it’s the default 0), the left edge of the thumb’s boxes coincides with the left edge of the input (and with that of the track).

Also, when the slider value is at its maximum (again, not set explicitly, so it takes the default value 100), the right edge of the thumb’s boxes coincides with the right edge of the input (and with that of the track). This puts the left edge of the thumb one thumb width ($thumb-d) before (to the left of) the right edge of the slider (and of the track).

The following illustration shows this relative to the input width ($track-w)—this is shown to be 1. The thumb width ($thumb-d) is shown as a fraction k of the input width (since we’ve set it as $thumb-d: $k*$track-w).

Illustration. On the left, it shows the slider thumb at the minimum value. In this case, the left edge of the thumb coincides with the left edge of the input and track. On the right, it shows the slider thumb at the maximum value. In this case, the right edge of the thumb coincides with the right edge of the input and track. The width of the thumb is a fraction k of the width of the track in both cases.
The slider thumb at the minimum value and at the maximum value (live).

From here, we get that the left edge of the thumb has moved by an input width ($track-w) minus a thumb width (thumb-d) in between the minimum and the maximum.

In order to move the output the same way, we use a translation. In its initial position, our output is at the leftmost position of the thumb, the one occupied when the slider value is at its minimum, so the transform we use is translate(0). To move it into the position occupied by the thumb when the slider value is at its maximum, we need to translate it by $track-w - $thumb-d = $track-w*(1 - $k).

Illustration. Shows that the range of motion of the thumb is the track width minus the thumb width as, in going from the minimum value (initial position) to the maximum value (final position), the left edge of the thumb goes from coinciding with slider's left edge to being one thumb width away from the slider's right edge. Since the distance between the slider's left and right edge is one track width, it results that the range of motion is the track width minus the thumb width. Given that the thumb width is a fraction k of the track width, the range of motion relative to the track width is 1 - k.
The range of motion for the slider thumb and, consequently, the output (live).

Alright, but what about the values in between?

Well, remember that every time the slider value gets updated, we’re not only setting the new value to the output‘s text content, but also to a CSS variable --val on the wrapper. This CSS variable goes between 0 at the left end (when the slider value is its minimum, 0 in this case) and 100 at the other end (when the slider value is its maximum, 100 in this case).

So if we translate our output along the horizontal (x) axis by calc(var(--val)/100*#{$track-w - $thumb-d}), this moves it along with the thumb without us needing to do anything else:

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

Note how the above works if we click elsewhere on the track, but not if we try to drag the thumb. This is because the output now sits on top of the thumb and catches our clicks instead.

We fix this problem by setting pointer-events: none on the output.

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

In the demo above, we have also removed the ugly outline on the output element as we don’t need it anymore.

Now that we have a nice fallback for browsers that don’t support conic-gradient() natively, we can move on to building the result we want for those that do (Chrome/ Opera with flag enabled).

The Chart Case

Drawing the Desired Layout

Before we start writing any code, we need to clearly know what we’re trying to achieve. In order to do that, we do a layout sketch with dimensions relative to the track width ($track-w), which is also the width of the input and the edge of the wrapper’s content-box (wrapper padding not included).

This means the content-box of our wrapper is a square of edge 1 (relative to the track width), the input is a rectangle having one edge along and equal to an edge of the wrapper and the other one a fraction k of the same edge, while its thumb is a kxk square.

Illustration. Shows the wrapper as a 1x1 square. The slider is vertical, right aligned horizontally, stretching from top to bottom vertically. Horizontally, it takes up a fraction k of the wrapper's width. The chart is left-aligned horizontally (its left edge goes along the left edge of the wrapper) and middle aligned vertically. The gaps from the chart to the top and bottom edges of the wrapper, as well as to the slider on the right, are all a fraction k of the wrapper's edge length. This makes the chart a square of edge 1 - 2·k.
The desired layout in the chart case (live).

The chart is a square of edge 1 - 2·k, touching the wrapper edge opposite to the slider, a k gap away from the slider and in the middle along the other direction. Given that the edge of the wrapper is 1 and that of the chart is 1 - 2·k, it results we have k gaps between the edges of the wrapper and those of the chart along this direction as well.

Sizing Our Elements

The first step towards getting this layout is making the wrapper square and setting the dimensions of the output to (1 - 2*$k)*100%:

$k: .1;
$track-w: 25em;
$chart-d: (1 - 2*$k)*100%;

.wrap.full {
  width: $track-w;
	
  output {
    width: $chart-d; height: $chart-d
  }
}

The result can be seen below, where we’ve also added some outlines to see things better:

Screenshot. We have a square wrapper containing an input and an output. The input is at the very top, its width equal to that of the wrapper, while its height is a fraction k of the wrapper's edge. The output is a square whose edge is a fraction 1 - 2*k of the wrapper's edge. It's positioned along the left edge of the wrapper, immediately under the input.
The result in a first stage (live demo, only if we have native conic-gradient() support).

This is a good start, as the output is already in the exact spot we want it to be.

Making the Slider Vertical

The “official” way of doing this for WebKit browsers is by setting -webkit-appearance: vertical on the range input. However, this would break the custom styles as they require us to have -webkit-appearance set to none and we cannot have it set to two different values at the same time.

So the only convenient solution we have is to use a transform. As it is, we have the minimum of the slider at the left end of the wrapper and the maximum at its right end. What we want is to have the minimum at the bottom of the wrapper and the maximum at the top of the wrapper.

Illustration. On the left, it shows the slider in it initial position, before applying any transform, at the top of the wrapper. The minimum is at the left end and the maximum is at the right end. On the right, we have the slider in its final position, along the right edge of the wrapper. The minimum is at the bottom and the maximum is at the top. This looks like a rotation in the negative direction (since the positive one would be clockwise).
The initial position of the slider vs. the final position we want to bring it to (live).

This sounds like a 90° rotation in the negative direction (as the clockwise direction is the positive one) around the top right corner (which gives us a transform-origin that’s at 100% horizontally and 0% vertically).

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

That’s a good start, but now our slider is outside the wrapper boundary. In order to decide what’s the best next step to bring it inside in the desired position, we need to understand what this rotation has done. Not only has it rotated the actual input element, but it has also rotated its local system of coordinates. Now its x axis points up and its y axis points to the right.

So in order to bring it inside, along the right edge of the wrapper, we need to translate it by its own height in the negative direction of its y axis after the rotation. This means the final transform chain we apply is rotate(-90deg) translatey(-100%). (Remember that % values used in translate() functions are relative to the dimensions of the translated element itself.)

.wrap.full {
  input {
    transform-origin: 100% 0;
    transform: rotate(-90deg) translatey(-100%)
  }
}

This gives us the desired layout:

Screenshot. We have a square wrapper containing an input and an output. The input is has been rotated such that its bottom edge now coincides with the wrapper's right edge, while its height takes up a fraction k of the top and bottom edges. The output is a square whose edge is a fraction 1 - 2*k of the wrapper's edge. It's positioned along the left edge of the wrapper and in the middle vertically.
The result in a second stage (live demo, only if we have native conic-gradient() support).

Styling the Chart

Of course, the first step is to make it round with border-radius and tweak the color, font-size and font-weight properties.

.wrap.full {
  output {
    border-radius: 50%;
    color: #7a7a7a;
    font-size: 4.25em;
    font-weight: 700
  }
}

You may have noticed we’ve set the dimensions of the chart as (1 - 2*$k)*100% instead of (1 - 2*$k)*$track-w. This is because $track-w is an em value, meaning that the computed pixel equivalent depends on the font-size of the element that uses it.

However, we wanted to be able to increase the font-size here without having to tweak down an em-valued size. This is possible and not that complicated, but compared to just setting the dimensions as % values that don’t depend on the font-size, it’s still a bit of extra work.

Screenshot. We have the same square wrapper with an input and an output. The output is now round and its text is grey and bigger.
The result in a third stage (live demo, only if we have native conic-gradient() support).

From Pie 🥧 to Doughnut 🍩

The simplest way to emulate that hole in the middle where we have the text is to add another background layer on top of the conic-gradient() one. We could probably add some blend modes to do the trick, but that’s not really necessary unless we have an image background. For a solid background as we have here, a simple cover layer will do.

$p: 39%;
background: radial-gradient($bg $p, transparent $p + .5% /* avoid ugly edge */),
            conic-gradient(#e64c65 calc(var(--val)*1%), #41a8ab 0%);

Alright, this does it for the chart itself!

Screenshot. We have the same square wrapper with an input and an output. The output is not a pie anymore, but a doughnut, looking like it has a hole in the middle.
The result in a fourth stage (live demo, only if we have native conic-gradient() support).

Showing the Value on the Thumb

We do this with an absolutely positioned ::after pseudo-element on the wrapper. We give this pseudo-element the dimensions of the thumb and position it in the bottom right corner of the wrapper, precisely where the thumb is when the slider value is at its minimum.

.wrap.full {
  position: relative;
  
  &::after {
    position: absolute;
    right: 0; bottom: 0;
    width: $thumb-d; height: $thumb-d;
    content: '';
  }
}

We also give it an outline just so that we can see it.

Screenshot. We have the same elements as before, plus a square pseudo-element whose edge is a fraction k of the wrapper edge. This pseudo-element is positioned in the bottom right corner of the wrapper.
The result in a fifth stage (live demo, only if we have native conic-gradient() support).

Moving it along with the thumb is achieved exactly the same as in the no chart case, except this time the translation happens along the y axis in the negative direction (instead of along the x axis in the positive direction).

transform: translatey(calc(var(--val)/-100*#{$track-w - $thumb-d}))

In order to be able to drag the thumb underneath, we have to also set pointer-events: none on this pseudo-element. The result can be seen below—dragging the thumb also moves the wrapper’s ::before pseudo-element.

Animated gif. We have the same elements as before and we drag the thumb to show click events pass through the pseudo-element placed on top of it.
The result in a sixth stage (live demo, only if we have native conic-gradient() support).

Alright, but what we really want here is to display the current value using this pseudo-element. Setting its content property to var(--val) does nothing, as --val is a number value, not a string. If we were to set it as a string, we could use it as a value for content, but then we couldn’t use it for calc() anymore.

Fortunately, we can get around this problem with a neat trick using CSS counters:

counter-reset: val var(--val);
content: counter(val)'%';

Now the whole thing is functional, yay!

Screenshot. We have the same elements as before, only now we also have the current value displayed on the thumb.
The result in a seventh stage (live demo, only if we have native conic-gradient() support).

So let’s move on to prettifying and adding some nice touches. We put the text in the middle of the thumb, we make it white, we get rid of all the outlines and we set cursor: pointer on the input:

.wrap.full {
  &::after {
    line-height: $thumb-d;
    color: #fff;
    text-align: center
  }
}

[type='range'] {
  /* same as before */
  cursor: pointer
}

This gives us the following nice result:

Screenshot. We have the same elements as before, only now we also have the current value displayed on the thumb, dead in the middle and all the outlines are gone.
The final look for the chart case (live demo, only if we have native conic-gradient() support).

Eliminating Repetition

One thing that’s nagging me is the fact that we have a bunch of common styles on the output in the no chart case and on the .wrap:after in the chart case.

Screenshot collage. Shows the styles on the output in the no chart case versus the styles on the .wrap:after in the chart case, highlighting the common ones.
Styles on the output in the no chart case vs. styles on the .wrap:after in the chart case.

We can do something about this and that’s using a silent class we then extend:

%thumb-val {
  position: absolute;
  width: $thumb-d; height: $thumb-d;
  color: #fff;
  pointer-events: none
}

.wrap {
  &:not(.full) output {
    @extend %thumb-val;
    /* same other styles */
  }

  &:after {
    @extend %thumb-val;
    /* same other styles */
  }
}

Nice Focus Styles

Let’s say we don’t want to have that ugly outline on :focus, but we also want to clearly differentiate this state visually. So what could we do? Well, let’s say we make the thumb smaller and a bit desaturated when the input isn’t focused and that we also hide the text in this case.

Sounds like a cool idea…but, since we have no parent selector, we cannot trigger a property change on the ::after of the slider’s parent when the slider gets or loses focus. Ugh.

What we can do instead is use the output‘s other pseudo-element (the ::before) to display the value on the thumb. This doesn’t come without any of its own complications, which we’ll discuss in a moment, but it allows us to do something like this:

[type='range']:focus + output:before { /* focus styles */ }

The problem with taking this approach is that we’re blowing up the font on the output itself, but for its ::before we need it to be the same size and weight as on the wrapper.

We can solve this by setting a relative font size as a Sass variable $fsr and then use that value to blow up the font on the actual output and bring it back down to its previous size on the output:before pseudo-element.

$fsr: 4;

.wrap {
  color: $fg;
	
  &.full {
    output {
      font-size: $fsr*1em;
      
      &:before {
        /* same styles as we had on .wrap:after */
        font-size: 1em/$fsr;
        font-weight: 200;
      }
    }
  }
}

Other than that, we just move the CSS declarations we had on the .wrap:after on the output:before.

Screenshot collage. Shows the styles on the .wrap.full:after (left) and highlights how all of them can be found afterwards on the .wrap.full output:before (right), in addition to those bringing the font down to the size and weight it has on the wrapper.
Styles on the wrapper pseudo-element vs. on the output pseudo-element.

Alright, now we can move on to the final step of differentiating between the normal and the focused look.

We start by hiding the ugly :focus state outline and the value on the thumb when the slider isn’t focused:

%thumb-val {
  /* same styles as before */
  opacity: 0;
}

[type='range']:focus {
  outline: none;
	
  .wrap:not(.full) & + output, 
  .wrap.full & + output:before { opacity: 1 }
}
Animated gif. The value on the thumb isn't visible when the slider isn't focused, as illustrated in this gif by clicking in and out of the boundaries of the range input.
The value on the thumb is only visible when the slider gets focus (live demo, only if we have native conic-gradient() support).

Next, we set different styles for the normal and focused states of the slider thumb:

@mixin thumb() {
  /* same styles as before */
  transform: scale(.7);
  filter: saturate(.7)
}

@mixin thumb-focus() {
  transform: none;
  filter: none
}

[type='range']:focus {
  /* same as before */
  &::-webkit-slider-thumb { @include thumb-focus }
  &::-moz-range-thumb { @include thumb-focus }
  &::-ms-thumb { @include thumb-focus }
}
Animated gif. The thumb is scaled down and desaturated as long as the slider isn't focused. Clicking on the range input to make it receive focus brings both the scale and saturation factor to 1.
The thumb is scaled down and desaturated as long as the slider isn’t focused (live demo, only if we have native conic-gradient() support).

The last step is to add a transition between these states:

$t: .5s;

@mixin thumb() {
  /* same styles as before */
  transition: transform $t linear, filter $t
}

%thumb-val {
  /* same styles as before */
  transition: opacity $t ease-in-out
}
Animated gif. Clicking within/ outside the boundaries of the range input causes it to gain/ lose focus and we have a transition between the two states.
The demo with a transition between the normal and focused state (live demo, only if we have native conic-gradient() support).

What About Screen Readers?

Since screen readers these days read generated content, we’d have the % value read twice in this case. So we go around this by setting role='img' on our output and then putting the current value that we want to be read in an aria-label attribute:

let conic = false;

function update() {
  let newval = +_R.value;

  if(val !== newval) {
    _W.style.setProperty('--val', _O.value = val = newval);
    if(conic) _O.setAttribute('aria-label', `${val}%`)
  }
};

update();

_O.setAttribute('for', _R.id);
_W.appendChild(_O);

if(getComputedStyle(_O).backgroundImage !== 'none') {
  conic = true;
  _W.classList.add('full');
  _O.setAttribute('role', 'img');
  _O.setAttribute('aria-label', `${val}%`)
}

The final demo can be found below. Note that we only see the fallback if your browser has no native conic-gradient() support

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

Final Words

While the browser support for this is still poor, the situation will change. For now it’s just Blink browsers that expose flags, but Safari lists conic-gradient() as being in development, so things are already getting better.

If you’d like cross-browser support to become a reality sooner rather than later, you can contribute by voting for conic-gradient() implementation in Edge or by leaving a comment on this Firefox bug on why you think this is important or what use cases you have in mind. here are mine for inspiration.


Using Conic Gradients and CSS Variables to Create a Doughnut Chart Output for a Range Input originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/using-conic-gradients-css-variables-create-doughnut-chart-output-range-input/feed/ 3 265472
Conical Gradients in CSS https://css-tricks.com/conical-gradients-css/ https://css-tricks.com/conical-gradients-css/#comments Tue, 08 Oct 2013 13:08:34 +0000 http://css-tricks.com/?p=152254 When I create filters, shadows, transformations or gradient backgrounds in CSS, it feels amazing. Who would have thought CSS would come this far. No need to use images for any of that stuff. I can almost hear the browser telling …


Conical Gradients in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
When I create filters, shadows, transformations or gradient backgrounds in CSS, it feels amazing. Who would have thought CSS would come this far. No need to use images for any of that stuff. I can almost hear the browser telling me “don’t worry, I do it for you.”

Despite all this evolution, CSS still has limitations. For instance, background gradients can only be created in linear or radial styles, but not a conical shape.

In 2011, Lea Verou started a thread about implementing a native CSS conical gradient, creating a draft specification which has already been introduced to W3C’s official draft. However, we still have to wait for the W3C to formalize the feature and for the browsers to implement it, which might still take quite some time. In the meantime, I will show you how to simulate a conical gradient using CSS3 only.

Example of a conical gradient.

Beautiful, right? Let’s look at the code!

The Beginning

To reduce code duplication, I’m using Sass. One of the most interesting features of CSS preprocessors is the @mixin. A @mixin is a blend of function and include which, when called, returns its contents.

@mixin circle($size) {
  content: "";
  position: absolute;
  border-radius: 50%;
  width: $size;
  height: $size;
  left: calc(50% - #{$size/2});
  top: calc(50% - #{$size/2});
}

This @mixin is only used to set properties of shape and placement, creating a circle of absolute position and centered horizontally and vertically in relation to its parent.

The Magic!

By combining a @mixin circle with the clip property, we can get a semi-circle. Let’s start by creating a full circle by placing two semi-circles (with different colors) together. Can you imagine what is going to happen if we rotate one of the semi-circles? Magic!

$wheel: 15em;
.color {
  @include circle($wheel);
  background: red;
  clip: rect(0, $wheel, $wheel, #{$wheel/2});
  &:after {
    @include circle($wheel);
    background: blue;
    clip: rect(0, #{$wheel/2}, $wheel, 0);
    transform: rotate(45deg);
  }
}

The clip: rect (top, right, bottom, left) property restricts the visible area to a rectangular element, which causes only half of the red circle to be seen in the example above. The same principle is applied to the blue circle, the .color:after element. At this point we would have a full circle which is half red and half blue. However, the transform property causes the blue circle’s visible area to invade the red circle’s. See example.

Semi-circles with clip property

The Colorful Umbrella

Colorful umbrella

We already know how to perform this magic trick. Let’s create a 12 color umbrella:

<div class="wheel">
  <ul class="umbrella">
    <li class="color"></li>
    <li class="color"></li>
    <li class="color"></li>
    <li class="color"></li>
    <li class="color"></li>
    <li class="color"></li>
    <li class="color"></li>
    <li class="color"></li>
    <li class="color"></li>
    <li class="color"></li>
    <li class="color"></li>
    <li class="color"></li>
  </ul>
</div>

Since we want to create a full circle by combining semi-circles, from the seventh item (first item in the second half), we must reverse the clip:

.color, .color:nth-child(n+7):after {
  clip: rect(0, $wheel, $wheel, #{$wheel/2});
}
.color:after, .color:nth-child(n+7) {
  @include circle($wheel);
  clip: rect(0, #{$wheel/2}, $wheel, 0);
}

From the seventh item, the .color becomes a semi-circle with the left half of the circle and the .color:after elements pass to the right half of the circle.

We are almost done!! We need to change the colors and the angle of each element. Again, let’s abuse the power of Sass to generate 26193^42 lines of code in just over 10 ;)

$colors: (#9ED110, #50B517, #179067, #476EAF, #9f49ac, #CC42A2, #FF3BA7, #FF5800, #FF8100, #FEAC00, #FFCC00, #EDE604);
@for $i from 0 to length($colors) {
  .color:nth-child(#{1+$i}):after {
    background-color: nth($colors, $i+1);
    @if $i < 6 {
      transform: rotate(#{30*(1+$i)}deg);
      z-index: #{length($colors)-$i};
    } @else {
      transform: rotate(#{-30+(30*(1+$i))}deg);
    }
  }
}

First, we define the $colors array with the “12 colors of the rainbow” and then iterate over the list creating .color:nth-child(n):after selector with the background-color, rotate and z-index properties.

The rotate property has some important points. Its angle is defined according to the number of colors in the circle. In our case, there are 12 colors, so 360/12 = 30 is the rotation of each color. But from the seventh item on, the other half of the circle starts, remember? Thus, the process we just described stops at the seventh item. We will then start the very same process again, but this time, the rotation will take place on another direction.. That’s why there is an @else with rotate(#{-30+(30*($i+1))}deg) which subtracts 30 degrees from the elements of the second half of the circle.

If you are a good observer (and understood everything so far), you should have noticed that our our umbrella is actually a fan! Bazinga! So for the last color of the first half of the circle does not get on top of other colors, we need to reverse the index of these elements. For example, the z-index (6) = 1 and z-index (1) = 6.

A Little More Magic

Finally, we need to ease the transition between colors, after all, we don’t want an umbrella-range-rainbow, we want a conical gradient!

.umbrella {
  -webkit-filter: blur(1.7em);
}

.wheel {
  overflow: hidden;
  box-shadow: inset 0 0 0 3em rgba(0, 0, 0, 0.3);
}

The blur filter is responsible for mixing the colors. But, by applying the blur, the colors strain the limits of the circle. That’s why the overflow: hidden property was also added to the .wheel element. The inner box-shadow is used to darken the edges that were “washed out”. This is the end result:

See the Pen Conical gradient in pure CSS by Shankar Cabus (@shankarcabus) on CodePen

Demo

The conical gradient can be used in different ways to create different effects. But one of the most interesting applications is the Color Picker, as in the example below:

See the Pen Color Picker in Pure CSS by Shankar Cabus (@shankarcabus) on CodePen

Other Demos

Editor’s note: I’ve seen many other approaches to this over time. I’ll start dropping them here as a reference.

See the Pen css conical gradient by Kai Waddington (@waddington) on CodePen.

Another demo by Michael Wehner.


Conical Gradients in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/conical-gradients-css/feed/ 27 152254