Drawing Images with CSS Gradients

What I mean by “CSS images” is images that are created using only HTML elements and CSS. They look as if they were SVGs drawn in Adobe Illustrator but they were made right in the browser. Some techniques I’ve seen used are tinkering with border radii, box shadows, and sometimes clip-path. You can find a lot of great examples if you search daily css images” on CodePen. I drew some myself, including this Infinity Gauntlet, but in one element with only backgrounds and minimal use of other properties.

Let’s take a look at how you can create CSS images that way yourself.

The Method

Understanding the shorthand background syntax as well as how CSS gradients work is practically all you need to draw anything in one element. As a review, the arguments are as follows:

background: <'background-color'> || <image> || <position> [ / <size> ]? || <repeat> || <attachment> || <origin> || <clip>;

They can occur in any order except that there must be a / between the position and size. We must keep those two arguments in that order as well, or else we’ll get unexpected results. Not all of these need to be included, and for this purpose we won’t be using the color, repeat, attachment, origin, or clip arguments. This leaves us with image, size, and position. Since backgrounds repeat by default, however, we must place background-repeat: no-repeat; right under everything in background (if certain backgrounds ought to be repeat, we can use repeating-linear-gradient() and repeating-radial-gradient()). In that case, the skeleton CSS will be this:

.image { background: <image> <position> / <size>; background-repeat: no-repeat;
}

We can even use multiple sets of background arguments! Therefore, we can stack and separate them with commas like this:

.image { background: <image> <position> / <size>, <image> <position> / <size>, <image> <position> / <size>; background-repeat: no-repeat;
}

The structure above is the basis of how we’ll draw images—one line per shape. Keep in mind that the rendering order is the opposite of how absolutely- or fixed-position elements are ordered. The first one will show up on top instead of at the bottom. In other words, the circles (radial gradients) below would be rendered from bottom to top (blue on bottom, red on top).

.circles { background: radial-gradient(7em 7em at 35% 35%, red 50%, transparent 50%), radial-gradient(7em 7em at 50% 50%, gold 50%, transparent 50%), radial-gradient(7em 7em at 65% 65%, blue 50%, transparent 50%); background-repeat: no-repeat; width: 240px; height: 240px;
}

Drawing

We’ll use Sass (SCSS) to draw these images so we can make use of variables for a color palette. That will make the code shorter, easier to read and change darkened or lighter variants of colors. We could use variables in normal CSS instead and forget Sass, but due to Internet Explorer’s lack of support, let’s stick with Sass. To explain how this works, we’ll experiment with shapes using both linear and radial gradients.

Setting Up a Color Palette

Our palette will consist of RGB or HSL colors. I’ll explain later why to keep the colors in either of those formats. For this example, we’ll use RGB.

$r: rgb(255,0,0); // hsl(0,100%,50%)
$o: rgb(255,128,0); // hsl(32,100%,50%)

What I like to do to keep code short and easy to read is use a minimum of one letter to represent each color (e.g. $r for red). If using darker or lighter shades of one color, I add a d before the base letter or letters for dark or an l for light. I’d use $dr for dark red and $lr for light red. If there’s a need for more than two other shades, then I add a number at the end to indicate the shade level. For instance, $dr1 for dark red, $dr2 for a darker red, and $lr1 for light red, $lr2 for a lighter red. Such a palette would look like this (with dark first, normal next, and light last):

$dr1: rgb(224,0,0);
$dr2: rgb(192,0,0);
$r: rgb(255,0,0);
$lr1: rgb(255,48,48);
$lr2: rgb(255,92,92);

Setting the Scale and Canvas

We’ll use em units for the image dimensions so that the image can easily be resized proportionally. Since 1em is equal to the element’s font size, each unit of the image will be adjusted accordingly if changed. Let’s set a font size of 10px and set both the width and height to 24em. Units of 10px is the easiest to think in because if you mentally do the math, you instantly get 240 × 240px. Then just to see where the edges of the canvas are, we’ll use a 1px gray outline.

$r: rgb(255,0,0); // hsl(0,100%,50%)
$o: rgb(255,128,0); // hsl(32,100%,50%) .image { background-repeat: no-repeat; font-size: 10px; outline: 1px solid #aaa; width: 24em; height: 24em;
}

Be mindful about using smaller font sizes, however; browsers have a minimum font size setting (for accessiblity reasons). If you set a font size of 4px and the minimum is 6px, it’ll be forced at 6px.

Furthermore, you can enable responsiveness just by using calc() and viewport units. Perhaps we can use something like calc(10px + 2vmin) if desired, but let’s stick with 10px for now.

Drawing Shapes

The fun part begins here. To draw a square that is 8 × 8em in the center, we use a linear-gradient() with two same-colored stops.

.image { background: linear-gradient($r, $r) 50% 50% / 8em 8em; ...
}

To mold it into something more like a trapezoid, set an angle of 60deg. At the same time, let’s add $T for transparent to our palette and then place both the stops for $r and $T at 63% (right before the bottom-right corner gets cut off).

$T: transparent; .image { background: linear-gradient(60deg,$r 63%, $T 63%) 50% 50% / 8em 8em; ...
}

Setting both stops at the same value makes the slanted side as crisp as the others. If you look at it more closely though, it appears to be pixelated:

To correct this, we slightly adjust one of the stops (by 1% or roughly so) so that the edge is smooth enough. So, let’s change $r’s 63% to 62%.

This will be an issue with round edges as well while working with radial gradients, which we’ll see later. If you’re viewing this in a browser other than Safari, everything looks great even if transitioning to a non-transparent color instead (say orange). The problem with transitioning to transparent in Safari is that you’ll notice a bit of black lining at the slanted side.

This is because the transparent keyword in Safari is always black transparency, and we see some black as a result. I really wish Apple would fix this, but they never will. For the time being, let’s add a new $redT variable for red transparency under $r. Scrap the $T we used for transparent as we’ll no longer use it.

$rT: rgba(255,0,0,0); // hsla(0,100%,50%,0)

Then let’s replace transparent with $redT. This takes care of our Safari problem!

.image { background: linear-gradient(60deg,$r 62%, $rT 63%) 50% 50% / 8em 8em; ...
}

If you’ve been wondering why we weren’t using hex colors, Internet Explorer and Edge don’t support the #rgba and #rrggbbaa notations (yep, hex has had an alpha channel since late 2016 if you never knew), and we want this to work as cross-browser as possible. We also want to stay consistent with our choice of color format.

