gradients – CSS-Tricks https://css-tricks.com Tips, Tricks, and Techniques on using Cascading Style Sheets. Thu, 15 Feb 2024 10:49:41 +0000 en-US hourly 1 https://wordpress.org/?v=6.4.3 https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1 gradients – CSS-Tricks https://css-tricks.com 32 32 45537868 Different Ways to Get CSS Gradient Shadows https://css-tricks.com/different-ways-to-get-css-gradient-shadows/ https://css-tricks.com/different-ways-to-get-css-gradient-shadows/#respond Fri, 10 Feb 2023 15:13:42 +0000 https://css-tricks.com/?p=376764 It’s a question I hear asked quite often: Is it possible to create shadows from gradients instead of solid colors? There is no specific CSS property that does this (believe me, I’ve looked) and any blog post you find about …


Different Ways to Get CSS Gradient Shadows originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
It’s a question I hear asked quite often: Is it possible to create shadows from gradients instead of solid colors? There is no specific CSS property that does this (believe me, I’ve looked) and any blog post you find about it is basically a lot of CSS tricks to approximate a gradient. We’ll actually cover some of those as we go.

But first… another article about gradient shadows? Really?

Yes, this is yet another post on the topic, but it is different. Together, we’re going to push the limits to get a solution that covers something I haven’t seen anywhere else: transparency. Most of the tricks work if the element has a non-transparent background but what if we have a transparent background? We will explore this case here!

Before we start, let me introduce my gradient shadows generator. All you have to do is to adjust the configuration, and get the code. But follow along because I’m going to help you understand all the logic behind the generated code.

Non-transparent solution

Let’s start with the solution that’ll work for 80% of most cases. The most typical case: you are using an element with a background, and you need to add a gradient shadow to it. No transparency issues to consider there.

The solution is to rely on a pseudo-element where the gradient is defined. You place it behind the actual element and apply a blur filter to it.

.box {
  position: relative;
}
.box::before {
  content: "";
  position: absolute;
  inset: -5px; /* control the spread */
  transform: translate(10px, 8px); /* control the offsets */
  z-index: -1; /* place the element behind */
  background: /* your gradient here */;
  filter: blur(10px); /* control the blur */
}

It looks like a lot of code, and that’s because it is. Here’s how we could have done it with a box-shadow instead if we were using a solid color instead of a gradient.

box-shadow: 10px 8px 10px 5px orange;

That should give you a good idea of what the values in the first snippet are doing. We have X and Y offsets, the blur radius, and the spread distance. Note that we need a negative value for the spread distance that comes from the inset property.

Here’s a demo showing the gradient shadow next to a classic box-shadow:

If you look closely you will notice that both shadows are a little different, especially the blur part. It’s not a surprise because I am pretty sure the filter property’s algorithm works differently than the one for box-shadow. That’s not a big deal since the result is, in the end, quite similar.

This solution is good, but still has a few drawbacks related to the z-index: -1 declaration. Yes, there is “stacking context” happening there!

I applied a transform to the main element, and boom! The shadow is no longer below the element. This is not a bug but the logical result of a stacking context. Don’t worry, I will not start a boring explanation about stacking context (I already did that in a Stack Overflow thread), but I’ll still show you how to work around it.

The first solution that I recommend is to use a 3D transform:

.box {
  position: relative;
  transform-style: preserve-3d;
}
.box::before {
  content: "";
  position: absolute;
  inset: -5px;
  transform: translate3d(10px, 8px, -1px); /* (X, Y, Z) */
  background: /* .. */;
  filter: blur(10px);
}

Instead of using z-index: -1, we will use a negative translation along the Z-axis. We will put everything inside translate3d(). Don’t forget to use transform-style: preserve-3d on the main element; otherwise, the 3D transform won’t take effect.

As far as I know, there is no side effect to this solution… but maybe you see one. If that’s the case, share it in the comment section, and let’s try to find a fix for it!

If for some reason you are unable to use a 3D transform, the other solution is to rely on two pseudo-elements — ::before and ::after. One creates the gradient shadow, and the other reproduces the main background (and other styles you might need). That way, we can easily control the stacking order of both pseudo-elements.

.box {
  position: relative;
  z-index: 0; /* We force a stacking context */
}
/* Creates the shadow */
.box::before {
  content: "";
  position: absolute;
  z-index: -2;
  inset: -5px;
  transform: translate(10px, 8px);
  background: /* .. */;
  filter: blur(10px);
}
/* Reproduces the main element styles */
.box::after {
  content: """;
  position: absolute;
  z-index: -1;
  inset: 0;
  /* Inherit all the decorations defined on the main element */
  background: inherit;
  border: inherit;
  box-shadow: inherit;
}

It’s important to note that we are forcing the main element to create a stacking context by declaring z-index: 0, or any other property that do the same, on it. Also, don’t forget that pseudo-elements consider the padding box of the main element as a reference. So, if the main element has a border, you need to take that into account when defining the pseudo-element styles. You will notice that I am using inset: -2px on ::after to account for the border defined on the main element.

As I said, this solution is probably good enough in a majority of cases where you want a gradient shadow, as long as you don’t need to support transparency. But we are here for the challenge and to push the limits, so even if you don’t need what is coming next, stay with me. You will probably learn new CSS tricks that you can use elsewhere.

Transparent solution

Let’s pick up where we left off on the 3D transform and remove the background from the main element. I will start with a shadow that has both offsets and spread distance equal to 0.

The idea is to find a way to cut or hide everything inside the area of the element (inside the green border) while keeping what is outside. We are going to use clip-path for that. But you might wonder how clip-path can make a cut inside an element.

Indeed, there’s no way to do that, but we can simulate it using a particular polygon pattern:

clip-path: polygon(-100vmax -100vmax,100vmax -100vmax,100vmax 100vmax,-100vmax 100vmax,-100vmax -100vmax,0 0,0 100%,100% 100%,100% 0,0 0)

Tada! We have a gradient shadow that supports transparency. All we did is add a clip-path to the previous code. Here is a figure to illustrate the polygon part.

Showing the clip-path coordinates for the element.

The blue area is the visible part after applying the clip-path. I am only using the blue color to illustrate the concept, but in reality, we will only see the shadow inside that area. As you can see, we have four points defined with a big value (B). My big value is 100vmax, but it can be any big value you want. The idea is to ensure we have enough space for the shadow. We also have four points that are the corners of the pseudo-element.

The arrows illustrate the path that defines the polygon. We start from (-B, -B) until we reach (0,0). In total, we need 10 points. Not eight points because two points are repeated twice in the path ((-B,-B) and (0,0)).

There’s still one more thing left for us to do, and it’s to account for the spread distance and the offsets. The only reason the demo above works is because it is a particular case where the offsets and spread distance are equal to 0.

Let’s define the spread and see what happens. Remember that we use inset with a negative value to do this:

The pseudo-element is now bigger than the main element, so the clip-path cuts more than we need it to. Remember, we always need to cut the part inside the main element (the area inside the green border of the example). We need to adjust the position of the four points inside of clip-path.

.box {
  --s: 10px; /* the spread  */
  position: relative;
}
.box::before {
  inset: calc(-1 * var(--s));
  clip-path: polygon(
    -100vmax -100vmax,
     100vmax -100vmax,
     100vmax 100vmax,
    -100vmax 100vmax,
    -100vmax -100vmax,
    calc(0px  + var(--s)) calc(0px  + var(--s)),
    calc(0px  + var(--s)) calc(100% - var(--s)),
    calc(100% - var(--s)) calc(100% - var(--s)),
    calc(100% - var(--s)) calc(0px  + var(--s)),
    calc(0px  + var(--s)) calc(0px  + var(--s))
  );
}

We’ve defined a CSS variable, --s, for the spread distance and updated the polygon points. I didn’t touch the points where I am using the big value. I only update the points that define the corners of the pseudo-element. I increase all the zero values by --s and decrease the 100% values by --s.

It’s the same logic with the offsets. When we translate the pseudo-element, the shadow is out of alignment, and we need to rectify the polygon again and move the points in the opposite direction.

.box {
  --s: 10px; /* the spread */
  --x: 10px; /* X offset */
  --y: 8px;  /* Y offset */
  position: relative;
}
.box::before {
  inset: calc(-1 * var(--s));
  transform: translate3d(var(--x), var(--y), -1px);
  clip-path: polygon(
    -100vmax -100vmax,
     100vmax -100vmax,
     100vmax 100vmax,
    -100vmax 100vmax,
    -100vmax -100vmax,
    calc(0px  + var(--s) - var(--x)) calc(0px  + var(--s) - var(--y)),
    calc(0px  + var(--s) - var(--x)) calc(100% - var(--s) - var(--y)),
    calc(100% - var(--s) - var(--x)) calc(100% - var(--s) - var(--y)),
    calc(100% - var(--s) - var(--x)) calc(0px  + var(--s) - var(--y)),
    calc(0px  + var(--s) - var(--x)) calc(0px  + var(--s) - var(--y))
  );
}

There are two more variables for the offsets: --x and --y. We use them inside of transform and we also update the clip-path values. We still don’t touch the polygon points with big values, but we offset all the others — we reduce --x from the X coordinates, and --y from the Y coordinates.

Now all we have to do is to update a few variables to control the gradient shadow. And while we are at it, let’s also make the blur radius a variable as well:

Do we still need the 3D transform trick?

It all depends on the border. Don’t forget that the reference for a pseudo-element is the padding box, so if you apply a border to your main element, you will have an overlap. You either keep the 3D transform trick or update the inset value to account for the border.

Here is the previous demo with an updated inset value in place of the 3D transform:

I‘d say this is a more suitable way to go because the spread distance will be more accurate, as it starts from the border-box instead of the padding-box. But you will need to adjust the inset value according to the main element’s border. Sometimes, the border of the element is unknown and you have to use the previous solution.

With the earlier non-transparent solution, it’s possible you will face a stacking context issue. And with the transparent solution, it’s possible you face a border issue instead. Now you have options and ways to work around those issues. The 3D transform trick is my favorite solution because it fixes all the issues (The online generator will consider it as well)

Adding a border radius

If you try adding border-radius to the element when using the non-transparent solution we started with, it is a fairly trivial task. All you need to do is to inherit the same value from the main element, and you are done.

Even if you don’t have a border radius, it’s a good idea to define border-radius: inherit. That accounts for any potential border-radius you might want to add later or a border radius that comes from somewhere else.

It’s a different story when dealing with the transparent solution. Unfortunately, it means finding another solution because clip-path cannot deal with curvatures. That means we won’t be able to cut the area inside the main element.

We will introduce the mask property to the mix.

This part was very tedious, and I struggled to find a general solution that doesn’t rely on magic numbers. I ended up with a very complex solution that uses only one pseudo-element, but the code was a lump of spaghetti that covers only a few particular cases. I don’t think it is worth exploring that route.

I decided to insert an extra element for the sake of simpler code. Here’s the markup:

<div class="box">
  <sh></sh>
</div>

I am using a custom element, <sh>, to avoid any potential conflict with external CSS. I could have used a <div>, but since it’s a common element, it can easily be targeted by another CSS rule coming from somewhere else that can break our code.

The first step is to position the <sh> element and purposely create an overflow:

.box {
  --r: 50px;
  position: relative;
  border-radius: var(--r);
}
.box sh {
  position: absolute;
  inset: -150px;
  border: 150px solid #0000;
  border-radius: calc(150px + var(--r));
}

The code may look a bit strange, but we’ll get to the logic behind it as we go. Next, we create the gradient shadow using a pseudo-element of <sh>.

.box {
  --r: 50px;
  position: relative;
  border-radius: var(--r);
  transform-style: preserve-3d;
}
.box sh {
  position: absolute;
  inset: -150px;
  border: 150px solid #0000;
  border-radius: calc(150px + var(--r));
  transform: translateZ(-1px)
}
.box sh::before {
  content: "";
  position: absolute;
  inset: -5px;
  border-radius: var(--r);
  background: /* Your gradient */;
  filter: blur(10px);
  transform: translate(10px,8px);
}

As you can see, the pseudo-element uses the same code as all the previous examples. The only difference is the 3D transform defined on the <sh> element instead of the pseudo-element. For the moment, we have a gradient shadow without the transparency feature:

Note that the area of the <sh> element is defined with the black outline. Why I am doing this? Because that way, I am able to apply a mask on it to hide the part inside the green area and keep the overflowing part where we need to see the shadow.

I know it’s a bit tricky, but unlike clip-path, the mask property doesn’t account for the area outside an element to show and hide things. That’s why I was obligated to introduce the extra element — to simulate the “outside” area.

Also, note that I am using a combination of border and inset to define that area. This allows me to keep the padding-box of that extra element the same as the main element so that the pseudo-element won’t need additional calculations.

Another useful thing we get from using an extra element is that the element is fixed, and only the pseudo-element is moving (using translate). This will allow me to easily define the mask, which is the last step of this trick.

mask:
  linear-gradient(#000 0 0) content-box,
  linear-gradient(#000 0 0);
mask-composite: exclude;

It’s done! We have our gradient shadow, and it supports border-radius! You probably expected a complex mask value with oodles of gradients, but no! We only need two simple gradients and a mask-composite to complete the magic.

Let’s isolate the <sh> element to understand what is happening there:

.box sh {
  position: absolute;
  inset: -150px;
  border: 150px solid red;
  background: lightblue;
  border-radius: calc(150px + var(--r));
}

Here’s what we get:

Note how the inner radius matches the main element’s border-radius. I have defined a big border (150px) and a border-radius equal to the big border plus the main element’s radius. On the outside, I have a radius equal to 150px + R. On the inside, I have 150px + R - 150px = R.

We must hide the inner (blue) part and make sure the border (red) part is still visible. To do that, I’ve defined two mask layers —One that covers only the content-box area and another that covers the border-box area (the default value). Then I excluded one from another to reveal the border.

mask:
  linear-gradient(#000 0 0) content-box,
  linear-gradient(#000 0 0);
mask-composite: exclude;

I used the same technique to create a border that supports gradients and border-radius. Ana Tudor has also a good article about masking composite that I invite you to read.

Are there any drawbacks to this method?

Yes, this definitely not perfect. The first issue you may face is related to using a border on the main element. This may create a small misalignment in the radii if you don’t account for it. We have this issue in our example, but perhaps you can hardly notice it.

The fix is relatively easy: Add the border’s width for the <sh> element’s inset.

.box {
  --r: 50px;
  border-radius: var(--r);
  border: 2px solid;
}
.box sh {
  position: absolute;
  inset: -152px; /* 150px + 2px */
  border: 150px solid #0000;
  border-radius: calc(150px + var(--r));
}

Another drawback is the big value we’re using for the border (150px in the example). This value should be big enough to contain the shadow but not too big to avoid overflow and scrollbar issues. Luckily, the online generator will calculate the optimal value considering all the parameters.

The last drawback I am aware of is when you’re working with a complex border-radius. For example, if you want a different radius applied to each corner, you must define a variable for each side. It’s not really a drawback, I suppose, but it can make your code a bit tougher to maintain.

.box {
  --r-top: 10px;
  --r-right: 40px;
  --r-bottom: 30px;
  --r-left: 20px;
  border-radius: var(--r-top) var(--r-right) var(--r-bottom) var(--r-left);
}
.box sh {
  border-radius: calc(150px + var(--r-top)) calc(150px + var(--r-right)) calc(150px + var(--r-bottom)) calc(150px + var(--r-left));
}
.box sh:before {
  border-radius: var(--r-top) var(--r-right) var(--r-bottom) var(--r-left);
}

The online generator only considers a uniform radius for the sake of simplicity, but you now know how to modify the code if you want to consider a complex radius configuration.

Wrapping up

We’ve reached the end! The magic behind gradient shadows is no longer a mystery. I tried to cover all the possibilities and any possible issues you might face. If I missed something or you discover any issue, please feel free to report it in the comment section, and I’ll check it out.

Again, a lot of this is likely overkill considering that the de facto solution will cover most of your use cases. Nevertheless, it’s good to know the “why” and “how” behind the trick, and how to overcome its limitations. Plus, we got good exercise playing with CSS clipping and masking.

And, of course, you have the online generator you can reach for anytime you want to avoid the hassle.


Different Ways to Get CSS Gradient Shadows originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/different-ways-to-get-css-gradient-shadows/feed/ 0 376764
A Fancy Hover Effect For Your Avatar https://css-tricks.com/a-fancy-hover-effect-for-your-avatar/ https://css-tricks.com/a-fancy-hover-effect-for-your-avatar/#comments Fri, 03 Feb 2023 15:11:46 +0000 https://css-tricks.com/?p=376676 Do you know that kind of effect where someone’s head is poking through a circle or hole? The famous Porky Pig animation where he waves goodbye while popping out of a series of red rings is the perfect example, and …


A Fancy Hover Effect For Your Avatar originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Do you know that kind of effect where someone’s head is poking through a circle or hole? The famous Porky Pig animation where he waves goodbye while popping out of a series of red rings is the perfect example, and Kilian Valkhof actually re-created that here on CSS-Tricks a while back.

I have a similar idea but tackled a different way and with a sprinkle of animation. I think it’s pretty practical and makes for a neat hover effect you can use on something like your own avatar.

See that? We’re going to make a scaling animation where the avatar seems to pop right out of the circle it’s in. Cool, right? Don’t look at the code and let’s build this animation together step-by-step.

The HTML: Just one element

If you haven’t checked the code of the demo and you are wondering how many divs this’ll take, then stop right there, because our markup is nothing but a single image element:

<img src="" alt="">

Yes, a single element! The challenging part of this exercise is using the smallest amount of code possible. If you have been following me for a while, you should be used to this. I try hard to find CSS solutions that can be achieved with the smallest, most maintainable code possible.

I wrote a series of articles here on CSS-Tricks where I explore different hover effects using the same HTML markup containing a single element. I go into detail on gradients, masking, clipping, outlines, and even layout techniques. I highly recommend checking those out because I will re-use many of the tricks in this post.

An image file that’s square with a transparent background will work best for what we’re doing. Here’s the one I’m using if you want start with that.

Designed by Cang

I’m hoping to see lots of examples of this as possible using real images — so please share your final result in the comments when you’re done so we can build a collection!

Before jumping into CSS, let’s first dissect the effect. The image gets bigger on hover, so we’ll for sure use transform: scale() in there. There’s a circle behind the avatar, and a radial gradient should do the trick. Finally, we need a way to create a border at the bottom of the circle that creates the appearance of the avatar behind the circle.

Let’s get to work!

The scale effect

Let’s start by adding the transform:

img {
  width: 280px;
  aspect-ratio: 1;
  cursor: pointer;
  transition: .5s;
}
img:hover {
  transform: scale(1.35);
}

Nothing complicated yet, right? Let’s move on.

The circle

We said that the background would be a radial gradient. That’s perfect because we can create hard stops between the colors of a radial gradient, which make it look like we’re drawing a circle with solid lines.

img {
  --b: 5px; /* border width */

  width: 280px;
  aspect-ratio: 1;
  background:
    radial-gradient(
      circle closest-side,
      #ECD078 calc(99% - var(--b)),
      #C02942 calc(100% - var(--b)) 99%,
      #0000
    );
  cursor: pointer;
  transition: .5s;
}
img:hover {
  transform: scale(1.35);
}

Note the CSS variable, --b, I’m using there. It represents the thickness of the “border” which is really just being used to define the hard color stops for the red part of the radial gradient.

The next step is to play with the gradient size on hover. The circle needs to keep its size as the image grows. Since we are applying a scale() transformation, we actually need to decrease the size of the circle because it otherwise scales up with the avatar. So, while the image scales up, we need the gradient to scale down.

Let’s start by defining a CSS variable, --f, that defines the “scale factor”, and use it to set the size of the circle. I’m using 1 as the default value, as in that’s the initial scale for the image and the circle that we transform from.

Here is a demo to illustrate the trick. Hover to see what is happening behind the scenes:

I added a third color to the radial-gradient to better identify the area of the gradient on hover:

radial-gradient(
  circle closest-side,
  #ECD078 calc(99% - var(--b)),
  #C02942 calc(100% - var(--b)) 99%,
  lightblue
);

Now we have to position our background at the center of the circle and make sure it takes up the full height. I like to declare everything directly on the background shorthand property, so we can add our background positioning and make sure it doesn’t repeat by tacking on those values right after the radial-gradient():

background: radial-gradient() 50% / calc(100% / var(--f)) 100% no-repeat;

The background is placed at the center (50%), has a width equal to calc(100%/var(--f)), and has a height equal to 100%.

Nothing scales when --f is equal to 1 — again, our initial scale. Meanwhile, the gradient takes up the full width of the container. When we increase --f, the element’s size grows — thanks to the scale() transform — and the gradient’s size decreases.

Here’s what we get when we apply all of this to our demo:

We’re getting closer! We have the overflow effect at the top, but we still need to hide the bottom part of the image, so it looks like it is popping out of the circle rather than sitting in front of it. That’s the tricky part of this whole thing and is what we’re going to do next.

The bottom border

I first tried tackling this with the border-bottom property, but I was unable to find a way to match the size of the border to the size to the circle. Here’s the best I could get and you can immediately see it’s wrong:

The actual solution is to use the outline property. Yes, outline, not border. In a previous article, I show how outline is powerful and allows us to create cool hover effects. Combined with outline-offset, we have exactly what we need for our effect.

The idea is to set an outline on the image and adjust its offset to create the bottom border. The offset will depend on the scaling factor the same way the gradient size did.

Now we have our bottom “border” (actually an outline) combined with the “border” created by the gradient to create a full circle. We still need to hide portions of the outline (from the top and the sides), which we’ll get to in a moment.

Here’s our code so far, including a couple more CSS variables you can use to configure the image size (--s) and the “border” color (--c):

img {
  --s: 280px; /* image size */
  --b: 5px; /* border thickness */
  --c: #C02942; /* border color */
  --f: 1; /* initial scale */

  width: var(--s);
  aspect-ratio: 1;
  cursor: pointer;
  border-radius: 0 0 999px 999px;
  outline: var(--b) solid var(--c);
  outline-offset: calc((1 / var(--f) - 1) * var(--s) / 2 - var(--b));
  background: 
    radial-gradient(
      circle closest-side,
      #ECD078 calc(99% - var(--b)),
      var(--c) calc(100% - var(--b)) 99%,
      #0000
    ) 50% / calc(100% / var(--f)) 100% no-repeat;
  transform: scale(var(--f));
  transition: .5s;
}
img:hover {
  --f: 1.35; /* hover scale */
}

Since we need a circular bottom border, we added a border-radius on the bottom side, allowing the outline to match the curvature of the gradient.

The calculation used on outline-offset is a lot more straightforward than it looks. By default, outline is drawn outside of the element’s box. And in our case, we need it to overlap the element. More precisely, we need it to follow the circle created by the gradient.

Diagram of the background transition.

When we scale the element, we see the space between the circle and the edge. Let’s not forget that the idea is to keep the circle at the same size after the scale transformation runs, which leaves us with the space we will use to define the outline’s offset as illustrated in the above figure.

Let’s not forget that the second element is scaled, so our result is also scaled… which means we need to divide the result by f to get the real offset value:

Offset = ((f - 1) * S/2) / f = (1 - 1/f) * S/2

We add a negative sign since we need the outline to go from the outside to the inside:

Offset = (1/f - 1) * S/2

Here’s a quick demo that shows how the outline follows the gradient:

You may already see it, but we still need the bottom outline to overlap the circle rather than letting it bleed through it. We can do that by removing the border’s size from the offset:

outline-offset: calc((1 / var(--f) - 1) * var(--s) / 2) - var(--b));

Now we need to find how to remove the top part from the outline. In other words, we only want the bottom part of the image’s outline.

First, let’s add space at the top with padding to help avoid the overlap at the top:

img {
  --s: 280px; /* image size */
  --b: 5px;   /* border thickness */
  --c: #C02942; /* border color */
  --f: 1; /* initial scale */

  width: var(--s);
  aspect-ratio: 1;
  padding-block-start: calc(var(--s)/5);
  /* etc. */
}
img:hover {
  --f: 1.35; /* hover scale */
}

There is no particular logic to that top padding. The idea is to ensure the outline doesn’t touch the avatar’s head. I used the element’s size to define that space to always have the same proportion.

Note that I have added the content-box value to the background:

background:
  radial-gradient(
    circle closest-side,
    #ECD078 calc(99% - var(--b)),
    var(--c) calc(100% - var(--b)) 99%,
    #0000
  ) 50%/calc(100%/var(--f)) 100% no-repeat content-box;

We need this because we added padding and we only want the background set to the content box, so we must explicitly tell the background to stop there.

Adding CSS mask to the mix

We reached the last part! All we need to do is to hide some pieces, and we are done. For this, we will rely on the mask property and, of course, gradients.

Here is a figure to illustrate what we need to hide or what we need to show to be more accurate

Showing how the mask applies to the bottom portion of the circle.

The left image is what we currently have, and the right is what we want. The green part illustrates the mask we must apply to the original image to get the final result.

We can identify two parts of our mask:

  • A circular part at the bottom that has the same dimension and curvature as the radial gradient we used to create the circle behind the avatar
  • A rectangle at the top that covers the area inside the outline. Notice how the outline is outside the green area at the top — that’s the most important part, as it allows the outline to be cut so that only the bottom part is visible.

Here’s our final CSS:

img {
  --s: 280px; /* image size */
  --b: 5px; /* border thickness */
  --c: #C02942; /* border color */
  --f: 1; /* initial scale */

  --_g: 50% / calc(100% / var(--f)) 100% no-repeat content-box;
  --_o: calc((1 / var(--f) - 1) * var(--s) / 2 - var(--b));

  width: var(--s);
  aspect-ratio: 1;
  padding-top: calc(var(--s)/5);
  cursor: pointer;
  border-radius: 0 0 999px 999px;
  outline: var(--b) solid var(--c);
  outline-offset: var(--_o);
  background: 
    radial-gradient(
      circle closest-side,
      #ECD078 calc(99% - var(--b)),
      var(--c) calc(100% - var(--b)) 99%,
      #0000) var(--_g);
  mask:
    linear-gradient(#000 0 0) no-repeat
    50% calc(-1 * var(--_o)) / calc(100% / var(--f) - 2 * var(--b)) 50%,
    radial-gradient(
      circle closest-side,
      #000 99%,
      #0000) var(--_g);
  transform: scale(var(--f));
  transition: .5s;
}
img:hover {
  --f: 1.35; /* hover scale */
}

Let’s break down that mask property. For starters, notice that a similar radial-gradient() from the background property is in there. I created a new variable, --_g, for the common parts to make things less cluttered.

--_g: 50% / calc(100% / var(--f)) 100% no-repeat content-box;

mask:
  radial-gradient(
    circle closest-side,
    #000 99%,
    #0000) var(--_g);

Next, there’s a linear-gradient() in there as well:

--_g: 50% / calc(100% / var(--f)) 100% no-repeat content-box;

mask:
  linear-gradient(#000 0 0) no-repeat
    50% calc(-1 * var(--_o)) / calc(100% / var(--f) - 2 * var(--b)) 50%,
  radial-gradient(
    circle closest-side,
    #000 99%,
    #0000) var(--_g);

This creates the rectangle part of the mask. Its width is equal to the radial gradient’s width minus twice the border thickness:

calc(100% / var(--f) - 2 * var(--b))

The rectangle’s height is equal to half, 50%, of the element’s size.

We also need the linear gradient placed at the horizontal center (50%) and offset from the top by the same value as the outline’s offset. I created another CSS variable, --_o, for the offset we previously defined:

--_o: calc((1 / var(--f) - 1) * var(--s) / 2 - var(--b));

One of the confusing things here is that we need a negative offset for the outline (to move it from outside to inside) but a positive offset for the gradient (to move from top to bottom). So, if you’re wondering why we multiply the offset, --_o, by -1, well, now you know!

Here is a demo to illustrate the mask’s gradient configuration:

Hover the above and see how everything move together. The middle box illustrates the mask layer composed of two gradients. Imagine it as the visible part of the left image, and you get the final result on the right!

Wrapping up

Oof, we’re done! And not only did we wind up with a slick hover animation, but we did it all with a single HTML <img> element. Just that and less than 20 lines of CSS trickery!

Sure, we relied on some little tricks and math formulas to reach such a complex effect. But we knew exactly what to do since we identified the pieces we needed up-front.

Could we have simplified the CSS if we allowed ourselves more HTML? Absolutely. But we’re here to learn new CSS tricks! This was a good exercise to explore CSS gradients, masking, the outline property’s behavior, transformations, and a whole bunch more. If you felt lost at any point, then definitely check out my series that uses the same general concepts. It sometimes helps to see more examples and use cases to drive a point home.

I will leave you with one last demo that uses photos of popular CSS developers. Don’t forget to show me a demo with your own image so I can add it to the collection!


A Fancy Hover Effect For Your Avatar originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/a-fancy-hover-effect-for-your-avatar/feed/ 2 376676
Animated Background Stripes That Transition on Hover https://css-tricks.com/animated-background-stripes-transition-hover/ https://css-tricks.com/animated-background-stripes-transition-hover/#comments Thu, 08 Dec 2022 15:36:21 +0000 https://css-tricks.com/?p=375697 How often to do you reach for the CSS background-size property? If you’re like me — and probably lots of other front-end folks — then it’s usually when you background-size: cover an image to fill the space of an entire …


Animated Background Stripes That Transition on Hover originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
How often to do you reach for the CSS background-size property? If you’re like me — and probably lots of other front-end folks — then it’s usually when you background-size: cover an image to fill the space of an entire element.

Well, I was presented with an interesting challenge that required more advanced background sizing: background stripes that transition on hover. Check this out and hover it with your cursor:

There’s a lot more going on there than the size of the background, but that was the trick I needed to get the stripes to transition. I thought I’d show you how I arrived there, not only because I think it’s a really nice visual effect, but because it required me to get creative with gradients and blend modes that I think you might enjoy.

Let’s start with a very basic setup to keep things simple. I’m talking about a single <div> in the HTML that’s styled as a green square:

<div></div>
div {
  width: 500px;
  height: 500px;
  background: palegreen;
}
Perfect square with a pale green background color.

Setting up the background stripes

If your mind went straight to a CSS linear gradient when you saw those stripes, then we’re already on the same page. We can’t exactly do a repeating gradient in this case since we want the stripes to occupy uneven amounts of space and transition them, but we can create five stripes by chaining five backgrounds on top of our existing background color and placing them to the top-right of the container:

div {
  width: 500px;
  height: 500px;
  background: 
    linear-gradient(black, black) top right,
    linear-gradient(black, black) top 100px right,
    linear-gradient(black, black) top 200px right,
    linear-gradient(black, black) top 300px right,
    linear-gradient(black, black) top 400px right, 
    palegreen;
}

I made horizontal stripes, but we could also go vertical with the approach we’re covering here. And we can simplify this quite a bit with custom properties:

div {
  --gt: linear-gradient(black, black);
  --n: 100px;

  width: 500px;
  height: 500px;
  background: 
    var(--gt) top right,
    var(--gt) top var(--n) right,
    var(--gt) top calc(var(--n) * 2) right,
    var(--gt) top calc(var(--n) * 3) right,
    var(--gt) top calc(var(--n) * 4) right, 
    palegreen;
}

So, the --gt value is the gradient and --n is a constant we’re using to nudge the stripes downward so they are offset vertically. And you may have noticed that I haven’t set a true gradient, but rather solid black stripes in the linear-gradient() function — that’s intentional and we’ll get to why I did that in a bit.

One more thing we ought to do before moving on is prevent our backgrounds from repeating; otherwise, they’ll tile and fill the entire space:

div {
  --gt: linear-gradient(black, black);
  --n: 100px;

  width: 500px;
  height: 500px;
  background: 
    var(--gt) top right,
    var(--gt) top var(--n) right,
    var(--gt) top calc(var(--n) * 2) right,
    var(--gt) top calc(var(--n) * 3) right,
    var(--gt) top calc(var(--n) * 4) right, 
    palegreen;
  background-repeat: no-repeat;
}

We could have set background-repeat in the background shorthand, but I decided to break it out here to keep things easy to read.

Offsetting the stripes

We technically have stripes, but it’s pretty tough to tell because there’s no spacing between them and they cover the entire container. It’s more like we have a solid black square.

This is where we get to use the background-size property. We want to set both the height and the width of the stripes and the property supports a two-value syntax that allows us to do exactly that. And, we can chain those sizes by comma separating them the same way we did on background.

Let’s start simple by setting the widths first. Using the single-value syntax for background-size sets the width and defaults the height to auto. I’m using totally arbitrary values here, so set the values to what works best for your design:

div {
  --gt: linear-gradient(black, black);
  --n: 100px;

  width: 500px;
  height: 500px;
  background: 
    var(--gt) top right,
    var(--gt) top var(--n) right,
    var(--gt) top calc(var(--n) * 2) right,
    var(--gt) top calc(var(--n) * 3) right,
    var(--gt) top calc(var(--n) * 4) right, 
    palegreen;
  background-repeat: no-repeat;
  background-size: 60%, 90%, 70%, 40%, 10%;
}

If you’re using the same values that I am, you’ll get this:

Doesn’t exactly look like we set the width for all the stripes, does it? That’s because of the auto height behavior of the single-value syntax. The second stripe is wider than the others below it, and it is covering them. We ought to set the heights so we can see our work. They should all be the same height and we can actually re-use our --n variable, again, to keep things simple:

div {
  --gt: linear-gradient(black, black);
  --n: 100px;

  width: 500px;
  height: 500px;
  background: 
    var(--gt) top right,
    var(--gt) top var(--n) right,
    var(--gt) top calc(var(--n) * 2) right,
    var(--gt) top calc(var(--n) * 3) right,
    var(--gt) top calc(var(--n) * 4) right, 
    palegreen;
    background-repeat: no-repeat;
    background-size: 60% var(--n), 90% var(--n), 70% var(--n), 40% var(--n), 10% var(--n); // HIGHLIGHT 15
}

Ah, much better!

Adding gaps between the stripes

This is a totally optional step if your design doesn’t require gaps between the stripes, but mine did and it’s not overly complicated. We change the height of each stripe’s background-size a smidge, decreasing the value so they fall short of filling the full vertical space.

We can continue to use our --n variable, but subtract a small amount, say 5px, using calc() to get what we want.

background-size: 60% calc(var(--n) - 5px), 90% calc(var(--n) - 5px), 70% calc(var(--n) - 5px), 40% calc(var(--n) - 5px), 10% calc(var(--n) - 5px);

That’s a lot of repetition we can eliminate with another variable:

div {
  --h: calc(var(--n) - 5px);
  /* etc. */
  background-size: 60% var(--h), 90% var(--h), 70% var(--h), 40% var(--h), 10% var(--h);
}

Masking and blending

Now let’s swap the palegreen background color we’ve been using for visual purposes up to this point for white.

div {
  /* etc. */
  background: 
    var(--gt) top right,
    var(--gt) top var(--n) right,
    var(--gt) top calc(var(--n) * 2) right,
    var(--gt) top calc(var(--n) * 3) right,
    var(--gt) top calc(var(--n) * 4) right, 
    #fff;
  /* etc. */
}

A black and white pattern like this is perfect for masking and blending. To do that, we’re first going to wrap our <div> in a new parent container and introduce a second <div> under it:

<section>
  <div></div>
  <div></div>
</section>

We’re going to do a little CSS re-factoring here. Now that we have a new parent container, we can pass the fixed width and height properties we were using on our <div> over there:

section {
  width: 500px;
  height: 500px;
} 

I’m also going to use CSS Grid to position the two <div> elements on top of one another. This is the same trick Temani Afif uses to create his super cool image galleries. The idea is that we place both divs over the full container using the grid-area property and align everything toward the center:

section {
  display: grid;
  align-items: center;
  justify-items: center;
  width: 500px;
  height: 500px;
} 

section > div {
  width: inherit;
  height: inherit;
  grid-area: 1 / 1;
}

Now, check this out. The reason I used a solid gradient that goes from black to black earlier is to set us up for masking and blending the two <div> layers. This isn’t true masking in the sense that we’re calling the mask property, but the contrast between the layers controls what colors are visible. The area covered by white will remain white, and the area covered by black leaks through. MDN’s documentation on blend modes has a nice explanation of how this works.

To get that working, I’ll apply the real gradient we want to see on the first <div> while applying the style rules from our initial <div> on the new one, using the :nth-child() pseudo-selector:

div:nth-child(1) { 
  background: linear-gradient(to right, red, orange); 
}

div:nth-child(2)  {
  --gt: linear-gradient(black, black);
  --n: 100px;
  --h: calc(var(--n) - 5px);
  background: 
    var(--gt) top right,
    var(--gt) top var(--n) right,
    var(--gt) top calc(var(--n) * 2) right,
    var(--gt) top calc(var(--n) * 3) right,
    var(--gt) top calc(var(--n) * 4) right, 
    white;
  background-repeat: no-repeat;
  background-size: 60% var(--h), 90% var(--h), 70% var(--h), 40% var(--h), 10% var(--h);
}

If we stop here, we actually won’t see any visual difference from what we had before. That’s because we haven’t done the actual blending yet. So, let’s do that now using the screen blend mode:

div:nth-child(2)  {
  /* etc. */
  mix-blend-mode: screen;
}

I used a beige background color in the demo I showed at the beginning of this article. That slightly darker sort of off-white coloring allows a little color to bleed through the rest of the background:

The hover effect

The last piece of this puzzle is the hover effect that widens the stripes to full width. First, let’s write out our selector for it. We want this to happen when the parent container (<section> in our case) is hovered. When it’s hovered, we’ll change the background size of the stripes contained in the second <div>:

/* When <section> is hovered, change the second div's styles */
section:hover > div:nth-child(2){
  /* styles go here */
}

We’ll want to change the background-size of the stripes to the full width of the container while maintaining the same height:

section:hover > div:nth-child(2){
  background-size: 100% var(--h);
}

That “snaps” the background to full-width. If we add a little transition to this, then we see the stripes expand on hover:

section:hover > div:nth-child(2){
  background-size: 100% var(--h);
  transition: background-size 1s;
}

Here’s that final demo once again:

I only added text in there to show what it might look like to use this in a different context. If you do the same, then it’s worth making sure there’s enough contrast between the text color and the colors used in the gradient to comply with WCAG guidelines. And while we’re touching briefly on accessibility, it’s worth considering user preferences for reduced motion when it comes to the hover effect.

That’s a wrap!

Pretty neat, right? I certainly think so. What I like about this, too, is that it’s pretty maintainable and customizable. For example, we can alter the height, colors, and direction of the stripes by changing a few values. You might even variablize a few more things in there — like the colors and widths — to make it even more configurable.

I’m really interested if you would have approached this a different way. If so, please share in the comments! It’d be neat to see how many variations we can collect.


Animated Background Stripes That Transition on Hover originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/animated-background-stripes-transition-hover/feed/ 2 375697
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/ 5 374697
Fancy Image Decorations: Outlines and Complex Animations https://css-tricks.com/fancy-image-decorations-outlines-and-complex-animations/ https://css-tricks.com/fancy-image-decorations-outlines-and-complex-animations/#comments Fri, 28 Oct 2022 12:45:47 +0000 https://css-tricks.com/?p=374302 We’ve spent the last two articles in this three-part series playing with gradients to make really neat image decorations using nothing but the <img> element. In this third and final piece, we are going to explore more techniques using the …


Fancy Image Decorations: Outlines and Complex Animations originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
We’ve spent the last two articles in this three-part series playing with gradients to make really neat image decorations using nothing but the <img> element. In this third and final piece, we are going to explore more techniques using the CSS outline property. That might sound odd because we generally use outline to draw a simple line around an element — sorta like border but it can only draw all four sides at once and is not part of the Box Model.

We can do more with it, though, and that’s what I want to experiment with in this article.

Fancy Image Decorations series

Let’s start with our first example — an overlay that disappears on hover with a cool animation:

We could accomplish this by adding an extra element over the image, but that’s what we’re challenging ourselves not to do in this series. Instead, we can reach for the CSS outline property and leverage that it can have a negative offset and is able to overlap its element.

img {
  --s: 250px; /* the size of the image */
  --b: 8px;   /* the border thickness*/
  --g: 14px;  /* the gap */
  --c: #4ECDC4;

  width: var(--s);
  aspect-ratio: 1;
  outline: calc(var(--s) / 2) solid #0009;
  outline-offset: calc(var(--s) / -2);
  cursor: pointer;
  transition: 0.3s;
}
img:hover {
  outline: var(--b) solid var(--c);
  outline-offset: var(--g);
}

The trick is to create an outline that’s as thick as half the image size, then offset it by half the image size with a negative value. Add in some semi-transparency with the color and we have our overlay!

Diagram showing the size of the outline sround the image and how it covers the image on hover.

The rest is what happens on :hover. We update the outline and the transition between both outlines creates the cool hover effect. The same technique can also be used to create a fading effect where we don’t move the outline but make it transparent.

Instead of using half the image size in this one, I am using a very big outline thickness value (100vmax) while applying a CSS mask. With this, there’s no longer a need to know the image size — it trick works at all sizes!

Diagram showing how adding a mask clips the extra outline around the image.

You may face issues using 100vmax as a big value in Safari. If it’s the case, consider the previous trick where you replace the 100vmax with half the image size.

We can take things even further! For example, instead of simply clipping the extra outline, we can create shapes and apply a fancy reveal animation.

Cool right? The outline is what creates the yellow overlay. The clip-path clips the extra outline to get the star shape. Then, on hover, we make the color transparent.

Oh, you want hearts instead? We can certainly do that!

Imagine all the possible combinations we can create. All we have to do is to draw a shape with a CSS mask and/or clip-path and combine it with the outline trick. One solution, infinite possibilities!

And, yes, we can definitely animate this as well. Let’s not forget that clip-path is animatable and mask relies on gradients — something we covered in super great detail in the first two articles of this series.

I know, the animation is a bit glitchy. This is more of a demo to illustrate the idea rather than the “final product” to be used in a production site. We’d wanna optimize things for a more natural transition.

Here is a demo that uses mask instead. It’s the one I teased you with at the end of the last article:

Did you know that the outline property was capable of so much awesomeness? Add it to your toolbox for fancy image decorations!

Combine all the things!

Now that we have learned many tricks using gradients, masks, clipping, and outline, it’s time for the grand finale. Let’s cap off this series by combine all that we have learned the past few weeks to showcase not only the techniques, but demonstrate just how flexible and modular these approaches are.

If you were seeing these demos for the first time, you might assume that there’s a bunch of extra divs wrappers and pseudo-elements being used to pull them off. But everything is happening directly on the <img> element. It’s the only selector we need to get these advanced shapes and effects!

Wrapping up

Well, geez, thanks for hanging out with me in this three-part series the past few weeks. We explored a slew of different techniques that turn simple images into something eye-catching and interactive. Will you use everything we covered? Certainly not! But my hope is that this has been a good exercise for you to dig into advanced uses of CSS features, like gradients, mask, clip-path, and outline.

And we did everything with just one <img> element! No extra div wrappers and pseudo-elements. Sure, it’s a constraint we put on ourselves, but it also pushed us to explore CSS and try to find innovative solutions to common use cases. So, before pumping extra markup into your HTML, think about whether CSS is already capable of handling the task.

You can find more fancy effects for images on my CSS Tip website. Here are a few examples:

Fancy Image Decorations series


Fancy Image Decorations: Outlines and Complex Animations originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/fancy-image-decorations-outlines-and-complex-animations/feed/ 3 374302
Fancy Image Decorations: Masks and Advanced Hover Effects https://css-tricks.com/fancy-image-decorations-masks-and-advanced-hover-effects/ https://css-tricks.com/fancy-image-decorations-masks-and-advanced-hover-effects/#comments Fri, 21 Oct 2022 12:46:27 +0000 https://css-tricks.com/?p=374194 Welcome to Part 2 of this three-part series! We are still decorating images without any extra elements and pseudo-elements. I hope you already took the time to digest Part 1 because we will continue working with a lot of gradients …


Fancy Image Decorations: Masks and Advanced Hover Effects originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Welcome to Part 2 of this three-part series! We are still decorating images without any extra elements and pseudo-elements. I hope you already took the time to digest Part 1 because we will continue working with a lot of gradients to create awesome visual effects. We are also going to introduce the CSS mask property for more complex decorations and hover effects.

Fancy Image Decorations series

Let’s turn to the first example we’re working on together…

The Postage Stamp

Believe or not, all it takes to make postage stamp CSS effect is two gradients and a filter:

img {
  --r: 10px; /* control the radius of the circles */
  padding: calc(2 * var(--r));
  filter: grayscale(.4);
  background: 
    radial-gradient(var(--r),#0000 98%,#fff) round
      calc(-1.5 * var(--r)) calc(-1.5 * var(--r)) / calc(3 * var(--r)) calc(3 * var(--r)),
    linear-gradient(#fff 0 0) no-repeat
      50% / calc(100% - 3 * var(--r)) calc(100% - 3 * var(--r));
}

As we saw in the previous article, the first step is to make space around the image with padding so we can draw a background gradient and see it there. Then we use a combination of radial-gradient() and linear-gradient() to cut those circles around the image.

Here is a step-by-step illustration that shows how the gradients are configured:

Note the use of the round value in the second step. It’s very important for the trick as it ensures the size of the gradient is adjusted to be perfectly aligned on all the sides, no matter what the image width or height is.

From the specification: The image is repeated as often as will fit within the background positioning area. If it doesn’t fit a whole number of times, it is rescaled so that it does.

The Rounded Frame

Let’s look at another image decoration that uses circles…

This example also uses a radial-gradient(), but this time I have created circles around the image instead of the cut-out effect. Notice that I am also using the round value again. The trickiest part here is the transparent gap between the frame and the image, which is where I reach for the CSS mask property:

img {
  --s: 20px; /* size of the frame */
  --g: 10px; /* the gap */
  --c: #FA6900; 

  padding: calc(var(--g) + var(--s));
  background: 
    radial-gradient(farthest-side, var(--c) 97%, #0000) 
      0 0 / calc(2 * var(--s)) calc(2 * var(--s)) round;
  mask:
    conic-gradient(from 90deg at calc(2 * var(--s)) calc(2 * var(--s)), #0000 25%, #000 0)
      calc(-1 * var(--s)) calc(-1 * var(--s)),
    linear-gradient(#000 0 0) content-box;
}

Masking allows us to show the area of the image — thanks to the linear-gradient() in there — as well as 20px around each side of it — thanks to the conic-gradient(). The 20px is nothing but the variable --s that defines the size of the frame. In other words, we need to hide the gap.

Here’s what I mean:

The linear gradient is the blue part of the background while the conic gradient is the red part of the background. That transparent part between both gradients is what we cut from our element to create the illusion of an inner transparent border.

The Inner Transparent Border

For this one, we are not going to create a frame but rather try something different. We are going to create a transparent inner border inside our image. Probably not that useful in a real-world scenario, but it’s good practice with CSS masks.

Similar to the previous example, we are going to rely on two gradients: a linear-gradient() for the inner part, and a conic-gradient() for the outer part. We’ll leave a space between them to create the transparent border effect.

img {
  --b: 5px;  /* the border thickness */
  --d: 20px; /* the distance from the edge */

  --_g: calc(100% - 2 * (var(--d) + var(--b)));
  mask:
    conic-gradient(from 90deg at var(--d) var(--d), #0000 25%, #000 0)
      0 0 / calc(100% - var(--d)) calc(100% - var(--d)),
    linear-gradient(#000 0 0) 50% / var(--_g) var(--_g) no-repeat;
}
Detailing the parts of the image that correspond to CSS variables.

You may have noticed that the conic gradient of this example has a different syntax from the previous example. Both are supposed to create the same shape, so why are they different? It’s because we can reach the same result using different syntaxes. This may look confusing at first, but it’s a good feature. You are not obliged to find the solution to achieve a particular shape. You only need to find one solution that works for you out of the many possibilities out there.

Here are four ways to create the outer square using gradients:

There are even more ways to pull this off, but you get the point.

There is no Best™ approach. Personally, I try to find the one with the smallest and most optimized code. For me, any solution that requires fewer gradients, fewer calculations, and fewer repeated values is the most suitable. Sometimes I choose a more verbose syntax because it gives me more flexibility to change variables and modify things. It comes with experience and practice. The more you play with gradients, the more you know what syntax to use and when.

Let’s get back to our inner transparent border and dig into the hover effect. In case you didn’t notice, there is a cool hover effect that moves that transparent border using a font-size trick. The idea is to define the --d variable with a value of 1em. This variables controls the distance of the border from the edge. We can transform like this:

--_d: calc(var(--d) + var(--s) * 1em)

…giving us the following updated CSS:

img {
  --b: 5px;  /* the border thickness */
  --d: 20px; /* the distance from the edge */
  --o: 15px; /* the offset on hover */
  --s: 1;    /* the direction of the hover effect (+1 or -1)*/

  --_d: calc(var(--d) + var(--s) * 1em);
  --_g: calc(100% - 2 * (var(--_d) + var(--b)));
  mask:
    conic-gradient(from 90deg at var(--_d) var(--_d), #0000 25%, #000 0)
     0 0 / calc(100% - var(--_d)) calc(100% - var(--_d)),
    linear-gradient(#000 0 0) 50% / var(--_g) var(--_g) no-repeat;
  font-size: 0;
  transition: .35s;
}
img:hover {
  font-size: var(--o);
}

The font-size is initially equal to 0 ,so 1em is also equal to 0 and --_d is be equal to --d. On hover, though, the font-size is equal to a value defined by an --o variable that sets the border’s offset. This, in turn, updates the --_d variable, moving the border by the offset. Then I add another variable, --s, to control the sign that decides whether the border moves to the inside or the outside.

The font-size trick is really useful if we want to animate properties that are otherwise unanimatable. Custom properties defined with @property can solve this but support for it is still lacking at the time I’m writing this.

The Frame Reveal

We made the following reveal animation in the first part of this series:

We can take the same idea, but instead of a border with a solid color we will use a gradient like this:

If you compare both codes you will notice the following changes:

  1. I used the same gradient configuration from the first example inside the mask property. I simply moved the gradients from the background property to the mask property.
  2. I added a repeating-linear-gradient() to create the gradient border.

That’s it! I re-used most of the same code we already saw — with super small tweaks — and got another cool image decoration with a hover effect.

/* Solid color border */

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

  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;
}
/* Gradient color border */

img {
  --b: 10px; /* the border thickness*/
  --g: 5px;  /* the gap on hover */
  background: repeating-linear-gradient(135deg, #F8CA00 0 10px, #E97F02 0 20px, #BD1550 0 30px);

  padding: calc(var(--g) + var(--b));
  --_g: #0000 25%, #000 0;
  mask: 
    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,
    linear-gradient(#000 0 0) content-box;
  transition: .3s, mask-position .3s .3s;
  cursor: pointer;
}
img:hover {
  --_i: 100%;
  transition: .3s, mask-size .3s .3s;
}

Let’s try another frame animation. This one is a bit tricky as it has a three-step animation:

The first step of the animation is to make the bottom edge bigger. For this, we adjust the background-size of a linear-gradient():

You are probably wondering why I am also adding the top edge. We need it for the third step. I always try to optimize the code I write, so I am using one gradient to cover both the top and bottom sides, but the top one is hidden and revealed later with a mask.

For the second step, we add a second gradient to show the left and right edges. But this time, we do it using background-position:

We can stop here as we already have a nice effect with two gradients but we are here to push the limits so let’s add a touch of mask to achieve the third step.

The trick is to make the top edge hidden until we show the bottom and the sides and then we update the mask-size (or mask-position) to show the top part. As I said previously, we can find a lot of gradient configurations to achieve the same effect.

Here is an illustration of the gradients I will be using:

I am using two conic gradients having a width equal to 200%. Both gradients cover the area leaving only the top part uncovered (that part will be invisible later). On hover, I slide both gradients to cover that part.

Here is a better illustration of one of the gradients to give you a better idea of what’s happening:

Now we put this inside the mask property and we are done! Here is the full code:

img {
  --b: 6px;  /* the border thickness*/
  --g: 10px; /* the gap */
  --c: #0E8D94;

  padding: calc(var(--b) + var(--g));
  --_l: var(--c) var(--b), #0000 0 calc(100% - var(--b)), var(--c) 0;
  background:
    linear-gradient(var(--_l)) 50%/calc(100% - var(--_i,80%)) 100% no-repeat,
    linear-gradient(90deg, var(--_l)) 50% var(--_i,-100%)/100% 200% no-repeat;  
  mask:
    conic-gradient(at 50% var(--b),#0000 25%, #000 0) calc(50% + var(--_i, 50%)) / 200%,
    conic-gradient(at 50% var(--b),#000 75%, #0000 0) calc(50% - var(--_i, 50%)) / 200%;
  transition: 
    .3s calc(.6s - var(--_t,.6s)) mask-position, 
    .3s .3s background-position,
    .3s var(--_t,.6s) background-size,
    .4s transform;
  cursor: pointer;
}
img:hover {
  --_i: 0%;
  --_t: 0s;
  transform: scale(1.2);
}

I have also introduced some variables to optimize the code, but you should be used to this right now.

What about a four-step animation? Yes, it’s possible!

No explanation for this because it’s your homework! Take all that you have learned in this article to dissect the code and try to articulate what it’s doing. The logic is similar to all the previous examples. The key is to isolate each gradient to understand each step of the animation. I kept the code un-optimized to make things a little easier to read. I do have an optimized version if you are interested, but you can also try to optimize the code yourself and compare it with my version for additional practice.

Wrapping up

That’s it for Part 2 of this three-part series on creative image decorations using only the <img> element. We now have a good handle on how gradients and masks can be combined to create awesome visual effects, and even animations — without reaching for extra elements or pseudo-elements. Yes, a single <img> tag is enough!

We have one more article in this series to go. Until then, here is a bonus demo with a cool hover effect where I use mask to assemble a broken image.

Fancy Image Decorations series


Fancy Image Decorations: Masks and Advanced Hover Effects originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/fancy-image-decorations-masks-and-advanced-hover-effects/feed/ 1 374194
CSS Checkerboard Background… But With Rounded Corners and Hover Styles https://css-tricks.com/css-checkerboard-background-but-with-rounded-corners-and-hover-styles/ https://css-tricks.com/css-checkerboard-background-but-with-rounded-corners-and-hover-styles/#comments Tue, 20 Sep 2022 13:19:24 +0000 https://css-tricks.com/?p=373167 On one hand, creating simple checkered backgrounds with CSS is easy. On the other hand, though, unless we are one of the CSS-gradient-ninjas, we are kind of stuck with basic patterns.

At least that’s what I thought while staring at …


CSS Checkerboard Background… But With Rounded Corners and Hover Styles originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
On one hand, creating simple checkered backgrounds with CSS is easy. On the other hand, though, unless we are one of the CSS-gradient-ninjas, we are kind of stuck with basic patterns.

At least that’s what I thought while staring at the checkered background on my screen and trying to round those corners of the squares just a little…until I remembered my favorite bullet point glyph — — and figured that if only I could place it over every intersection in the pattern, I’ll surely get the design I want.

Turns out it’s possible! Here’s the proof.

Let’s start with the basic pattern:

<div></div>
div {
 background: 
  repeating-linear-gradient(
    to right, transparent, 
    transparent 50px, 
    white 50px, 
    white 55px
  ),
  repeating-linear-gradient(
    to bottom, transparent,  
    transparent 50px, 
    white 50px, 
    white 55px
  ),
  linear-gradient(45deg, pink, skyblue);
  /* more styles */
}

What that gives us is a repeating background of squares that go from pink to blue with 5px white gaps between them. Each square is fifty pixels wide and transparent. This is created using repeating-linear-gradient, which creates a linear gradient image where the gradient repeats throughout the containing area.

In other words, the first gradient in that sequence creates white horizontal stripes and the second gradient creates white vertical stripes. Layered together, they form the checkered pattern, and the third gradient fills in the rest of the space.

Now we add the star glyph I mentioned earlier, on top of the background pattern. We can do that by including it on the same background property as the gradients while using an encoded SVG for the shape:

div {
  background: 
    repeat left -17px top -22px/55px 55px
    url("data:image/svg+xml,
    <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 35px 35px'>
      <foreignObject width='35px' height='35px'>
        <div xmlns='http://www.w3.org/1999/xhtml' style='color: white; font-size: 35px'>✦</div>
      </foreignObject>
    </svg>"
    ), 
    repeating-linear-gradient(
      to right, transparent,
      transparent 50px,
      white 50px,
      white 55px
    ),
    repeating-linear-gradient(
      to bottom, transparent,
      transparent 50px,
      white 50px,
      white 55px
    ),
    linear-gradient(45deg, pink, skyblue);
  /* more style */
}

Let’s break that down. The first keyword, repeat, denotes that this is a repeating background image. Followed by that is the position and size of each repeating unit, respectively (left -17px top -22px/55px 55px). This offset position is based on the glyph and pattern’s size. You’ll see below how the glyph size is given. The offset is added to re-position the repeating glyph exactly over each intersection in the checkered pattern.

The SVG has an HTML <div> carrying the glyph. Notice that I declared a font-size on it. That ultimately determines the border radius of the squares in the checkerboard pattern — the bigger the glyph, the more rounded the squares. The unrolled SVG from the data URL looks like this:

<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 35px 35px'>
  <foreignObject width='35px' height='35px'>
    <div xmlns='http://www.w3.org/1999/xhtml' style='color:white;font-size:35px'>✦</div>
  </foreignObject>
</svg>

Now that a CSS pattern is established, let’s add a :hover effect where the glyph is removed and the white lines are made slightly translucent by using rgb() color values with alpha transparency.

div:hover {
  background:
    repeating-linear-gradient(
      to right, transparent,
      transparent 50px,
      rgb(255 255 255 / 0.5) 50px,
      rgb(255 255 255 / 0.5) 55px
    ),
    repeating-linear-gradient(
      to bottom, transparent,
      transparent 50px,
      rgb(255 255 255 / 0.5) 50px,
      rgb(255 255 255 / 0.5) 55px
    ),
  linear-gradient(45deg, pink, skyblue);
  box-shadow: 10px 10px 20px pink;
}

There we go! Now, not only do we have our rounded corners, but we also have more control control over the pattern for effects like this:

Again, this whole exercise was an attempt to get a grid of squares in a checkerboard pattern that supports rounded corners, a background gradient that serves as an overlay across the pattern, and interactive styles. I think this accomplishes the task quite well, but I’m also interested in how you might’ve approached it. Let me know in the comments!


CSS Checkerboard Background… But With Rounded Corners and Hover Styles originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/css-checkerboard-background-but-with-rounded-corners-and-hover-styles/feed/ 4 373167
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
Single Element Loaders: Going 3D! https://css-tricks.com/single-element-loaders-going-3d/ https://css-tricks.com/single-element-loaders-going-3d/#comments Fri, 01 Jul 2022 13:24:09 +0000 https://css-tricks.com/?p=366544 For this fourth and final article of our little series on single-element loaders, we are going to explore 3D patterns. When creating a 3D element, it’s hard to imagine that just one HTML element is enough to simulate something like all six faces of a cube. But  maybe we can get away …


Single Element Loaders: Going 3D! originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
For this fourth and final article of our little series on single-element loaders, we are going to explore 3D patterns. When creating a 3D element, it’s hard to imagine that just one HTML element is enough to simulate something like all six faces of a cube. But  maybe we can get away with something more cube-like instead by showing only the front three sides of the shape — it’s totally possible and that’s what we’re going to do together.

Article series

The split cube loader

Here is a 3D loader where a cube is split into two parts, but is only made with only a single element:

Each half of the cube is made using a pseudo-element:

Cool, right?! We can use a conic gradient with CSS clip-path on the element’s ::before and ::after pseudos to simulate the three visible faces of a 3D cube. Negative margin is what pulls the two pseudos together to overlap and simulate a full cube. The rest of our work is mostly animating those two halves to get neat-looking loaders!

Let’s check out a visual that explains the math behind the clip-path points used to create this cube-like element:

We have our variables and an equation, so let’s put those to work. First, we’ll establish our variables and set the sizing for the main .loader element:

.loader {
  --s: 150px; /* control the size */
  --_d: calc(0.353 * var(--s)); /* 0.353 = sin(45deg)/2 */

  width: calc(var(--s) + var(--_d)); 
  aspect-ratio: 1;
  display: flex;
}

Nothing too crazy so far. We have a 150px square that’s set up as a flexible container. Now we establish our pseudos:

.loader::before,
.loader::after {
  content: "";
  flex: 1;
}

Those are two halves in the .loader container. We need to paint them in, so that’s where our conic gradient kicks in:

.loader::before,
.loader::after {
  content: "";
  flex: 1;
  background:
    conic-gradient(from -90deg at calc(100% - var(--_d)) var(--_d),
    #fff 135deg, #666 0 270deg, #aaa 0);
}

The gradient is there, but it looks weird. We need to clip it to the element:

.loader::before,
.loader::after {
  content: "";
  flex: 1;
  background:
    conic-gradient(from -90deg at calc(100% - var(--_d)) var(--_d),
    #fff 135deg, #666 0 270deg, #aaa 0);
  clip-path:
    polygon(var(--_d) 0, 100% 0, 100% calc(100% - var(--_d)), calc(100% - var(--_d)) 100%, 0 100%, 0 var(--_d));
}

Let’s make sure the two halves overlap with a negative margin:

.loader::before {
  margin-right: calc(var(--_d) / -2);
}

.loader::after {
  margin-left: calc(var(--_d) / -2);
}

Now let’s make ‘em move!

.loader::before,
.loader::after {
  /* same as before */
  animation: load 1.5s infinite cubic-bezier(0, .5, .5, 1.8) alternate;
}

.loader::after {
  /* same as before */
  animation-delay: -.75s
}

@keyframes load{
  0%, 40%   { transform: translateY(calc(var(--s) / -4)) }
  60%, 100% { transform: translateY(calc(var(--s) / 4)) }
}

Here’s the final demo once again:

The progress cube loader

Let’s use the same technique to create a 3D progress loader. Yes, still only one element!

We’re not changing a thing as far as simulating the cube the same way we did before, other than changing the loader’s height and aspect ratio. The animation we’re making relies on a surprisingly easy technique where we update the width of the left side while the right side fills the remaining space, thanks to flex-grow: 1.

The first step is to add some transparency to the right side using opacity:

This simulates the effect that one side of the cube is filled in while the other is empty. Then we update the color of the left side. To do that, we either update the three colors inside the conic gradient or we do it by adding a background color with a background-blend-mode:

.loader::before {
  background-color: #CC333F; /* control the color here */
  background-blend-mode: multiply;
}

This trick only allows us to update the color only once. The right side of the loader blends in with the three shades of white from the conic gradient to create three new shades of our color, even though we’re only using one color value. Color trickery!

Let’s animate the width of the loader’s left side:

Oops, the animation is a bit strange at the beginning! Notice how it sort of starts outside of the cube? This is because we’re starting the animation at the 0% width. But due to the clip-path and negative margin we’re using, what we need to do instead is start from our --_d variable, which we used to define the clip-path points and the negative margin:

@keyframes load {
  0%,
  5% {width: var(--_d); }
  95%,
  100% {width: 100%; }
}

That’s a little better:

But we can make this animation even smoother. Did you notice we’re missing a little something? Let me show you a screenshot to compare what the final demo should look like with that last demo:

It’s the bottom face of the cube! Since the second element is transparent, we need to see the bottom face of that rectangle as you can see in the left example. It’s subtle, but should be there!

We can add a gradient to the main element and clip it like we did with the pseudos:

background: linear-gradient(#fff1 0 0) bottom / 100% var(--_d) no-repeat;

Here’s the full code once everything is pulled together:

.loader {
  --s: 100px; /* control the size */
  --_d: calc(0.353*var(--s)); /* 0.353 = sin(45deg) / 2 */

  height: var(--s); 
  aspect-ratio: 3;
  display: flex;
  background: linear-gradient(#fff1 0 0) bottom / 100% var(--_d) no-repeat;
  clip-path: polygon(var(--_d) 0, 100% 0, 100% calc(100% - var(--_d)), calc(100% - var(--_d)) 100%, 0 100%, 0 var(--_d));
}
.loader::before,
.loader::after {
  content: "";
  clip-path: inherit;
  background:
    conic-gradient(from -90deg at calc(100% - var(--_d)) var(--_d),
     #fff 135deg, #666 0 270deg, #aaa 0);
}
.loader::before {
  background-color: #CC333F; /* control the color here */
  background-blend-mode: multiply;
  margin-right: calc(var(--_d) / -2);
  animation: load 2.5s infinite linear;
}
.loader:after {
  flex: 1;
  margin-left: calc(var(--_d) / -2);
  opacity: 0.4;
}

@keyframes load {
  0%,
  5% { width: var(--_d); }
  95%,
  100% { width: 100%; }
}

That’s it! We just used a clever technique that uses pseudo-elements, conic gradients, clipping, background blending, and negative margins to get, not one, but two sweet-looking 3D loaders with nothing more than a single element in the markup.

More 3D

We can still go further and simulate an infinite number of 3D cubes using one element — yes, it’s possible! Here’s a grid of cubes:

This demo and the following demos are unsupported in Safari at the time of writing.

Crazy, right? Now we’re creating a repeated pattern of cubes made using a single element… and no pseudos either! I won’t go into fine detail about the math we are using (there are very specific numbers in there) but here is a figure to visualize how we got here:

We first use a conic-gradient to create the repeating cube pattern. The repetition of the pattern is controlled by three variables:

  • --size: True to its name, this controls the size of each cube.
  • --m: This represents the number of columns.
  • --n: This is the number of rows.
  • --gap: this the gap or distance between the cubes
.cube {
  --size: 40px; 
  --m: 4; 
  --n: 5;
  --gap :10px;

  aspect-ratio: var(--m) / var(--n);
  width: calc(var(--m) * (1.353 * var(--size) + var(--gap)));
  background:
    conic-gradient(from -90deg at var(--size) calc(0.353 * var(--size)),
      #249FAB 135deg, #81C5A3 0 270deg, #26609D 0) /* update the colors here */
    0 0 / calc(100% / var(--m)) calc(100% / var(--n));
}

Then we apply a mask layer using another pattern having the same size. This is the trickiest part of this idea. Using a combination of a linear-gradient and a conic-gradient we will cut a few parts of our element to keep only the cube shapes visible.

.cube {
  /* etc. */
  mask: 
    linear-gradient(to bottom right,
       #0000 calc(0.25 * var(--size)),
       #000 0 calc(100% - calc(0.25 * var(--size)) - 1.414 * var(--gap)),
       #0000 0),
    conic-gradient(from -90deg at right var(--gap) bottom var(--gap), #000 90deg, #0000 0);  
  mask-size: calc(100% / var(--m)) calc(100% / var(--n));
  mask-composite: intersect;
}

The code may look a bit complex but thanks to CSS variables all we need to do is to update a few values to control our matrix of cubes. Need a 10⨉10 grid? Update the --m and --n variables to 10. Need a wider gap between cubes? Update the --gap value. The color values are only used once, so update those for a new color palette!

Now that we have another 3D technique, let’s use it to build variations of the loader by playing around with different animations. For example, how about a repeating pattern of cubes sliding infinitely from left to right?

This loader defines four cubes in a single row. That means our --n value is 4 and --m is equal to 1 . In other words, we no longer need these!

Instead, we can work with the --size and --gap variables in a grid container:

.loader {
  --size: 70px;
  --gap: 15px;  

  width: calc(3 * (1.353 * var(--size) + var(--gap)));
  display: grid;
  aspect-ratio: 3;
}

This is our container. We have four cubes, but only want to show three in the container at a time so that we always have one sliding in as one is sliding out. That’s why we are factoring the width by 3 and have the aspect ratio set to 3 as well.

Let’s make sure that our cube pattern is set up for the width of four cubes. We’re going to do this on the container’s ::before pseudo-element:

.loader::before { 
  content: "";
  width: calc(4 * 100% / 3);
  /*
     Code to create four cubes
  */
}

Now that we have four cubes in a three-cube container, we can justify the cube pattern to the end of the grid container to overflow it, showing the last three cubes:

.loader {
  /* same as before */
  justify-content: end;
}

Here’s what we have so far, with a red outline to show the bounds of the grid container:

Now all we have to do is to move the pseudo-element to the right by adding our animation:

@keyframes load {
  to { transform: translate(calc(100% / 4)); }
}

Did you get the trick of the animation? Let’s finish this off by hiding the overflowing cube pattern and by adding a touch of masking to create that fading effect that the start and the end:

.loader {
  --size: 70px;
  --gap: 15px;  
  
  width: calc(3*(1.353*var(--s) + var(--g)));
  display: grid;
  justify-items: end;
  aspect-ratio: 3;
  overflow: hidden;
  mask: linear-gradient(90deg, #0000, #000 30px calc(100% - 30px), #0000);
}

We can make this a lot more flexible by introducing a variable, --n, to set how many cubes are displayed in the container at once. And since the total number of cubes in the pattern should be one more than --n, we can express that as calc(var(--n) + 1).

Here’s the full thing:

OK, one more 3D loader that’s similar but has the cubes changing color in succession instead of sliding:

We’re going to rely on an animated background with background-blend-mode for this one:

.loader {
  /* ... */
  background:
    linear-gradient(#ff1818 0 0) 0% / calc(100% / 3) 100% no-repeat,
    /* ... */;
  background-blend-mode: multiply;
  /* ... */
  animation: load steps(3) 1.5s infinite;
}
@keyframes load {
  to { background-position: 150%; }
}

I’ve removed the superfluous code used to create the same layout as the last example, but with three cubes instead of four. What I am adding here is a gradient defined with a specific color that blends with the conic gradient, just as we did earlier for the progress bar 3D loader.

From there, it’s animating the background gradient’s background-position as a three-step animation to make the cubes blink colors one at a time.

If you are not familiar with the values I am using for background-position and the background syntax, I highly recommend one of my previous articles and one of my Stack Overflow answers. You will find a very detailed explanation there.

Can we update the number of cubes to make it variables?

Yes, I do have a solution for that, but I’d like you to take a crack at it rather than embedding it here. Take what we have learned from the previous example and try to do the same with this one — then share your work in the comments!

Variations galore!

Like the other three articles in this series, I’d like to leave you with some inspiration to go forth and create your own loaders. Here is a collection that includes the 3D loaders we made together, plus a few others to get your imagination going:

That’s a wrap

I sure do hope you enjoyed spending time making single element loaders with me these past few weeks. It’s crazy that we started with seemingly simple spinner and then gradually added new pieces to work ourselves all the way up to 3D techniques that still only use a single element in the markup. This is exactly what CSS looks like when we harness its powers: scalable, flexible, and reusable.

Thanks again for reading this little series! I’ll sign off by reminding you that I have a collection of more than 500 loaders if you’re looking for more ideas and inspiration.

Article series


Single Element Loaders: Going 3D! originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/single-element-loaders-going-3d/feed/ 2 366544
Single Element Loaders: The Bars https://css-tricks.com/single-element-loaders-the-bars/ https://css-tricks.com/single-element-loaders-the-bars/#comments Fri, 24 Jun 2022 20:00:29 +0000 https://css-tricks.com/?p=366526 We’ve looked at spinners. We’ve looked at dots. Now we’re going to tackle another common pattern for loaders: bars. And we’re going to do the same thing in this third article of the series as we have the others …


Single Element Loaders: The Bars originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
We’ve looked at spinners. We’ve looked at dots. Now we’re going to tackle another common pattern for loaders: bars. And we’re going to do the same thing in this third article of the series as we have the others by making it with only one element and with flexible CSS that makes it easy to create variations.

Article series

Let’s start with not one, not two, but 20 examples of bar loaders.

What?! Are you going to detail each one of them? That’s too much for an article!

It might seem like that at first glance! But all of them rely on the same code structure and we only update a few values to create variations. That’s all the power of CSS. We don’t learn how to create one loader, but we learn different techniques that allow us to create as much loader as we want using merely the same code structure.

Let’s make some bars!

We start by defining the dimensions for them using width (or height) with aspect-ratio to maintain proportion:

.bars {
  width: 45px;
  aspect-ratio: 1;
}

We sort of “fake” three bars with a linear gradient on the background — very similar to how we created dot loaders in Part 2 of this series.

.bars {
  width: 45px;
  aspect-ratio: 1;
  --c: no-repeat linear-gradient(#000 0 0); /* we define the color here */
  background: 
    var(--c) 0%   50%,
    var(--c) 50%  50%,
    var(--c) 100% 50%;
  background-size: 20% 100%; /* 20% * (3 bars + 2 spaces) = 100% */
}

The above code will give us the following result:

Like the other articles in this series, we are going to deal with a lot of background trickery. So, if you ever feel like we’re jumping around too fast or feel you need a little more detail, please do check those out. You can also read my Stack Overflow answer where I give a detailed explanation on how all this works.

Animating the bars

We either animate the element’s size or position to create the bar loader. Let’s animate the size by defining the following animation keyframes:

@keyframes load {
  0%   { background-size: 20% 100%, 20% 100%, 20% 100%; }  /* 1 */
  33%  { background-size: 20% 10% , 20% 100%, 20% 100%; }  /* 2 */
  50%  { background-size: 20% 100%, 20% 10% , 20% 100%; }  /* 3 */
  66%  { background-size: 20% 100%, 20% 100%, 20% 10%;  }  /* 4 */
  100% { background-size: 20% 100%, 20% 100%, 20% 100%; }  /* 5 */
}

See what’s happening there? Between 0% and 100%, the animation changes the background-size of the element’s background gradient. Each keyframe sets three background sizes (one for each gradient).

And here’s what we get:

Can you start to imagine all the possible variations we can get by playing with different animation configurations for the sizes or the positions?

Let’s fix the size to 20% 50% and update the positions this time:

.loader {
  width: 45px;
  aspect-ratio: .75;
  --c: no-repeat linear-gradient(#000 0 0);
  background: 
    var(--c),
    var(--c),
    var(--c);
  background-size: 20% 50%;
  animation: load 1s infinite linear;
}
@keyframes load {
  0%   { background-position: 0% 100%, 50% 100%, 100% 100%; } /* 1 */
  20%  { background-position: 0% 50% , 50% 100%, 100% 100%; } /* 2 */
  40%  { background-position: 0% 0%  , 50% 50% , 100% 100%; } /* 3 */
  60%  { background-position: 0% 100%, 50% 0%  , 100% 50%;  } /* 4 */
  80%  { background-position: 0% 100%, 50% 100%, 100% 0%;   } /* 5 */ 
  100% { background-position: 0% 100%, 50% 100%, 100% 100%; } /* 6 */
}

…which gets us another loader!

You’ve probably got the trick by now. All you need is to define a timeline that you translate into a keyframe. By animating the size, the position — or both! — there’s an infinite number of loader possibilities at our fingertips.

And once we get comfortable with such a technique we can go further and use a more complex gradient to create even more loaders.

Expect for the last two examples in that demo, all of the bar loaders use the same underlying markup and styles and different combinations of animations. Open the code and try to visualize each frame independently; you’ll see how relatively trivial it is to make dozens — if not hundreds — of variations.

Getting fancy

Did you remember the mask trick we did with the dot loaders in the second article of this series? We can do the same here!

If we apply all the above logic inside the mask property we can use any background configuration to add a fancy coloration to our loaders.

Let’s take one demo and update it:

All I did is updating all the background-* with mask-* and I added a gradient coloration. As simple as that and yet we get another cool loader.

So there is no difference between the dots and the bars?

No difference! I wrote two different articles to cover as many examples as possible but in both, I am relying on the same techniques:

  1. Gradients to create the shapes (dots or bars or maybe something else)
  2. Animating background-size and/or background-position to create the loader animation
  3. Adding mask to add a touch of colors

Rounding the bars

Let’s try something different this time where we can round the edges of our bars.

Using one element and its ::before and ::after pseudos, we define three identical bars:

.loader {
  --s: 100px; /* control the size */

  display: grid;
  place-items: center;
  place-content: center;
  margin: 0 calc(var(--s) / 2); /* 50px */
}
.loader::before,
.loader::after {
  content: "";
  grid-area: 1/1;
}
.loader,
.loader::before,
.loader::after {
  height: var(--s);
  width: calc(var(--s) / 5); /* 20px */
  border-radius: var(--s);
  transform: translate(calc(var(--_i, 0) * 200%));
}
.loader::before { --_i: -1; }
.loader::after { --_i:  1; }

That gives us three bars, this time without relying on a linear gradient:

Now the trick is to fill in those bars with a lovely gradient. To simulate a continuous gradient, we need to play with background properties. In the above figure, the green area defines the area covered by the loader. That area should be the size of the gradient and, if we do the math, it’s equal to multiplying both sides labeled S in the diagram, or background-size: var(--s) var(--s).

Since our elements are individually placed, we need to update the position of the gradient inside each one to make sure all of them overlap. This way, we’re simulating one continuous gradient even though it’s really three of them.

For the main element (placed at the center), the background needs to be at the center. We use the following:

.loader {
  /* etc. */
  background: linear-gradient() 50% / var(--s) var(--s);
}

For the pseudo-element on the left, we need the background on the left

.loader::before {
  /* etc. */
  background: linear-gradient() 0% / var(--s) var(--s);
}

And for the pseudo on the right, the background needs to be positioned to the right:

.loader::after {
  background: linear-gradient() 100% / var(--s) var(--s);
}

Using the same CSS variable, --_i, that we used for the translate, we can write the code like this:

.loader {
  --s: 100px; /* control the size */
  --c: linear-gradient(/* etc. */); /* control the coloration */

  display: grid;
  place-items: center;
  place-content: center;
}
.loader::before,
.loader::after{
  content: "";
  grid-area: 1/1;
}
.loader,
.loader::before,
.loader::after{
  height: var(--s);
  width: calc(var(--s) / 5);
  border-radius: var(--s);
  background: var(--c) calc(50% + var(--_i, 0) * 50%) / var(--s) var(--s);
  transform: translate(calc(var(--_i, 0) * 200%));
}
.loader::before { --_i: -1; }
.loader::after  { --_i:  1; }

Now, all we have to do is to animate the height and add some delays! Here are three examples where all that’s different are the colors and sizes:

Wrapping up

I hope so far you are feeling super encouraged by all the powers you have to make complex-looking loading animations. All we need is one element, either gradients or pseudos to draw the bars, then some keyframes to move things around. That’s the entire recipe for getting an endless number of possibilities, so go out and starting cooking up some neat stuff!

Until the next article, I will leave you with a funny collection of loaders where I am combining the dots and the bars!

Article series


Single Element Loaders: The Bars originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/single-element-loaders-the-bars/feed/ 8 366526
Single Element Loaders: The Dots https://css-tricks.com/single-element-loaders-the-dots/ https://css-tricks.com/single-element-loaders-the-dots/#comments Fri, 17 Jun 2022 14:47:55 +0000 https://css-tricks.com/?p=366342 We’re looking at loaders in this series. More than that, we’re breaking down some common loader patterns and how to re-create them with nothing more than a single div. So far, we’ve picked apart the classic spinning loader. Now, …


Single Element Loaders: The Dots originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
We’re looking at loaders in this series. More than that, we’re breaking down some common loader patterns and how to re-create them with nothing more than a single div. So far, we’ve picked apart the classic spinning loader. Now, let’s look at another one you’re likely well aware of: the dots.

Dot loaders are all over the place. They’re neat because they usually consist of three dots that sort of look like a text ellipsis (…) that dances around.

Article series

Our goal here is to make this same thing out of a single div element. In other words, there is no one div per dot or individual animations for each dot.

That example of a loader up above is made with a single div element, a few CSS declarations, and no pseudo-elements. I am combining two techniques using CSS background and mask. And when we’re done, we’ll see how animating a background gradient helps create the illusion of each dot changing colors as they move up and down in succession.

The background animation

Let’s start with the background animation:

.loader {
  width: 180px; /* this controls the size */
  aspect-ratio: 8/5; /* maintain the scale */
  background: 
    conic-gradient(red   50%, blue   0) no-repeat, /* top colors */
    conic-gradient(green 50%, purple 0) no-repeat; /* bottom colors */
  background-size: 200% 50%; 
  animation: back 4s infinite linear; /* applies the animation */
}

/* define the animation */
@keyframes back {
  0%,                       /* X   Y , X     Y */
  100% { background-position: 0%   0%, 0%   100%; }
  25%  { background-position: 100% 0%, 0%   100%; }
  50%  { background-position: 100% 0%, 100% 100%; }
  75%  { background-position: 0%   0%, 100% 100%; }
}

I hope this looks pretty straightforward. What we’ve got is a 180px-wide .loader element that shows two conic gradients sporting hard color stops between two colors each — the first gradient is red and blue along the top half of the .loader, and the second gradient is green and purple along the bottom half.

The way the loader’s background is sized (200% wide), we only see one of those colors in each half at a time. Then we have this little animation that pushes the position of those background gradients left, right, and back again forever and ever.

When dealing with background properties — especially background-position — I always refer to my Stack Overflow answer where I am giving a detailed explanation on how all this works. If you are uncomfortable with CSS background trickery, I highly recommend reading that answer to help with what comes next.

In the animation, notice that the first layer is Y=0% (placed at the top) while X is changes from 0% to 100%. For the second layer, we have the same for X but Y=100% (placed at the bottom).

Why using a conic-gradient() instead of linear-gradient()?

Good question! Intuitively, we should use a linear gradient to create a two-color gradients like this:

linear-gradient(90deg, red 50%, blue 0)

But we can also reach for the same using a conic-gradient() — and with less of code. We reduce the code and also learn a new trick in the process!

Sliding the colors left and right is a nice way to make it look like we’re changing colors, but it might be better if we instantly change colors instead — that way, there’s no chance of a loader dot flashing two colors at the same time. To do this, let’s change the animation‘s timing function from linear to steps(1)

The loader dots

If you followed along with the first article in this series, I bet you know what comes next: CSS masks! What makes masks so great is that they let us sort of “cut out” parts of a background in the shape of another element. So, in this case, we want to make a few dots, show the background gradients through the dots, and cut out any parts of the background that are not part of a dot.

We are going to use radial-gradient() for this:

.loader {
  width: 180px;
  aspect-ratio: 8/5;
  mask:
    radial-gradient(#000 68%, #0000 71%) no-repeat,
    radial-gradient(#000 68%, #0000 71%) no-repeat,
    radial-gradient(#000 68%, #0000 71%) no-repeat;
  mask-size: 25% 40%; /* the size of our dots */
}

There’s some duplicated code in there, so let’s make a CSS variable to slim things down:

.loader {
  width: 180px;
  aspect-ratio: 8/5;
  --_g: radial-gradient(#000 68%, #0000 71%) no-repeat;
  mask: var(--_g),var(--_g),var(--_g);
  mask-size: 25% 40%;
}

Cool cool. But now we need a new animation that helps move the dots up and down between the animated gradients.

.loader {
  /* same as before */
  animation: load 2s infinite;
}

@keyframes load {      /* X  Y,     X   Y,    X   Y */
  0%     { mask-position: 0% 0%  , 50% 0%  , 100% 0%; } /* all of them at the top */
  16.67% { mask-position: 0% 100%, 50% 0%  , 100% 0%; }
  33.33% { mask-position: 0% 100%, 50% 100%, 100% 0%; }
  50%    { mask-position: 0% 100%, 50% 100%, 100% 100%; } /* all of them at the bottom */
  66.67% { mask-position: 0% 0%  , 50% 100%, 100% 100%; }
  83.33% { mask-position: 0% 0%  , 50% 0%  , 100% 100%; }
  100%   { mask-position: 0% 0%  , 50% 0%  , 100% 0%; } /* all of them at the top */
}

Yes, that’s a total of three radial gradients in there, all with the same configuration and the same size — the animation will update the position of each one. Note that the X coordinate of each dot is fixed. The mask-position is defined such that the first dot is at the left (0%), the second one at the center (50%), and the third one at the right (100%). We only update the Y coordinate from 0% to 100% to make the dots dance.

Dot loader dots with labels showing their changing positions.

Here’s what we get:

Now, combine this with our gradient animation and magic starts to happen:

Dot loader variations

The CSS variable we made in the last example makes it all that much easier to swap in new colors and create more variations of the same loader. For example, different colors and sizes:

What about another movement for our dots?

Here, all I did was update the animation to consider different positions, and we get another loader with the same code structure!

The animation technique I used for the mask layers can also be used with background layers to create a lot of different loaders with a single color. I wrote a detailed article about this. You will see that from the same code structure we can create different variations by simply changing a few values. I am sharing a few examples at the end of the article.

Why not a loader with one dot?

This one should be fairly easy to grok as I am using the same technique but with a more simple logic:

Here is another example of loader where I am also animating radial-gradient combined with CSS filters and mix-blend-mode to create a blobby effect:

If you check the code, you will see that all I am really doing there is animating the background-position, exactly like we did with the previous loader, but adding a dash of background-size to make it look like the blob gets bigger as it absorbs dots.

If you want to understand the magic behind that blob effect, you can refer to these interactive slides (Chrome only) by Ana Tudor because she covers the topic so well!

Here is another dot loader idea, this time using a different technique:

This one is only 10 CSS declarations and a keyframe. The main element and its two pseudo-elements have the same background configuration with one radial gradient. Each one creates one dot, for a total of three. The animation moves the gradient from top to bottom by using different delays for each dot..

Oh, and take note how this demo uses CSS Grid. This allows us to leverage the grid’s default stretch alignment so that both pseudo-elements cover the whole area of their parent. No need for sizing! Push the around a little with translate() and we’re all set.

More examples!

Just to drive the point home, I want to leave you with a bunch of additional examples that are really variations of what we’ve looked at. As you view the demos, you’ll see that the approaches we’ve covered here are super flexible and open up tons of design possibilities.

Next up…

OK, so we covered dot loaders in this article and spinners in the last one. In the next article of this four-part series, we’ll turn our attention to another common type of loader: the bars. We’ll take a lot of what we learned so far and see how we can extend them to create yet another single element loader with as little code and as much flexibility as possible.

Article series


Single Element Loaders: The Dots originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/single-element-loaders-the-dots/feed/ 5 366342
Single Element Loaders: The Spinner https://css-tricks.com/single-element-loaders-the-spinner/ https://css-tricks.com/single-element-loaders-the-spinner/#comments Fri, 10 Jun 2022 14:26:06 +0000 https://css-tricks.com/?p=366266 Making CSS-only loaders is one of my favorite tasks. It’s always satisfying to look at those infinite animations. And, of course, there are lots of techniques and approaches to make them — no need to look further than CodePen to …


Single Element Loaders: The Spinner originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Making CSS-only loaders is one of my favorite tasks. It’s always satisfying to look at those infinite animations. And, of course, there are lots of techniques and approaches to make them — no need to look further than CodePen to see just how many. In this article, though, we will see how to make a single element loader writing as little code as possible.

I have made a collection of more than 500 single div loaders and in this four-part series, I am going to share the tricks I used to create many of them. We will cover a huge number of examples, showing how small adjustments can lead to fun variations, and how little code we need to write to make it all happen!

Single-Element Loaders series:

  1. Single Element Loaders: The Spinner — you are here
  2. Single Element Loaders: The Dots
  3. Single Element Loaders: The Bars
  4. Single Element Loaders: Going 3D

For this first article, we are going to create a one of the more common loader patterns: spinning bars:

Here’s the approach

A trivial implementation for this loader is to create one element for each bar wrapped inside a parent element (for nine total elements), then play with opacity and transform to get the spinning effect.

My implementation, though, requires only one element:

<div class="loader"></div>

…and 10 CSS declarations:

.loader {
  width: 150px; /* control the size */
  aspect-ratio: 1;
  display: grid;
  mask: conic-gradient(from 22deg, #0003, #000);
  animation: load 1s steps(8) infinite;
}
.loader,
.loader:before {
  --_g: linear-gradient(#17177c 0 0) 50%; /* update the color here */
  background: 
    var(--_g)/34% 8%  space no-repeat,
    var(--_g)/8%  34% no-repeat space;
}
.loader:before {
  content: "";
  transform: rotate(45deg);
}
@keyframes load {
  to { transform: rotate(1turn); }
}

Let’s break that down

At first glance, the code may look strange but you will see that it’s more simple than what you might think. The first step is to define the dimension of the element. In our case, it’s a 150px square. We can put aspect-ratio to use so the element stays square no matter what.

.loader {
  width: 150px; /* control the size */
  aspect-ratio: 1; /* make height equal to width */
}

When building CSS loaders, I always try to have one value for controlling the overall size. In this case, it’s the width and all the calculations we cover will refer to that value. This allows me to change a single value to control the loader. It’s always important to be able to easily adjust the size of our loaders without the need to adjust a lot of additional values.

Next, we will use gradients to create the bars. This is the trickiest part! Let’s use one gradient to create two bars like the below:

background: linear-gradient(#17177c 0 0) 50%/34% 8% space no-repeat;
Showing a space between two gradient lines for a single element loader.

Our gradient is defined with one color and two color stops. The result is a solid color with no fading or transitions. The size is equal to 34% wide and 8% tall. It’s also placed in the center (50%). The trick is the use of the keyword value space — this duplicates the gradient, giving us two total bars.

From the specification:

The image is repeated as often as will fit within the background positioning area without being clipped and then the images are spaced out to fill the area. The first and last images touch the edges of the area.

I am using a width equal to 34% which means we cannot have more than two bars (3*34% is greater than 100%) but with two bars we will have empty spaces (100% - 2 * 34% = 32%). That space is placed in the center between the two bars. In other words, we use a width for the gradient that is between 33% and 50% to make sure we have at least two bars with a little bit of space between them. The value space is what correctly places them for us.

We do the same and make a second similar gradient to get two more bars at the top and bottom, which give us a background property value of:

background: 
 linear-gradient(#17177c 0 0) 50%/34% 8%  space no-repeat,
 linear-gradient(#17177c 0 0) 50%/8%  34% no-repeat space;

We can optimize that using a CSS variable to avoid repetition:

--_g: linear-gradient(#17177c 0 0) 50%; /* update the color here */
background: 
 var(--_g)/34% 8%  space no-repeat,
 var(--_g)/8%  34% no-repeat space;

So, now we have four bars and, thanks to CSS variables, we can write the color value once which makes it easy to update later (like we did with the size of the loader).

To create the remaining bars, let’s tap into the .loader element and its ::before pseudo-element to get four more bars for a grand total of eight in all.

.loader {
  width: 150px; /* control the size */
  aspect-ratio: 1;
  display: grid;
}
.loader,
.loader::before {
  --_g: linear-gradient(#17177c 0 0) 50%; /* update the color here */
  background: 
    var(--_g)/34% 8%  space no-repeat,
    var(--_g)/8%  34% no-repeat space;
}
.loader::before {
  content: "";
  transform: rotate(45deg);
}

Note the use of display: grid. This allows us to rely on the grid’s default stretch alignment to make the pseudo-element cover the whole area of its parent; thus there’s no need to specify a dimension on it — another trick that reduces the code and avoid us to deal with a lot of values!

Now let’s rotate the pseudo-element by 45deg to position the remaining bars. Hover the following demo to see the trick:

Setting opacity

What we’re trying to do is create the impression that there is one bar that leaves a trail of fading bars behind it as it travels a circular path. What we need now is to play with the transparency of our bars to make that trail, which we are going to do with CSS mask combined with a conic-gradient as follows:

mask: conic-gradient(from 22deg,#0003,#000);

To better see the trick, let’s apply this to a full-colored box:

The transparency of the red color is gradually increasing clockwise. We apply this to our loader and we have the bars with different opacity:

Radial gradient plus, spinner bars equals spinner bars with gradients.

In reality, each bar appears to fade because it’s masked by a gradient and falls between two semi-transparent colors. It’s hardly noticeable when this runs, so it’s sort of like being able to say that all the bars have the same color with a different level of opacity.

The rotation

Let’s apply a rotation animation to get our loader. Note, that we need a stepped animation and not a continuous one that’s why I am using steps(8). 8 is nothing but the number of the bars, so that value can be changed depending on how many bars are in use.

.loader {
  animation: load 3s steps(8) infinite;
}

/* Same as before: */
@keyframes load {
  to { transform: rotate(1turn) }
}

That’s it! We have our loader with only one element and a few lines of CSS. We can easily control its size and color by adjusting one value.

Since we only used the ::before pseudo-element, we can add four more bars by using ::after to end with 12 bars in total and almost the same code:

We update the rotation of our pseudo-elements to consider 30deg and 60deg instead of 45deg while using an twelve-step animation, rather than eight. I also decreased the height to 5% instead of 8% to make the bars a little thinner.

Notice, too, that we have grid-area: 1/1 on the pseudo-elements. This allows us to place them in the same area as one another, stacked on top of each other.

Guess what? We can reach for the same loader using another implementation:

Can you figure out the logic behind the code? Here is a hint: the opacity is no longer handled with a CSS mask but inside the gradient and is also using the opacity property.

Why not dots instead?

We can totally do that:

If you check the code, you will see that we’re now working with a radial gradient instead of a linear one. Otherwise, the concept is exactly the same where the mask creates the impression of opacity, but we made the shapes as circles instead of lines.

Below is a figure to illustrate the new gradient configuration:

Showing placement of dots in the single-element loader.

If you’re using Safari, note that the demo may be buggy. That’s because Safari currently lacks support for the at syntax in radial gradients. But we can reconfigure the gradient a bit to overcome that:

.loader,
.loader:before,
.loader:after {
  background:
    radial-gradient(
      circle closest-side,
      currentColor 90%,
      #0000 98%
    ) 
    50% -150%/20% 80% repeat-y,
    radial-gradient(
      circle closest-side,
      currentColor 90%,
      #0000 98%
    ) 
    -150% 50%/80% 20% repeat-x;
}

More loader examples

Here is another idea for a spinner loader similar to the previous one.

For this one, I am only relying on background and mask to create the shape (no pseudo-elements needed). I am also defining the configuration with CSS variables to be able to create a lot of variations from the same code — another example of just the powers of CSS variables. I wrote another article about this technique if you want to more details.

Note that some browsers still rely on a -webkit- prefix for mask-composite with its own set of values, and will not display the spinner in the demo. Here is a way to do it without mast-composite for more browser support.

I have another one for you:

For this one, I am using a background-color to control the color, and use mask and mask-composite to create the final shape:

Different steps for applying a master to a element in the shape of a circle.

Before we end, here are some more spinning loaders I made a while back. I am relying on different techniques but still using gradients, masks, pseudo-element, etc. It could be a good exercise to figure out the logic of each one and learn new tricks at the same time. This said, if you have any question about them, the comment section is down below.

Wrapping up

See, there’s so much we can do in CSS with nothing but a single div, a couple of gradients, pseudo-elements, variables. It seems like we created a whole bunch of different spinning loaders, but they’re all basically the same thing with slight modifications.

This is only the beginning. In this series, we will be looking at more ideas and advanced concepts for creating CSS loaders.

Single-Element Loaders series:

  1. Single Element Loaders: The Spinner — you are here
  2. Single Element Loaders: The Dots
  3. Single Element Loaders: The Bars
  4. Single Element Loaders: Going 3D

Single Element Loaders: The Spinner originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/single-element-loaders-the-spinner/feed/ 4 366266
Add a CSS Lens Flare to Photos for a Bright Touch https://css-tricks.com/add-a-css-lens-flare-to-photos-for-a-bright-touch/ https://css-tricks.com/add-a-css-lens-flare-to-photos-for-a-bright-touch/#comments Tue, 12 Apr 2022 17:10:13 +0000 https://css-tricks.com/?p=364385 I’m a big fan of movies by J.J. Abrams. I enjoy their tight plots, quippy dialog, and of course: anamorphic lens flares. Filmmakers like Abrams use lens flare to add a dash of ‘homemade’ realism to their movies, …


Add a CSS Lens Flare to Photos for a Bright Touch originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I’m a big fan of movies by J.J. Abrams. I enjoy their tight plots, quippy dialog, and of course: anamorphic lens flares. Filmmakers like Abrams use lens flare to add a dash of ‘homemade’ realism to their movies, a technique we can easily recreate in tools like Photoshop, then add to our sites as raster images.

But what if we wanted to apply the same lens flare look without the use of photo editing tools? We can create a CSS lens flare to add a cinematic touch to our gallery images, background photos, or user profiles.

There are different types of flares in photography. The one we’re working with is known as artifacts, as they leave behind little blotches of light that take the shape of a camera’s aperture where the light enters and reflects off the surface of the lens.

A diagram showing how light enters a camera lens at various angles to create a flare.
Source: Wikipedia

Here’s a good example of the sort of lens flare we’re going to make, pulled straight from a J.J. Abrams movie still, naturally:

An example of the CSS lens flare we are making, showing a flare to the right of an actor in a still from the 2009 Star Trek movie
Star Trek (2009)

There are a few parts to the lens flare above. Let’s list them out so we know what we’re aiming for:

  1. The center light source appears as a glowing ball of light.
  2. There are some horizontal elliptical light streaks — rays of light that are distorted and blurred, resulting in elongated ellipses.
  3. Random rays of light shoot off from the center light source at various angles.

We start with the HTML elements below that map to our flare components. There is a central light source and two off-diagonal circular flares, three horizontal lens flares, and three conical ray-like flares.

<div class="lens-center"></div>
    <div class="circle-1"></div>
    <div class="circle-2"></div>
    <div class="left-flare horizontal-flare"></div>
    <div class="right-flare horizontal-flare"></div>
    <div class="full-flare horizontal-flare"></div>
    <div class="conic-1"></div>
    <div class="conic-2"></div>
    <div class="conic-3"></div>
</div>

Lastly, in order for our lens flare to be believably superimposed on an image, its center light source has to be adjustable. This way, we can place it over a believable existing light source on a picture and not overlap with any faces.

The background and light source of a CSS lens flare

Let’s start with a black background and central light source for our CSS lens flare. Most gradients on the web are linear gradients with solid-color transitions, but we can apply alpha channels to them which is actually a nice way to produce a glowing effect. A circular-shaped radial gradient with multiple layers of semi-transparent colors gives us a good camera center effect.

background: radial-gradient(
  closest-side circle at center,
  hsl(4 5% 100% / 100%) 0%,
  hsl(4 5% 100% / 100%) 15%,
  hsl(4 10% 70% / 70%) 30%,
  hsl(4 0% 50% / 30%) 55%,
  hsl(4 0% 10% / 5%) 75%,
  transparent 99
);
filter: blur(4px);

Curious about that HSL syntax? It’s new and appears to be the future direction of defining alpha transparency in all CSS color functions.

Notice we’re using a CSS blur filter in there to make the gradients look a bit closer to layers of diffused light.

Now that we know how to add circular flares, we will also add a larger, diffused flare behind the light source, as well as three additional flares at a 45deg angle from the center, to give the effect a more realistic look.

Setting up horizontal light streaks

Let’s start with horizontal flares. There are a few options we can take, a very elongated ellipse gradient would be the simplest approach. However, I’ve noticed that horizontal lens flares are usually less symmetrical than the ones in my reference photos, so I wanted to make mine a little less symmetrical as well.

Luckily, radial gradients have an optional location argument in CSS. We can create two slightly differently-sized left and right portions of the same horizontal flare, and with slightly different colors. We can also add an opacity filter to make the area where the horizontal flares join the center to make the flare less jarring.

background: radial-gradient(
  closest-side circle at center,
  transparent 50%,
  hsl(4 10% 70% / 40%) 90%,
  transparent 100%
);
filter: blur(5px);

While we are at it, let’s also add a single full elongated elliptical bottom flare three-quarters of the way down the viewport for another touch of “realism.”

Creating the diffused rays of light

With both the radial and horizontal flares in place, all we have left are the angled rays of light shooting off from the light source. We could add additional elliptical radial gradients then skew and translate the container to get a close approximation. But we also have a CSS gradient that’s already made for the job, the conic gradient. Below is an example that gives us a 7deg conic gradient at a 5deg offset from its container’s bottom-right corner.

background: conic-gradient(
  from 5deg at 0% 100%,
  transparent 0deg,
  hsl(4 10% 70% / 30%) 7deg,
  transparent 15deg
);
transform-origin: bottom left;
transform: rotate(-45deg);

We’ll add a few conic gradients centered at our flare center, with various gradient angles of semi-transparent colors. Because conic gradients can show the corner of its container div, we will rotationally transform them using our light source as its origin, resulting in an offset diffused ray filter effect.

Using CSS custom properties for a more flexible lens flare

So far, we’ve created a responsive, but statically-positioned, lens flare effect at a fixed location. It would be difficult to adjust the center of the lens flare without also breaking the horizontal and conic flares around it.

To make the CSS lens flare both adjustable and less brittle, we’ll expose the light source flare’s position, size, and hue via a set of custom properties.

:root {
  --hue: 4;
  --lens-center-size: 40%;
  --lens-spread-size: 80%;
  --lens-left: 55%;
  --lens-top: 15%;
}

While we are at it, we are also going to adjust the flare hue and the size of the horizontal flare height. For horizontal flare width, we use CSS variable overloading to make them adjustable on their own; otherwise, we fall back to the light source flare center or the image center.

.left-flare {
  width: var(--left-flare-width, var(--lens-left, 50%));
}

This is what the completed CSS lens flare effect looks like with a photo background and the lens flare moved up so the light source location looks believable. Go ahead, add your own photo to see how it works in different contexts!

Other CSS and non-CSS lens flare examples

This is just one way to create a CSS lens flare, of course. I like this approach because it’s flexible in terms of the color, size, and positioning of the flare and its parts. That makes it more of a reusable component that can be used in many contexts.

Here’s one by Keith Grant that uses a linear gradient as well as CSS custom properties. Then it sprinkles some JavaScript in there to randomize the HSLA values.

Nicholas Guest has another CSS lens flare that applies a box shadow on the ::before and ::after pseudo-elements of a .flare element to get the effect, plus a smidge of jQuery that makes the flare follow the mouse on hover.

This one is made with Canvas and is neat in how the light source follows the mouse on hover while showing how the lens flare artifacts change position as the light source position changes.

The same sort of idea here:

And a fun one that uses GSAP, Canvas, and a library called JS.LensFlare:

How would you approach a CSS lens flare effect? Share in the comments!


Add a CSS Lens Flare to Photos for a Bright Touch originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/add-a-css-lens-flare-to-photos-for-a-bright-touch/feed/ 1 364385
Tricks to Cut Corners Using CSS Mask and Clip-Path Properties https://css-tricks.com/cut-corners-using-css-mask-and-clip-path-properties/ https://css-tricks.com/cut-corners-using-css-mask-and-clip-path-properties/#comments Wed, 30 Mar 2022 16:35:08 +0000 https://css-tricks.com/?p=364279 We recently covered creating fancy borders with CSS mask properties, and now we are going to cut the corners with CSS mask and clip-path! A lot of techniques exist to cut different shapes from the corners of any element. …


Tricks to Cut Corners Using CSS Mask and Clip-Path Properties originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
We recently covered creating fancy borders with CSS mask properties, and now we are going to cut the corners with CSS mask and clip-path! A lot of techniques exist to cut different shapes from the corners of any element. In this article, we will consider modern techniques to create unique corner shapes while trying to work from reusable code that allows us to produce different results by adjusting variables.

Check this online tool to get an idea of what we are building. It’s a CSS generator where you select the shape, the corners, and the size then you get the code in no time!

We mainly have two types of cuts: a circular one and an angled one. For each, we can get the full shape or the border-only shape, not to mention that we can select the corners we want to cut. A lot of combinations!

Like in the previous article, we will make lots of use of the CSS mask property. So, if you are not familiar with it, I recommend reading the quick primer I wrote before continuing.

Circular cut-out

For a circular or rounded cut, we will use radial-gradient(). To cut four corners, the logical solution is to create four gradients, one for each corner:

Each gradient is taking a quarter of the element’s dimensions. The syntax of the gradient is self-explanatory:

radial-gradient(circle 30px at top left, #0000 98%, red) top left;

Translated, this renders a circle at the top-left corner with a 30px radius. The main color is transparent (#0000) and the remaining is red. The whole gradient is also placed so that it starts at the element’s top-left corner. Same logic for the three other gradients. The keyword circle can be omitted since we explicitly specified one value for the radius.

Like I did in the previous article, I will be using slightly bigger or smaller values this time around in order to avoid bad visual result. Here, I am using 98% instead of 100% to avoid jagged edges and 51% instead of 50% to create an overlap between gradients and avoid white spaces. This logic will follow throughout this article. In fact, you will find that adding or removing 1% or 1deg typically results in a nice visual.

We apply this to the CSS mask property and we are done!

We can actually optimize that code a little:

--g: #0000 98%,#000;
--r: 30px;
mask:
  radial-gradient(var(--r) at 0    0   ,var(--g)) 0    0,
  radial-gradient(var(--r) at 100% 0   ,var(--g)) 100% 0,
  radial-gradient(var(--r) at 0    100%,var(--g)) 0    100%,
  radial-gradient(var(--r) at 100% 100%,var(--g)) 100% 100%;
mask-size: 51% 51%;
mask-repeat: no-repeat;

This way, we use custom properties for the redundant values and, as a personal preference, I am using numeric values for the positions instead of keywords.

In the generator, I will use the following syntax:

--g: #0000 98%,#000;
--r: 30px;
mask:
  radial-gradient(var(--r) at 0    0   ,var(--g)) 0    0   /51% 51% no-repeat,
  radial-gradient(var(--r) at 100% 0   ,var(--g)) 100% 0   /51% 51% no-repeat,
  radial-gradient(var(--r) at 0    100%,var(--g)) 0    100%/51% 51% no-repeat,
  radial-gradient(var(--r) at 100% 100%,var(--g)) 100% 100%/51% 51% no-repeat;

The shorthand syntax is easier to generate plus the whole value can be used as one custom property.

Can we use fewer gradients if we want?

Sure! One gradient can do the job. Hover the below to see the trick:

Here, we define one radial-gradient() with no size (by default it is 100% height and 100% width). This gives us a hole in the center. We translate/move the gradient by half the width and height of the image to move the hole to one corner. Since, by default, the CSS mask repeats, we get the same on each corner. We have four cut corners with only one gradient!

The only drawback of this method is that we need to know the width and height of the element in advance.

Can’t we use -50% instead of half the width and height?

Unfortunately, we’re unable to do that here because percentages doesn’t behave the same as pixel values when used with the CSS mask-position property. They’re tricky.

I have a detailed Stack Overflow answer that explains the difference. It deals with background-position but the same logic applies to the CSS mask-position property.

However, we can use some tricks to make it work with percentage values and without the need to know the width or the height. When a gradient (or a background layer) has a width and height equal to the element, we cannot move it using percentage values. So we need to change its size!

I will define a size equal to 99.5% 99.5%. I am reducing 0.5% from the width and the height to have a value different from 100% and at the same time keep the same visual result since we won’t notice a big difference between 100% and 99.5%. Now that our gradient has a size different from 100% we can move it using percentage values.

I will not detail all the math, but to move it by half the width and the height we need to use this equation:

100% * (50/(100 - 99.5)) = 100% * 100 = 10000%

It’s a strange value but it does the job:

mask: radial-gradient(30px,#0000 98%,#000) 10000% 10000%/99.5% 99.5%

As you can see, the trick works just fine. Whatever the size of the element is, we can cut four corners using only one gradient. However, this method has a small drawback when the width or the height of the element is a decimal value. Here is an example with an image having a width equal to 150.5px:

The use of 99.5% combined with 150.5px will create rounding issues that will break the calculation, resulting in the mask being misaligned. So, use this method with caution.

Guess what? There is a solution with one gradient and no rounding issue. Using the following code:

mask: radial-gradient(30px at 30px 30px,#0000 98%,#000) -30px -30px

The trick is to create a hole placed at the top left corner and by moving it with a negative offset we cover the four corners. Hover the below to see the trick.

This method is perfect as it uses one gradient and has no rounding issue but it has one drawback. The value of the radius is used 5 times. Not a big deal as we can use a custom property for it:

--r: 30px;
mask: radial-gradient(var(--r) at var(--r) var(--r),#0000 98%,#000) calc(-1*var(--r)) calc(-1*var(--r))

Let’s quickly recap the three methods we just covered:

  • The first method uses four gradients and has no drawbacks as far as usage. Sure, it’s verbose but it works with any kind of element and size.
  • The second method uses one gradient, but it can break in some particular cases. It’s suitable with fixed-size elements. It’s ok to use, but maybe less frequently.
  • The third method uses one gradient and has no rounding issue. It’s the perfect method among all of them but it requires using the radius many time within the gradient value.

The generator only supports the first and third methods.

Now that we saw the case with all the corners, let’s disable some of them. Using the first method, any corner we want to keep uncut we simply remove its gradient and adjust the size of what remains.

To disable the top-right corner:

  • We remove the top-right gradient (the blue one).
  • We have an empty corner, so we increase the size of the red gradient (or the purple one) to cover that leftover space.

Done!

You probably see just how many possibilities and combinations we can do here. If we want to cut N corners (where N ranges from 1 to 4), we use N gradients. All we need is to correctly set the size of each one to leave no space.

What about the other methods where there’s only one gradient? We will need another gradient! Those two methods use only one radial-gradient() to cut the corners, so we will rely on another gradient to “hide” the cut. We can use a conic-gradient() with four sections for this task:

conic-gradient(red 25%, blue 0 50%, green 0 75%, purple 0)

We add it on the top of the radial gradient to get the following:

The conic-gradient() covers the radial-gradient() and no corner is cut. Let’s change one color in the conic-gradient() to transparent. The one at the top-right, for example:

Did you see that? We revealed one corner of the radial-gradient() and we end with one cut corner!

Now let’s do the same thing, but for the bottom-left corner.

I think you probably get the trick by now. By changing the colors of the conic-gradient() from opaque to transparent, we reveal the corners we want to cut and gain all kinds of possible combinations.

Now you are probably wondering which method you have to use and when. As I said, I don’t recommend the second method a lot due to the rounding issue but you have to use the two others based on the number of cut-outs.

To cut four corners, the first method requires four gradients while the third one requires only one gradient so we use the latter. To cut one corner, the first method requires one gradient while the third one requires two gradients so you use the first one. To cut two corners, both use two gradients, and to cut three corners one method will use three gradients and the other one only two.

By picking the adequate method for each case, we don’t need more than two gradients in total. I have detailed all the methods but in the end, you should pick the optimized code for each case.

Circular border-only cut-out

Let’s make the border-only version of the previous shape. In other words, we achieve the same shape but knock out the fill so all we’re left with is a border of the shape.

This is a bit tricky because we have different cases with different code. Fair warning, I will be using a lot of gradients here while finding opportunities to trim the number of them.

It should be noted that we will consider a pseudo-element in this case. Showing only the border means we need to hide the inner “fill” of the shape. Applying this to the main element will also hide the content — that’s why this is a nice use case for a pseudo-element.

One cut corner

This one needs one radial gradient and two conic gradients:

The first example illustrates the radial gradient (in red) and both conic gradients (in blue and green). In the second example, we apply all of them inside the CSS mask property to create the border-only shape with one cut corner.

Here’s a diagram of the game plan.

As the diagram shows, the radial-gradient() creates the quarter of a circle and each conic-gradient() creates two perpendicular segments to cover two sides. It should be noted that overlapping gradients is not an issue since we are not going to change the CSS mask-composite property value.

Using the same code an adjusting a few variables, we can get the shape for the other corners.

Two cut corners

For the two-corner configuration we have two situations taking place.

In the first situation, there are two opposite corners where we need two radial gradients and two conic gradients.

The configuration is almost the same as cutting only one corner: we add an extra gradient and update a few variables.

In the second situation, there are two adjacent corners and, in this case, we need one radial gradient, one conic gradient, and one linear gradient.

“Wait!” you might exclaim. “How come the conic gradient covers three sides?” If you check the code, notice the repeat-y. In all of the examples, we always used no-repeat, but for this we can repeat one of them to cover more sides and reduce the number of gradients we use.

Here is an example with only the conic-gradient() to understand the repetition. The trick is to have a height equal to 100% minus the border size so that the gradient fills that space when repeating, which covers the third side in the process.

You are probably wondering how one radial gradient is cutting two corners. To do this, we create half a circle that we place at the top left corner. Then by using a negative offset we cut two adjacent corners. Hover the below to understand the trick.

Three cut corners

For this configuration, we need two radial gradients, one conic gradient, and two linear gradients.

Four corners cut

It takes one radial gradient and two linear gradients to cut all four corners.

I can hear you screaming, “How the heck am I supposed to memorize all these cases?!” You don’t need to memorize anything since you can easily generate the code for each case using the online generator. All you need is to understand the overall trick rather than each individual case. That’s why I’ve only gone into fine detail on the first configurations — the rest are merely iterations that tweak the initial foundation of the trick.

Notice there’s a general pattern we’ve been following throughout the examples:

  1. We add a radial-gradient() on the corners we want to cut.
  2. We fill the sides using either a conic-gradient() or a linear-gradient() to create the final shape.

It should be noted that we can find different ways to create the same shape. What I am showing in this post are the methods I found to be best after trying lots of other ideas. You may have a different approach you consider to be better! If so, definitely share it in the comments!

Angled cut-out

Let’s tackle another type of cut shape: the angled cut.

We have two parameters: the size and angle of the cut. To get the shape, we need a conic-gradient() for each corner. This configuration is very similar to the example that kicked off this article.

Here is an illustration of one corner to understand the trick:

The difference between each corner is an extra offset of 90deg in from and the at position. The full code is like below:

--size: 30px;
--angle: 130deg;

--g: #0000 var(--angle), #000 0;
mask:
  conic-gradient(from calc(var(--angle)/-2 -  45deg) 
    at top    var(--size) left  var(--size),var(--g)) top left,
  conic-gradient(from calc(var(--angle)/-2 + 45deg) 
    at top    var(--size) right var(--size),var(--g)) top right,
  conic-gradient(from calc(var(--angle)/-2 - 135deg) 
    at bottom var(--size) left  var(--size),var(--g)) bottom left,
  conic-gradient(from calc(var(--angle)/-2 + 135deg) 
    at bottom var(--size) right var(--size),var(--g)) bottom right;
mask-size: 51% 51%;
mask-repeat: no-repeat;

If we want to disable one corner, we remove the conic-gradient() for that corner and update the size of another one to fill the remaining space exactly like we did with the circular cut. Here’s how that looks for one corner:

We can do the exact same thing for all the other corners to get the same effect.

In addition to CSS mask, we can also use the CSS clip-path property to cut the corners. Each corner can be defined with three points.

Zooming in on a corner of the shape showing the three points that form the angled cut.
The shape consists of two points at each end of the cut, and one between them to form the angle.

The other corners will have the same value with an offset of 100%. This gives us the final code with a total of 12 points — three per corner.

/* I will define T = [1-tan((angle-90)/2)]*size */
clip-path: polygon(
  /* Top-left corner */
  0 T, size size,0 T, /* OR 0 0 */
  /* Top-right corner */
  calc(100% - T) 0,calc(100% - size) size,100% T, /* OR  100% 0 */
  /* Bottom-right corner*/
  100% calc(100% - T),calc(100% - size) calc(100% - size), calc(100% - T) 100%, /* OR 100% 100% */
  /* Bottom-left corner */ 
  T 100%, size calc(100% - size),0 calc(100% - T) /* OR 0 100% */
)

Notice the OR comments in that code. It defines the code we have to consider if we want to disable a particular corner. To cut a corner, we use three points. To uncut a corner, we use one point — which is nothing but the coordinate of that corner.

The 90deg special case

When the angle is equal to 90deg, we can optimize the code of the gradient version and rely on fewer gradients. To cut four corners we can use only one gradient:

--size: 30px;
mask: 
  conic-gradient(at var(--size) var(--size),#000 75%,#0000 0) 
  0 0/calc(100% - var(--size)) calc(100% - var(--size))

This doesn’t remind you of something? It’s exactly similar to the circular cut-out! For the 90deg we have two gradient methods, the first one we detailed previously where each corner is cut with one gradient, and this last method where we cut all the corners using one gradient. I think you know the rest of the story: to uncut some corners we combine the last method with a conic gradient

Two methods with gradients, one with clip-path, we have to add a conic-gradient?! I am lost …

As I said, no need to remember all the methods and tricks. The generator will do the job of generating the code for you. I simply try to make this article as detailed as possible to cover all the possible cases.

Border-only angled cut

Oof, we have reached the last and trickiest shape at last! This one can be achieved with either gradients or clip-path, but let’s go with the clip-path approach.

Things would get complex and verbose if we go with the gradient approach. Here’s a demo that illustrates that point:

There are nine gradients total, and I am still not done with the calculation. As you can tell, the thickness of the border is incorrect, plus the final result is unsatisfying due to the nature of gradients and their anti-aliasing issues. This approach might be a good exercise to push the limit of gradients, but I don’t recommend it in a production environment.

So, back to the clip-path method. We will still wind up with verbose code, but less of a big deal since the generator can do the job for us with a cleaner end result.

Here is an overview of the path. I am adding a small gap to better see the different points but we should have an overlap of points instead.

We have 13 outer points (the ones in black) and 13 inner points (the ones in blue).

The way we calculate the outer points is the same as how we did it for the regular angled cut. For the inner points, however, we need more math. Don’t worry, I’ll spare you some “boring” geometry explanation for this one. I know most of you don’t want it, but in case you need to dig into this, you can check the JavaScript file of the generator to find the code and the math I am using to generate the shape.

The 180deg special case

There’s a special case for the angle cut I want to call out. It’s where we use an angle equal to 180deg. Here’s what that produces:

We have a straight line on the corner so we can optimize the clip-path code. For the full shape, we can use eight points (two points per corner) instead of 12. And for the border-only version, we can use 18 points (nine inner points and outer points) instead of 26. In other words, we can remove the middle point.

The border-only shape can also be made using gradients. But rather than using nine gradients like we did before, we can get away with only four linear gradients and a clean result.

Conclusion

We just combined CSS masks with gradients to create some fancy shapes without resorting to hacks and a lot of code! We also experienced just how much it takes to strike the right balance of code to get the right results. We even learned a few tricks along the way, like changing values by one or even half a unit. CSS is super powerful!

But, as we discussed, the online generator I made is a great place to get the code you need rather than writing it out by hand. I mean, I went through all the work of figuring out how all of this works and I would likely still need to reference this very article to remember how it’s all put together. If you can memorize all of this, kudos! But it’s nice to have a generator to fall back on.


Tricks to Cut Corners Using CSS Mask and Clip-Path Properties originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/cut-corners-using-css-mask-and-clip-path-properties/feed/ 2 364279