Now let’s move the shape vertically to 20% and draw an orange circle of the same dimensions. Also, add another variable for its transparent version. For the smooth edge, insert a 1% gap between the solid and transparent oranges.

$oT: rgba(255,128,0,0); // hsla(32,100%,50%,0) .image { background: linear-gradient(60deg,$r 62%, $rT 63%) 50% 20% / 8em 8em, radial-gradient(8em 8em at 50% 80%, $o 49%, $oT 50%); ...
}

To maintain consistency with our sizing, the second color stop should be at 50% instead of 100%.

Positioning Shapes

The way gradients are positioned is based on whether the unit is fixed or a percentage. Suppose we turn both of the gradients into squares and try to place them all the way across the div horizontally.

.image { background: linear-gradient($r, $r) 24em 20% / 8em 8em, linear-gradient($o, $o) 100% 80% / 8em 8em; ...
}

The red square ends up completely off canvas (outlined), and the right side of the orange square touches the other side. Using fixed units is like placing absolutely positioned elements or drawing shapes in HTML5 canvas. It’s true in that sense that the point of origin is at the top left. When using percent and a set background size, the div gets “fake padding” of half the background size. At the same time, the background’s point of origin is centered (not to be confused with background-origin, which regards box corners).

Now, if we turned these gradients into radial gradients as circles and applied the same x-positions 24em and 100%, both end up at the other side cut in half. This is because the point of origin is always in the center if we write the background like so:

.image { background: radial-gradient(8em 8em at 24em 20%, $r 49%, $rT 50%), radial-gradient(8em 8em at 100% 80%, $o 49%, $oT 50%); ...
}

If we rewrote the background so that the position and size are after the gradient and used 100% 100% at center, they’ll be positioned like the linear gradients. The red one ends up outside, and the orange one touches the right edge. The “fake padding” occurs once again with the orange.

.image { background: radial-gradient(100% 100% at center, $r 49%, $rT 50%) 24em 20% / 8em 8em, radial-gradient(100% 100% at center, $o 49%, $oT 50%) 100% 80% / 8em 8em; ...
}

There’s no single proper way to position shapes, but to place it like an absolutely or fixed positioned HTML element, use fixed units. If in need of a quick way to place a shape (using the position / size parameters) in the dead center, 50% is the best option as the shape’s origin will be its center. Use 100% if it should touch the very right side.

Sizing Shapes

Sizing in CSS backgrounds works as we’d expect, but it’s still influenced by the kind of unit used for position—fixed or percent. If we take our squares again and change their width to 10em, the red one expands to the right, and the orange one expands sideways.

.image { background: linear-gradient($r, $r) 12em 20% / 10em 8em, linear-gradient($o, $o) 50% 80% / 10em 8em; ...
}

If we used em units for the y-position, the shape will grow downwards or shrink upwards after changing height. If we use a percentage, then it will expand both vertical directions.

A moment ago, we looked at two ways to draw circles with radial gradients. The first way is to specify the width and height between the ( and at and then the position after that:

.image { background: radial-gradient(8em 8em at 50% 50%, $r 49%, $rT 50%); ...
}

The second way is to use 100% 100% in the center and then give the position and size:

.image { background: radial-gradient(100% 100% at 50% 50%, $r 49%, $rT 50%) 50% 50% / 8em 8em; ...
}

These methods both draw circles but will result in different outputs because:

  1. The first way occupies the whole div since there was no real background position or size.
  2. Giving a real position and size to the second sets it a bounding box. Consequently, it’ll behave just like a linear gradient shape.

Suppose we replaced $rT with $o. You’ll see that the orange will cover what was white or shapes under it (if we added any) for the first way. Using the second way, you’ll easily notice the bounding box revealed by the orange.

Additionally, the purpose of 100% 100% instead of using circle or ellipse is to allow the circle to occupy whole bounding box. It even gives us complete control over its dimensions. That way, it’ll remain the same if you change the 50% 50% position to something else. If using one of the two keywords, the edge of the circle stops only about 71% of the way when centered and becomes more distorted if its position is adjusted. For example, here’s what happens when we change the x-position to 0 for circle and ellipse:

In the long run, you can reimagine the syntax as radial-gradient(width height at x y) or radial-gradient(100% 100% at x-in-bounding-box y-in-bounding-box) x y / width height. If you are drawing just a circle or oval, you can simplify the code with the first way. If drawing part of a circle or part of a ring, then the second way comes into play. There will be many applications of that in the examples we’ll create next.

Examples

Ready to draw something for real now? We’ll walk through three examples step by step. The first two will be static—one with lots of half-circles and the other dealing with some rounded rectangles. The last example will be smaller but focused on animation.

Static Image

This parasol will be our first static image:

We’ll use a palette with red ($r and $rT), white ($w and $wT), orange ($o and $oT), and dark orange ($do and $doT).

$r: rgb(255,40,40);
$rT: rgba(255,40,40,0); $w: rgb(240,240,240);
$wT: rgba(240,240,240,0); $o: rgb(255,180,70);
$oT: rgba(255,180,70,0); $do: rgb(232,144,0);
$doT: rgba(232,144,0,0);

Let’s set up our drawing area of 30 × 29em.

.parasol { // background to go here background-repeat: no-repeat; font-size: 10px; outline: 1px solid #aaa; width: 30em; height: 29em;
}

Above the background-repeat, we’ll begin drawing the parts of the parasol. First, add the gradients that make up the pole (since neither overlap one another, the bottom-to-top order doesn’t matter at this point):

.parasol { background: // 1 radial-gradient(200% 200% at 100% 100%, $do 49%, $doT 50%) 14em 0 / 1em 1em, radial-gradient(200% 200% at 0% 100%, $o 49%, $oT 50%) 15em 0 / 1em 1em, // 2 linear-gradient(90deg, $do 50%, $o 50%) 14em 1em / 2em 25em, // 3 radial-gradient(100% 200% at 50% 0, $oT 0.95em, $o 1em, $o 1.95em, $do 2em, $do 2.95em, $doT 3em) 14em 26em / 6em 3em, // 4 radial-gradient(200% 200% at 100% 100%, $o 49%, $oT 50%) 18em 25em / 1em 1em, radial-gradient(200% 200% at 0% 100%, $do 49%, $doT 50%) 19em 25em / 1em 1em; ...
}
  1. To draw each side of the top of the pole, we used quarters of a circle that are 1 × 1em. To make them occupy their bounding boxes, we used circles that are twice the size (200% 200%) positioned at the bottom right and at the bottom left. We could also use keyword pairs like right bottom or left bottom, but it’s shorter to use the percent equivalents. Notice the 1% gaps between the stops to ensure smoothness.
  2. For the long part, we used a long rectangle with an abrupt dark orange-to-orange. There’s no need for a fractional tiny gap since we’re not working with a curve or slant.
  3. This part of the pole is a bit trickier to draw than the others because we have to maintain the 2em diameter. To draw this arc, we use a box of 6 × 3em so that there is a 2em space between the ends that are also 2em. Then we use a radial gradient at the center top where each stop occurs 1em each (and spread by 0.05em for smoothness).
  4. The last two are just like the first except they are positioned at the right end of the arc so that they seamlessly fit. Also, the colors swap places.

Then above the previous gradients, add the following from bottom to top to draw the top of the umbrella without the pointy ends:

.parasol { background: radial-gradient(100% 200% at 50% 100%, $r 50%, $rT 50.25%) 50% 1.5em / 9em 12em, radial-gradient(100% 200% at 50% 100%, $w 50%, $wT 50.25%) 50% 1.5em / 21em 12em, radial-gradient(100% 200% at 50% 100%, $r 50%, $rT 50.25%) 50% 1.5em / 30em 12em, ...
}

To draw the half circles that make up this part, we used a gradient size of 100% 200%, which makes each diameter fit into its background width but have twice the height and centered at the bottom. By ordering them bottom to top so that the largest is on bottom and smallest on top, we get the curves we want.

As our stack of gradients grows taller, it’ll become difficult after a while to identify which background or group of backgrounds corresponds to what part of the image. So to make it easier to pin them down, we can split them into groups lead by a comment describing what each group is for. Right now, we have split the stack to groups for the top of the parasol and the pole.

.parasol { background: /* top */ radial-gradient(100% 200% at 50% 100%, $r 50%, $rT 50.25%) 50% 1.5em / 9em 12em, radial-gradient(100% 200% at 50% 100%, $w 50%, $wT 50.25%) 50% 1.5em / 21em 12em, radial-gradient(100% 200% at 50% 100%, $r 50%, $rT 50.25%) 50% 1.5em / 30em 12em, /* pole */ radial-gradient(200% 200% at 100% 100%, $do 49%, $doT 50%) 14em 0 / 1em 1em, radial-gradient(200% 200% at 0% 100%, $o 49%, $oT 50%) 15em 0 / 1em 1em, linear-gradient(90deg, $do 50%, $o 50%) 14em 1em / 2em 25em, radial-gradient(100% 200% at 50% 0, $oT 0.95em, $o 1em, $o 1.95em, $do 2em, $do 2.95em, $doT 3em) 14em 26em / 6em 3em, radial-gradient(200% 200% at 100% 100%, $o 49%, $oT 50%) 18em 25em / 1em 1em, radial-gradient(200% 200% at 0% 100%, $do 49%, $doT 50%) 19em 25em / 1em 1em; ...
}

Then, in between the top and the pole, we’ll add the next chunk of backgrounds to render the pointy ends. To determine the widths of each segment, we must get the distance between each point where red and white meet. They all must add up to 30em.

Starting with the white and narrowest red half circles, we subtract the red’s width of 9em from the white’s width of 21em and divide the result by 2 to get the width of the two white segments (point “b” in the figure). So, the result would be 6em ( b = (21 – 9) / 2 = 6 ). Then the middle red segment would be 9em (21 – (6 + 6) = 9). What we have left now are the outer red segments (point “a” in the figure). Subtract the sum of what we have now from the width of the larger red half circle and divide that result by 2. That would be make the value of point a: (30em – (6 + 6 + 9)) / 2 = 4.5em.

.parasol { background: ... /* pointy ends */ radial-gradient() 0 13.5em / 4.5em 3em, radial-gradient() 4.5em 13.5em / 6em 3em, radial-gradient() 50% 13.5em / 9em 3em, radial-gradient() 19.5em 13.5em / 6em 3em, radial-gradient() 25.5em 13.5em / 4.5em 3em, ...
}

To draw the half circles similar to how we drew the top part, we start with the transparent counterpart of the color for each shape so that they resemble arc bridges. We’ll also add an extra 5% to each gradient width (not the background box width) so that each point formed by adjacent backgrounds won’t overly sharp and thin.

.parasol { background: ... /* pointy ends */ radial-gradient(105% 200% at 50% 100%, $rT 49%, $r 50%) 0 13.5em / 4.5em 3em, radial-gradient(105% 200% at 50% 100%, $wT 49%, $w 50%) 4.5em 13.5em / 6em 3em, radial-gradient(105% 200% at 50% 100%, $rT 49%, $r 50%) 50% 13.5em / 9em 3em, radial-gradient(105% 200% at 50% 100%, $wT 49%, $w 50%) 19.5em 13.5em / 6em 3em, radial-gradient(105% 200% at 50% 100%, $rT 49%, $r 50%) 25.5em 13.5em / 4.5em 3em, ...
}

Finally, you’ll no longer need that 1px solid #aaa outline. Our parasol is complete!

See the Pen Parasol by Jon Kantner (@jkantner) on CodePen.

Something With Rounded Rectangles

This next example will be an old iPhone model in which there are more details than the newer models. The thing about this one is the two rounded rectangles, which are the outside and middle of the home button.

The palette will consist of black ($bk and $bkT) for the home button edge, dark gray ($dg and$dgT) for the body, gray ($g and $gT) for the camera and speaker, light gray ($lg and $lgT) for the outside border, blue ($bl and $blT) for the camera lens, and a very dark purple ($p and $pT) for the screen.

$bk: rgb(10,10,10);
$bkT: rgba(10,10,10,0); $dg: rgb(50,50,50);
$dgT: rgba(50,50,50,0); $g: rgb(70,70,70);
$gT: rgba(70,70,70,0); $lg: rgb(120,120,120);
$lgT: rgba(120,120,120,0); $bl: rgb(20,20,120);
$blT: rgba(20,20,120,0); $p: rgb(25,20,25);
$pT: rgba(25,20,25,0);

Let’s set up our canvas at 20 × 40em and use the same font size we used for the parasol, 10px:

.iphone { // background goes here background-repeat: no-repeat; font-size: 10px; outline: 1px solid #aaa; width: 20em; height: 40em;
}

Before we begin drawing our first rounded rectangle, we need to think about our border radius, which will be 2em. Also, we want to leave some space at the left for the lock switch and volume buttons, which will be 0.25em. For this reason, the rectangle will be 19.75 × 40em. Considering the 2em border radius, we’ll need two linear gradients intersecting each other. One must have a width of 15.75em (19.75em – 2 × 2), and the other must have a height of 36em (40em – 2 × 2). Position the first 2.25em from the left and then the second 0.25em from the left and 2em from the top.

.iphone { background: /* body */ linear-gradient() 2.25em 0 / 15.75em 40em, linear-gradient() 0.25em 2em / 19.75em 36em; ...
}

Since the light gray border will be 0.5em thick, let’s make each gradient stop immediately switch from light gray ($lg) to dark gray ($dg) and vice versa at 0.5em and 0.5em before the end (40em – 0.5 = 39.5em for the first gradient, 19.75em – 0.5 = 19.25em for the second). Set an angle of 90deg for the second to make it go horizontal.

.iphone { background: /* body */ linear-gradient($lg 0.5em, $dg 0.5em, $dg 39.5em, $lg 39.5em) 2.25em 0 / 15.75em 40em, linear-gradient(90deg, $lg 0.5em, $dg 0.5em, $dg 19.25em, $lg 19.25em) 0.25em 2em / 19.75em 36em; ...
}

In each square corner, as indicated by the orange box in the figure, we’ll place the rounded edges. To create those shapes, we use radial gradients that are twice the size of their bounding boxes and located in each corner. Insert them above the body backgrounds.

.iphone { background: /* corners */ radial-gradient(200% 200% at 100% 100%, $dg 1.45em, $lg 1.5em, $lg 50%, $lgT 51%) 0.25em 0 / 2em 2em, radial-gradient(200% 200% at 0% 100%, $dg 1.45em, $lg 1.5em, $lg 50%, $lgT 51%) 18em 0 / 2em 2em, radial-gradient(200% 200% at 100% 0%, $dg 1.45em, $lg 1.5em, $lg 50%, $lgT 51%) 0.25em 38em / 2em 2em, radial-gradient(200% 200% at 0% 0%, $dg 1.45em, $lg 1.5em, $lg 50%, $lgT 51%) 18em 38em / 2em 2em, ...
}

To get the 0.5em-thick light gray ends, think about where the gradient starts and then do the math. Because the light gray is at the end, we subtract 0.5em from 2em to properly place first stop. For the smoothness, we take off a tiny bit from the first 1.5em and add 1% to the second 50% so that the round edges blend in with the flat edges.

Now if we enlarged the image by changing the font size to 40px or more, we notice seams between the rounded and flat edges (circled in orange):

Since they appear to be so tiny, we can easily patch them by going back to the body backgrounds and slightly altering the gradient stops as long as everything still looks right when changing the font size back to 10px.

.iphone { background: /* body */ linear-gradient($lg 0.5em, $dg 0.55em, $dg 39.5em, $lg 39.55em) 2.25em 0 / 15.75em 40em, linear-gradient(90deg, $lg 0.5em, $dg 0.55em, $dg 19.175em, $lg 19.25em) 0.25em 2em / 19.75em 36em; ...
}

Then in one linear gradient, we’ll add the lock switch and volume buttons to fill the 0.25em space on the left. If a 1px space is going to happen between the buttons and body, we can add a tiny bleed of 0.05em to the background width (making it 0.3em) so that it won’t stick out into the dark gray.

.iphone { background: /* volume buttons */ linear-gradient($lgT 5em, $lg 5em, $lg 7.5em, $lgT 7.5em, $lgT 9.5em, $lg 9.5em, $lg 11em, $lgT 11em, $lgT 13em, $lg 13em, $lg 14.5em, $lgT 14.5em) 0 0 / 0.3em 100%, ...
}

It looks like we could’ve used three light gray-to-light gray gradients, but since it was possible, only one was needed. It’s just lots of sudden transitions between the transparent and opaque light grays running downwards.

Next, let’s add the edge of the home button as well as the flat edges of the square inside it. Now the square inside home button will be 1.5 × 1.5em and follow basically the same procedure as the body: two intersecting linear gradients and radials to fill the corners. To place them horizontally in the center, calc() comes in handy. 50% + 0.125em will be the expression; if we centered only the phone body, there will be 0.125em spaces on each side. Therefore, we move it 0.125em more to the right. The same x-positioning will apply to the upper two backgrounds.

.iphone { background: /* home button */ linear-gradient() calc(50% + 0.125em) 36.5em / 0.5em 1.5em, linear-gradient() calc(50% + 0.125em) 37em / 1.5em 0.5em, radial-gradient(3em 3em at calc(50% + 0.125em) 37.25em, $bkT 1.25em, $bk 1.3em, $bk 49%, $bkT 50%), ...
}

Similar to how we shaded the linear gradient parts of the phone body, the stops will begin and end with light gray but with transparent in the middle. Notice we left 0.05em gaps between each gray-to-transparent transition (and vice versa). Just like the corners of the body, this is to ensure the blend between a round corner and flat end inside.

.iphone { background: /* home button */ linear-gradient($lg 0.15em, $lgT 0.2em, $lgT 1.35em, $lg 1.35em) calc(50% + 0.125em) 36.5em / 0.5em 1.5em, linear-gradient(90deg, $lg 0.15em, $lgT 0.2em, $lgT 1.3em, $lg 1.35em) calc(50% + 0.125em) 37em / 1.5em 0.5em, radial-gradient(3em 3em at calc(50% + 0.125em) 37.25em, $bkT 1.25em, $bk 1.3em, $bk 49%, $bkT 50%), ...
}

By the way, because the outlines will be so small as you’ve seen earlier, we can better see what we’re doing by increasing the font size to at least 20px. It’s like using the zoom tool in image editing software.

Now to get the corners of the gray square to exactly where they should be, let’s focus on the x-position first. We start with calc(50% + 0.125em), then we add or subtract the width of each piece, or should I say the square’s border radius. These backgrounds will go above the last three.

.iphone { background: /* home button */ radial-gradient(200% 200% at 100% 100%, $lgT 0.3em, $lg 0.35em, $lg 0.48em, $lgT 0.5em) calc(50% + 0.125em - 0.5em) 36.5em / 0.5em 0.5em, radial-gradient(200% 200% at 0% 100%, $lgT 0.3em, $lg 0.35em, $lg 0.48em, $lgT 0.5em) calc(50% + 0.125em + 0.5em) 36.5em / 0.5em 0.5em, radial-gradient(200% 200% at 100% 0%, $lgT 0.3em, $lg 0.35em, $lg 0.48em, $lgT 0.5em) calc(50% + 0.125em - 0.5em) 37.5em / 0.5em 0.5em, radial-gradient(200% 200% at 0% 0%, $lgT 0.3em, $lg 0.35em, $lg 0.48em, $lgT 0.5em) calc(50% + 0.125em + 0.5em) 37.5em / 0.5em 0.5em, ...
}

Then the screen will be a 17.25 × 30em rectangle. Just like parts of the home button, we’ll horizontally center it using calc(50% + 0.125em). From the top, it’ll be 5em.

.iphone { background: /* screen */ linear-gradient($p, $p) calc(50% + 0.125em) 5em / 17.25em 30em, ...
}

Lastly, we’ll add the camera and speaker. The camera is a straightforward 1 × 1 blue-to-gray radial with no fancy calculations. The pure-gray speaker though will be a bit more involved. It will be a 5 × 1em rectangle and have a 0.5em border radius. To draw that, we first draw a rectangle with the first 4ems of the width and center it with calc(50% + 0.125em). Then we use 0.5 × 1em half circles in which the gradient positions are 100% 50% and 0% 50%. The best way to position these left and right of the rectangle is to use some new calc() expressions. For the left, we’ll subtract half the rectangle width and half the half circle width from the body center (50% + 0.125em - 2em - 0.25em). The right will follow the same pattern but with addition, so 50% + 0.125em + 2em + 0.25em.

.iphone { background: /* camera */ radial-gradient(1em 1em at 6.25em 2.5em, $bl 0.2em, $g 0.21em, $g 49%, $gT 50%), /* speaker */ radial-gradient(200% 100% at 100% 50%, $g 49%, $gT 50%) calc(50% + 0.125em - 2em - 0.25em) 2em / 0.5em 1em, radial-gradient(200% 100% at 0% 50%, $g 49%, $gT 50%) calc(50% + 0.125em + 2em + 0.25em) 2em / 0.5em 1em, linear-gradient($g, $g) calc(50% + 0.125em) 2em / 4em 1em, ...
}

Remove the gray outline around the div, and the iPhone is complete!

See the Pen iPhone by Jon Kantner (@jkantner) on CodePen.

Animated Images

You might be thinking you could use background-position to animate these sorts of images, but you can only do so much. For instance, it’s impossible to animate the rotation of an individual background by itself. In fact, background-position animations don’t typically perform as well as transform animations, so I don’t recommend it.

To animate any part of an image any way we wish, we can let the :before or :after pseudo-elements be responsible for that part. If we need more selections, then we can revert to multiple child divs, yet not needing one for each little detail. For our animated image example, we’ll create this animated radar:

We draw the static part first, which is everything except the gray frame and rotating hand. Before that, let’s supply our palette (note: We won’t need a $dgnT for $dgn) and base code.

$gn: rgb(0,192,0);
$gnT: rgba(0,192,0,0);
$dgn: rgb(0,48,0);
$gy: rgb(128,128,128);
$gyT: rgba(128,128,128,0);
$bk: rgb(0,0,0);
$bkT: rgba(0,0,0,0); .radar { background-repeat: no-repeat; font-size: 10px; outline: 1px solid #aaa; width: 20em; height: 20em;
}

Since this image is going to be completely round, we can safely apply a border radius of 50%. Then, we can use a repeating radial gradient to draw the rings—about 1/3 way apart from each other.

.radar { background: /* rings */ repeating-radial-gradient($dgn, $dgn 2.96em, $gn 3em, $gn 3.26em, $dgn 3.3em); background-repeat: no-repeat; border-radius: 50%; ...
}

Also note the extra $dgn at the start. For repeating gradients to start, end, and loop as expected, we need to specify the starting color at 0 (or without 0).

Unlike the previous example, we’re not using calc() to center the lines because Internet Explorer will render the whole thing awkwardly when we use a pseudo-element later. To draw four 0.4em lines that intersect one other in the center, know that half of the line should be half the div at 10em. So then, we subtract and add half of 0.4 (0.4 / 2 = 0.2) on each side. In other words, the left of the green should be 9.8em, and the right should be 10.2em. For the 45deg diagonals though, we must multiply 10em by the square root of 2 to get their center (10 × √2 ≈ 14.14). It’s the length of the longest side of a 10em right triangle. As a result, the sides of each diagonal would be approximately at 13.94 and 14.34em.

.radar { background: /* lines */ linear-gradient($gnT 9.8em, $gn 9.8em, $gn 10.2em, $gnT 10.2em), linear-gradient(45deg,$gnT 13.94em, $gn 13.98em, $gn 14.3em, $gnT 14.34em), linear-gradient(90deg,$gnT 9.8em, $gn 9.8em, $gn 10.2em, $gnT 10.2em), linear-gradient(-45deg,$gnT 13.94em, $gn 13.98em, $gn 14.3em, $gnT 14.34em), ...
}

To prevent the pixelation of the diagonals, we left a tiny 0.04em gap between green and transparent green. Then, to create some lighting, add this transparent-to-black radial gradient:

.radar { background: /* lighting */ radial-gradient(100% 100%, $bkT, $bk 9.9em,$bkT 10em), ...
}

That completes the static part of the radar. Now we draw the gray frame and hand in another background stack under :before and add the animation. There’s a reason we didn’t include the frame here. Because the hand container should fit the whole div, we don’t want it to overlap the frame.

This pseudo-element shall fill the space, and to ensure it stays in there, let’s absolutely position it. We’ll use the same border radius so that it stays round while animated in Safari.

.radar { ... position: relative; &:before { background-repeat: no-repeat; border-radius: 50%; content: ""; position: absolute; width: 100%; height: 100%; }
}

Then, to draw the hand, we make it half the size of its container and keep it at the top left corner. Finally, on top of that, we draw the frame.

.radar { ... &:before { animation: scan 5s linear infinite; background: /* frame */ radial-gradient($gyT 9.20em, $gy 9.25em, $gy 10em, $gyT 10.05em), /* hand */ linear-gradient(45deg, $gnT 6em, $gn) 0 0 / 50% 50%; ... }
} @keyframes scan { from { transform: rotate(0); } to { transform: rotate(1turn); }
}

Now our little gadget is complete!

See the Pen Radar by Jon Kantner (@jkantner) on CodePen.

Benefits (Plus a Drawback)

This approach of drawing CSS images has several advantages. First, the HTML will be very lightweight compared to a rasterized image file. Second, it’s great for tackling images that are impossible to draw well without using experimental properties and APIs that might not be widely supported.

It’s not to say that this method is better than using a parent element nested with children for the shapes. There is a drawback though. You have to give up being able to highlight individual shapes using the browser dev tools. You’ll need to comment and uncomment a background to identify which it is. As long as you group and label each chunk of backgrounds, you can find that particular background faster.

Conclusion

In a nutshell, the method for drawing of CSS images we’ve covered in this post allows us to:

  1. Set up a palette made up of variables for the colors.
  2. Disable the background repeat, set a scale with font-size, and a canvas width and height in em units for the target element.
  3. Use a temporary outline to show the edges as we work.
  4. Draw each shape from bottom to top because backgrounds are rendered in that order. The background syntax for each shape follows image position / size (with or without the position and size).

There’s a lot of thinking outside the box going on as well as experimentation to get the desired result. The three examples we created were just enough to demonstrate the concept. We’ve looked at how we order each background, drawing parts of circles, rounded rectangles, and slightly adjusting gradient stops for smooth edges. To learn more, feel free to dissect and study other examples I’ve made in this CodePen collection!

The post Drawing Images with CSS Gradients appeared first on CSS-Tricks.

1 Element CSS Rainbow Gradient Infinity

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—let’s see how I did that!

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 haves, 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 eqial 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.

Addiing 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 for most of the unmasked part of the two halves and goes to solid white 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.

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;
})

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!

The post 1 Element CSS Rainbow Gradient Infinity appeared first on CSS-Tricks.

The State of Changing Gradients with CSS Transitions and Animations

Back in 2012, Internet Explorer 10 came out and, among other things, it finally supported CSS gradients and, in addition to that, the ability to animate them with just CSS! No other browser supported this at the time, but I was hopeful for the future.

Sadly, six years have passed and nothing has changed in this department. Edge supports animating gradients with CSS, just like IE 10 did back then, but no other browser has added support for this. And while animating background-size or background-position or the opacity or rotation of a pseudo element layered on top can take us a long way in terms of achieving cool effects, these workarounds are still limited.

There are effects we cannot reproduce without adding lots of extra elements or lots of extra gradients, such as “the blinds effect” seen below.

Animated GIF showing a recording of the opening and closing blinds effect. When the blinds are closed, we only see a grey background, when the blinds start to open, we start seeing vertical orange strips (the light coming in) that grow horizontally until the blinds are fully open, so we only see an orange background. After that, the blinds start to close, so the vertical orange strips start getting narrower until they're reduced to nothing when the blinds are fully closed and we only see a grey background again. The whole cycle then repeats itself.
The blinds effect (live demo, Edge/ IE 10+ only).

In Edge, getting the above effect is achieved with a keyframe animation:

html { background: linear-gradient(90deg, #f90 0%, #444 0) 50%/ 5em; animation: blinds 1s ease-in-out infinite alternate;
} @keyframes blinds { to { background-image: linear-gradient(90deg, #f90 100%, #444 0); }
}

If that seems WET, we can DRY it up with a touch of Sass:

@function blinds($open: 0) { @return linear-gradient(90deg, #f90 $open*100%, #444 0);
} html { background: blinds() 50%/ 5em; animation: blinds 1s ease-in-out infinite alternate;
} @keyframes blinds { to { background-image: blinds(1) } }

While we’ve made the code we write and what we’ll need to edit later a lot more maintainable, we still have repetition in the compiled CSS and we’re limited by the fact that we can only animate between stops with the same unit — while animating from 0% to 100% works just fine, trying to use 0 or 0px instead of 0% results in no animation happening anymore. Not to mention that Chrome and Firefox just flip from orange to grey with no stop position animation at all!

Fortunately, these days we have an even better option: CSS variables!

Right out of the box, CSS variables are not animatable, though we can get transition (but not animation!) effects if the property we use them for is animatable. For example, when used inside a transform function, we can transition the transform the property.

Let’s consider the example of a box that gets shifted and squished when a checkbox is checked. On this box, we set a transform that depends on a factor --f which is initially 1:

.box { /* basic styles like dimensions and background */ --f: 1; transform: translate(calc((1 - var(--f))*100vw)) scalex(var(--f));
}

When the checkbox is :checked, we change the value of the CSS variable --f to .5:

:checked ~ .box { --f: .5 }

Setting a transition on the .box makes it go smoothly from one state to the other:

.box { /* same styles as before */ transition: transform .3s ease-in;
}

Note that this doesn’t really work in the current version of Edge due to this bug.

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

However, CSS gradients are background images, which are only animatable in Edge and IE 10+. So, while we can make things easier for ourselves and reduce the amount of generated CSS for transitions (as seen in the code below), we’re still not making progress in terms of extending support.

.blinds { background: linear-gradient(90deg, #f90 var(--pos, 0%), #444 0) 50%/ 5em; transition: .3s ease-in-out; :checked ~ & { --pos: 100%; }
}
Animated gif. The blinds opening effect happens on checking an 'open blinds' checkbox, while unchecking it triggers the closing effect.
Open/close blinds on checking/unchecking the checkbox (live demo, Edge only).

Enter Houdini, which allows us to register custom properties and then animate them. Currently, this is only supported by Blink browsers behind the Experimental Web Platform features flag, but it’s still extending support a bit from Edge alone.

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

Going back to our example, we register the --pos custom property:

CSS.registerProperty({ name: '--pos', syntax: '<length-percentage>', initialValue: '0%'
});

Note that <length-percentage> means it accepts not only length and percentage values, but also calc() combinations of them. By contrast, <length> | <percentage> only accepts length and percentage values, but not calc() combinations of them.

However, doing this doesn’t make any difference in Chrome, even with the flag enabled, probably because, in the case of transitions, what’s being transitioned is the property whose value depends on the CSS variable and not the CSS variable itself. And since we generally can’t transition between two background images in Chrome in general, this fails as well.

It does work in Edge, but it worked in Edge even without registering the --pos variable because Edge allows us to transition between gradients in general.

What does work in Blink browsers with the flag enabled is having an animation instead of a transition.

html { background: linear-gradient(90deg, #f90 var(--pos, 0%), #444 0) 50%/ 5em; animation: blinds .85s ease-in-out infinite alternate;
} @keyframes blinds { to { --pos: 100%; } }

However, this is now not working in Edge anymore because, while Edge can animate between gradient backgrounds, it cannot do the same for custom properties.

So we need to take an alternative approach for Edge here. This is where @supports comes in handy, since all we have to do is check whether a -ms- prefixed property is supported.

@function grad($pos: 100%) { @return linear-gradient(90deg, #f90 $pos, #444 0);
} html{ /* same as before */ @supports (-ms-user-select: none) { background-image: grad(0%); animation-name: blinds-alt; }
} @keyframes blinds-alt { to { background-image: grad() } }

Stop positions aren’t the only thing we can animate this way. We can do the same thing for the gradient angle. The idea behind it is pretty much the same, except now our animation isn’t an alternating one anymore and we use an easeInOutBack kind of timing function.

@function grad($ang: 1turn) { @return linear-gradient($ang, #f90 50%, #444 0);
} html { background: grad(var(--ang, 0deg)); animation: rot 2s cubic-bezier(.68, -.57, .26, 1.65) infinite; @supports (-ms-user-select: none) { background-image: grad(0turn); animation-name: rot-alt; }
} @keyframes rot { to { --ang: 1turn; } } @keyframes rot-alt { to { background-image: grad(); } }

Remember that, just like in the case of stop positions, we can only animate between gradient angles expressed in the same unit in Edge, so calling our Sass function with grad(0deg) instead of grad(0turn) doesn’t work.

And, of course, the CSS variable we now use accepts angle values instead of lengths and percentages:

CSS.registerProperty({ name: '--ang', syntax: '<angle>', initialValue: '0deg'
});
Animated gif. Shows a top to bottom gradient with an abrupt change from grey to orange at 50%. The angle of this gradient is animated using a easeInOutBack timing function (which overshoots the end values at both ends).
Sweeping around (live demo, Blink browsers with flag and Edge only).

In a similar fashion, we can also animate radial gradients. And the really cool thing about the CSS variable approach is that it allows us to animate different components of the gradient differently, which is something that’s not possible when animating gradients as a whole the way Edge does (which is why the following demos don’t work as well in Edge).

Let’s say we have the following radial-gradient():

$p: 9%; html { --x: #{$p}; --y: #{$p}; background: radial-gradient(circle at var(--x) var(--y), #f90, #444 $p);
}

We register the --x and --y variables:

CSS.registerProperty({ name: '--x', syntax: '<length-percentage>', initialValue: '0%'
}); CSS.registerProperty({ name: '--y', syntax: '<length-percentage>', initialValue: '0%'
});

Then we add the animations:

html { /* same as before */ animation: a 0s ease-in-out -2.3s alternate infinite; animation-name: x, y; animation-duration: 4.1s, 2.9s;
} @keyframes x { to { --x: #{100% - $p} } }
@keyframes y { to { --y: #{100% - $p} } }

The result we get can be seen below:

Animated GIF. Shows a moving glowing orange light on a grey background. This is achieved by animating the coordinates of the central point of a radial gradient independently with the help of CSS variables and Houdini.
Moving light (live demo, Blink browsers with flag only).

We can use this technique of animating the different custom properties we use inside the gradient function to make the blinds in our initial example close the other way instead of going back. In order to do this, we introduce two more CSS variables, --c0 and --c1:

$c: #f90 #444; html { --c0: #{nth($c, 1)}; --c1: #{nth($c, 2)}; background: linear-gradient(90deg, var(--c0) var(--pos, 0%), var(--c1) 0) 50%/ 5em;
}

We register all these custom properties:

CSS.registerProperty({ name: '--pos', syntax: '<length-percentage>', initialValue: '0%'
}); CSS.registerProperty({ name: '--c0', syntax: '<color>', initialValue: 'red'
}); /* same for --c1 */

We use the same animation as before for the position of the first stop --pos and, in addition to this, we introduce two steps() animations for the other two variables, switching their values every time an iteration of the first animation (the one changing the value of --pos) is completed:

$t: 1s; html { /* same as before */ animation: a 0s infinite; animation-name: c0, pos, c1; animation-duration: 2*$t, $t; animation-timing-function: steps(1), ease-in-out;
} @keyframes pos { to { --pos: 100%; } } @keyframes c0 { 50% { --c0: #{nth($c, 2)} } }
@keyframes c1 { 50% { --c1: #{nth($c, 1)} } }

And we get the following result:

Animated GIF. Shows the blinds effect with the blinds closing the other way. Once the vertical orange strips (openings) have expanded horizontally such that they cover the whole background, they don't start contracting again. Instead, vertical grey orange strips start expanding from nothing until they cover the whole background.
Another version of the blinds animation (live demo, Blink browsers with flag only).

We can also apply this to a radial-gradient() (nothing but the background declaration changes):

background: radial-gradient(circle, var(--c0) var(--pos, 0%), var(--c1) 0);
Animated gif. We start with a grey background and we have an orange disc growing from nothing in the middle until it covers everything. Then we have a grey disc growing from nothing in the middle until it covers the entire background and we're back where we started from: a grey background.
Growing discs (live demo, Blink browsers with flag only).

The exact same tactic works for conic-gradient() as well:

background: conic-gradient(var(--c0) var(--pos, 0%), var(--c1) 0);
Animated gif. We start with a grey background and we have an orange pie slice (circular sector) growing from nothing to covering everything around the central point. Then we have a grey pie slice growing from nothing to covering everything around the central point and we're back where we started from: a grey background.
Growing slices (live demo, Blink browsers with flag only).

Repeating gradients are also an option creating a ripple-like effect in the radial case:

$p: 2em; html { /* same as before */ background: repeating-radial-gradient(circle, var(--c0) 0 var(--pos, 0px), var(--c1) 0 $p);
} @keyframes pos { 90%, 100% { --pos: #{$p} } }
Animated gif. We start with a grey background and we have concentric orange circles growing outwards from really thin until they meet and cover everything, so now it looks like we have an orange background. Then we have grey circles growing outwards from really thin until they cover the entire background and we're back where we started from: a grey background.
Ripples (live demo, Blink browsers with flag only).

And a helix/rays effect in the conic case:

$p: 5%; html { /* same as before */ background: repeating-conic-gradient( var(--c0) 0 var(--pos, 0%), var(--c1) 0 $p);
} @keyframes pos { 90%, 100% { --pos: #{$p} } }
Animated gif. We start with a grey background and we have orange rays growing clockwise from really thin until they meet and cover everything, so now it looks like we have an orange background. Then we have grey rays growing clockwise from really thin until they cover the entire background and we're back where we started from: a grey background.
Growing rays (live demo, Blink browsers with flag only).

We can also add another CSS variable to make things more interesting:

$n: 20; html { /* same as before */ background: radial-gradient(circle at var(--o, 50% 50%), var(--c0) var(--pos, 0%), var(--c1) 0); animation: a 0s infinite; animation-name: c0, o, pos, c1; animation-duration: 2*$t, $n*$t, $t; animation-timing-function: steps(1), steps(1), ease-in-out;
} @keyframes o { @for $i from 0 to $n { #{$i*100%/$n} { --o: #{random(100)*1%} #{random(100)*1%} } }
}

We need to register this variable for the whole thing to work:

CSS.registerProperty({ name: '--o', syntax: '<length-percentage>+', initialValue: '50%'
});

And that’s it! The result can be seen below:

Animated gif. We start with a grey background and we have an oranges disc, randomly positioned, growing from nothing until it covers everything, so now it looks like we have an orange background. Then we have grey disc, randomly positioned, growing from nothing until it covers the entire background and we're back where we started from: a grey background.
Randomly positioned growing discs (live demo, Blink browsers with flag only).

I’d say the future of changing gradients with keyframe animations looks pretty cool. But in the meanwhile, for cross-browser solutions, the JavaScript way remains the only valid one.

The post The State of Changing Gradients with CSS Transitions and Animations appeared first on CSS-Tricks.

Radial Gradient Recipes

Radial gradients are pretty dang cool. It’s amazing we can paint the background of an element with them so easily. Easily is a relative term though. It’s certainly easier than needing to create a graphic in third-party software to use as the background, and the syntax is highly learnable. But it’s also not that easy to remember if you don’t use it often, and it’s more complicated than linear-gradient().

I figured I’d put together a page of reference examples, so if you know what you need but forget the syntax, it’s easy to find that starter code example here.

Centered Bursts

The simplest possible syntax places the first color in the center of the element and the second color on the outside and that’s that:

See the Pen Radial Gradient – Centered by Chris Coyier (@chriscoyier) on CodePen.

That will stretch the gradient into an ellipse on a non-square element though. If you don’t like that, you can force the shape into a circle, like the second example here demonstrates:

See the Pen Radial Gradient – Circle vs. Ellipse by Chris Coyier (@chriscoyier) on CodePen.

You can also control the size by literally saying how large the circle/ellipse should be (the final color will still stretch to cover the element) by:

  • Using a keyword closest-side, farthest-side, closest-corner, farthest-corner
  • Explicitly saying like radial-gradient(circle 100px, ...)
  • Using color stops like radial-gradient(#56ab2f, #a8e063 150px)

See the Pen Radial Gradient – Sizing by Chris Coyier (@chriscoyier) on CodePen.

Here’s some of that stuff in use:

See the Pen Usage of Radial Gradients by Chris Coyier (@chriscoyier) on CodePen.

See the Pen Lit text by Chris Coyier (@chriscoyier) on CodePen.

Positioned

Besides controlling the size and shape of the gradient, the other big trick to know with radial gradients is that you can position the center of them.

This is one of the shortcomings, I find, with gradient generators. They help you pick colors and color stops and stuff, but they usually punt on the positioning stuff.

This is a beautiful gradient tool, but doesn’t help with positioning or sizing. Some of them do help a little with positioning (see “Expert” settings), but don’t expose all the possibilities.

The center of a radial gradient doesn’t have to be in the center! For example, you can position the center in the top left like this:

.element { background: radial-gradient( at top left, var(--light), var(--dark) /* using variables just for fun! */ )
}

Here’s all the four corners:

See the Pen Positioned Radial Gradients by Chris Coyier (@chriscoyier) on CodePen.

You can also be very specifically positioned. Here’s an example of a gradient positioned exactly 195px from the left along the bottom of the element. It also has a specific size, but otherwise does the default ellipse shape:

.element { background: radial-gradient( 150px 40px at 195px bottom, #666, #222 );
}

See the Pen Specifically positioned gradient by Chris Coyier (@chriscoyier) on CodePen.

Another little thing to know is that you can use transparent in the gradients to expose the color behind if that’s needed, or partially transparent colors like rgba(255, 255, 255, 0.5) to do the same at a colorized color stop.

Also, radial gradients can be used with multiple backgrounds, applying multiple of them to a single element, even overlapping!

.element { background: radial-gradient( circle at top left, rgba(255, 255, 255, 0.5), transparent 100px ), radial-gradient( circle at top right, rgba(255, 255, 255, 0.5), transparent 100px ), radial-gradient( at bottom left, rgba(255, 0, 255, 0.5), transparent 400px ), radial-gradient( at bottom right, rgba(255, 100, 100, 0.5), transparent 400px );
}

See the Pen Multiple Gradients by Chris Coyier (@chriscoyier) on CodePen.

To highlight the idea that the center of the gradient can be anywhere, here’s a gradient that follows the mouse:

See the Pen Radial Gradient Move With Mouse by Leo Sammarco (@lsammarco) on CodePen.

Resources

People tend to think about browser support, and rightfully so, but don’t think too hard about it in this case. We’re at pretty much across the board support even without any prefixes.

  • There is also repeating-linear-gradient() and here Ana Tudor gets into some detail and use cases.
  • Need some colors that go nicely togehter in a gradient? uiGradients is nice.
  • MDN on radial-gradient
  • See our complete guide to all gradients here!

OK bye!

See the Pen CSS Sunset Sunrise by Marty Saetre (@msaetre) on CodePen.

The post Radial Gradient Recipes appeared first on CSS-Tricks.