mask – CSS-Tricks https://css-tricks.com Tips, Tricks, and Techniques on using Cascading Style Sheets. Fri, 02 Dec 2022 14:12:58 +0000 en-US hourly 1 https://wordpress.org/?v=6.1.1 https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1 mask – CSS-Tricks https://css-tricks.com 32 32 45537868 CSS Infinite and Circular Rotating Image Slider https://css-tricks.com/css-only-infinite-and-circular-image-slider/ https://css-tricks.com/css-only-infinite-and-circular-image-slider/#comments Fri, 02 Dec 2022 14:12:57 +0000 https://css-tricks.com/?p=375308 Image sliders (also called carousels) are everywhere. There are a lot of CSS tricks to create the common slider where the images slide from left to right (or the opposite). It’s the same deal with the many JavaScript libraries out


CSS Infinite and Circular Rotating Image Slider originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Image sliders (also called carousels) are everywhere. There are a lot of CSS tricks to create the common slider where the images slide from left to right (or the opposite). It’s the same deal with the many JavaScript libraries out there that create fancy sliders with complex animations. We are not going to do any of that in this post.

Through a little series of articles, we are going to explore some fancy and uncommon CSS-only sliders. If you are of tired seeing the same ol’ classic sliders, then you are in the right place!

CSS Sliders series

For this first article, we will start with something I call the “circular rotating image slider”:

Cool right? let’s dissect the code!

The HTML markup

If you followed my series of fancy image decorations or CSS grid and custom shapes, then you know that my first rule is to work with the smallest HTML possible. I always try hard to find CSS solutions before cluttering my code with a lot <div>s and other stuff.

The same rule applies here — our code is nothing but a list of images in a container.

Let’s say we’re working with four images:

<div class="gallery">
  <img src="" alt="">
  <img src="" alt="">
  <img src="" alt="">
  <img src="" alt="">
</div>

That’s it! Now let’s move to the interesting part of the code. But first, we’re going to dive into this to understand the logic of how our slider works.

How does it work?

Here is a video where I remove overflow: hidden from the CSS so we can better understand how the images are moving:

It’s like our four images are placed on a large circle that rotates counter-clockwise.

All the images have the same size (denoted by S in the figure). Note the blue circle which is the circle that intersects with the center of all the images and has a radius (R). We will need this value later for our animation. R is equal to 0.707 * S. (I’m going to skip the geometry that gives us that equation.)

Let’s write some CSS!

We will be using CSS Grid to place all the images in the same area above each other:

.gallery  {
  --s: 280px; /* control the size */

  display: grid;
  width: var(--s);
  aspect-ratio: 1;
  padding: calc(var(--s) / 20); /* we will see the utility of this later */
  border-radius: 50%;
}
.gallery > img {
  grid-area: 1 / 1;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
}

Nothing too complex so far. The tricky part is the animation.

We talked about rotating a big circle, but in reality, we will rotate each image individually creating the illusion of a big rotating circle. So, let’s define an animation, m, and apply it to the image elements:

.gallery > img {
  /* same as before */
  animation: m 8s infinite linear;
  transform-origin: 50% 120.7%;
}

@keyframes m {
  100% { transform: rotate(-360deg); }
}

The main trick relies on that highlighted line. By default, the CSS transform-origin property is equal to center (or 50% 50%) which makes the image rotate around its center, but we don’t need it to do that. We need the image to rotate around the center of the big circle that contains our images hence the new value for transform-origin.

Since R is equal to 0.707 * S, we can say that R is equal to 70.7% of the image size. Here’s a figure to illustrate how we got the 120.7% value:

Let’s run the animation and see what happens:

I know, I know. The result is far from what we want, but in reality we are very close. It may looks like there’s just one image there, but don’t forget that we have stacked all the images on top of each other. All of them are rotating at the same time and only the top image is visible. What we need is to delay the animation of each image to avoid this overlap.

.gallery > img:nth-child(2) { animation-delay: -2s; } /* -1 * 8s / 4 */
.gallery > img:nth-child(3) { animation-delay: -4s; } /* -2 * 8s / 4 */
.gallery > img:nth-child(4) { animation-delay: -6s; } /* -3 * 8s / 4 */

Things are already getting better!

If we hide the overflow on the container we can already see a slider, but we will update the animation a little so that each image remains visible for a short period before it moves along.

We’re going to update our animation keyframes to do just that:

@keyframes m {
  0%, 3% { transform: rotate(0); }
  22%, 27% { transform: rotate(-90deg); }
  47%, 52% { transform: rotate(-180deg); }
  72%, 77% { transform: rotate(-270deg); }
  98%, 100% { transform: rotate(-360deg); }
}

For each 90deg (360deg/4, where 4 is the number of images) we will add a small pause. Each image will remain visible for 5% of the overall duration before we slide to the next one (27%-22%, 52%-47%, etc.). I’m going to update the animation-timing-function using a cubic-bezier() function to make the animation a bit fancier:

Now our slider is perfect! Well, almost perfect because we are still missing the final touch: the colorful circular border that rotates around our images. We can use a pseudo-element on the .gallery wrapper to make it:

.gallery {
  padding: calc(var(--s) / 20); /* the padding is needed here */
  position: relative;
}
.gallery::after {
  content: "";
  position: absolute;
  inset: 0;
  padding: inherit; /* Inherits the same padding */
  border-radius: 50%;
  background: repeating-conic-gradient(#789048 0 30deg, #DFBA69 0 60deg);
  mask: 
    linear-gradient(#fff 0 0) content-box, 
    linear-gradient(#fff 0 0);
  mask-composite: exclude;
}
.gallery::after,
.gallery >img {
  animation: m 8s infinite cubic-bezier(.5, -0.2, .5, 1.2);
}

I have created a circle with a repeating conic gradient for the background while using a masking trick that only shows the padded area. Then I apply to it the same animation we defined for the images.

We are done! We have a cool circular slider:

Let’s add more images

Working with four images is good, but it would be better if we can scale it to any number of images. After all, this is the purpose of an image slider. We should be able to consider N images.

For this, we are going to make the code more generic by introducing Sass. First, we define a variable for the number of images ($n) and we will update every part where we hard-coded the number of images (4).

Let’s start with the delays:

.gallery > img:nth-child(2) { animation-delay: -2s; } /* -1 * 8s / 4 */
.gallery > img:nth-child(3) { animation-delay: -4s; } /* -2 * 8s / 4 */
.gallery > img:nth-child(4) { animation-delay: -6s; } /* -3 * 8s / 4 */

The formula for the delay is (1 - $i)*duration/$n, which gives us the following Sass loop:

@for $i from 2 to ($n + 1) {
  .gallery > img:nth-child(#{$i}) {
    animation-delay: calc(#{(1 - $i) / $n} * 8s);
  }
}

We can make the duration a variable as well if we really want to. But let’s move on to the animation:

@keyframes m {
  0%, 3% { transform: rotate(0); }
  22%, 27% { transform: rotate(-90deg); }
  47%, 52% { transform: rotate(-180deg); }
  72%, 77% { transform: rotate(-270deg); }
  98%, 100% {transform: rotate(-360deg); }
}

Let’s simplify it to get a better view of the pattern:

@keyframes m {
  0% { transform: rotate(0); }
  25% { transform: rotate(-90deg); }
  50% { transform: rotate(-180deg); }
  75% { transform: rotate(-270deg); }
  100% { transform: rotate(-360deg); }
}

The step between each state is equal to 25% — which is 100%/4 — and we add a -90deg angle — which is -360deg/4. That means we can write our loop like this instead:

@keyframes m {
  0% { transform: rotate(0); }
  @for $i from 1 to $n {
    #{($i / $n) * 100}% { transform: rotate(#{($i / $n) * -360}deg); }  
  }
  100% { transform: rotate(-360deg); }
}

Since each image takes 5% of the animation, we change this:

#{($i / $n) * 100}%

…with this:

#{($i / $n) * 100 - 2}%, #{($i / $n) * 100 + 3}%

It should be noted that 5% is an arbitrary value I choose for this example. We can also make it a variable to control how much time each image should stay visible. I am going to skip that for the sake of simplicity, but for homework, you can try to do it and share your implementation in the comments!

@keyframes m {
  0%,3% { transform: rotate(0); }
  @for $i from 1 to $n {
    #{($i / $n) * 100 - 2}%, #{($i / $n) * 100 + 3}% { transform: rotate(#{($i / $n) * -360}deg); }  
  }
  98%,100% { transform: rotate(-360deg); }
}

The last bit is to update transform-origin. We will need some geometry tricks. Whatever the number of images, the configuration is always the same. We have our images (small circles) placed inside a big circle and we need to find the value of the radius, R.

You probably don’t want a boring geometry explanation so here’s how we find R:

R = S / (2 * sin(180deg / N))

If we express that as a percentage, that gives us:

R = 100% / (2 * sin(180deg / N)) = 50% / sin(180deg / N)

…which means the transform-origin value is equal to:

transform-origin: 50% (50% / math.sin(180deg / $n) + 50%);

We’re done! We have a slider that works with any number images!

Let’s toss nine images in there:

Add as many images as you want and update the $n variable with the total number of images.

Wrapping up

With a few tricks using CSS transforms and standard geometry, we created a nice circular slider that doesn’t require a lot of code. What is cool about this slider is that we don’t need to bother duplicating the images to keep the infinite animation since we have a circle. After a full rotation, we will get back to the first image!


CSS Infinite and Circular Rotating Image Slider originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/css-only-infinite-and-circular-image-slider/feed/ 7 375308
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.

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


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

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

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

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

Fancy Image Decorations series

Let’s start with our first example

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

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

Let’s get back to our first example:

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

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

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

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

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

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

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

Let’s try another idea:

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

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

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

The Corner-Only Frame

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

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

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

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

#0000 25%, darkblue 0

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

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

#0000 25%, darkblue 25%

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

Somewhere in the specification, it says:

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

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

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

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

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

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

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

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

Let’s try another idea using the same technique:

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

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

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

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

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

Here is an illustration to better understand the different values:

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

The Frame Reveal

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

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

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

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

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

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

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

Another one? Let’s go!

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

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

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

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

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

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

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

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

The Frame Rotation

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

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

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

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

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

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

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

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

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

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

Wrapping up

That’s a lot of gradients in one article!

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

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

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

Fancy Image Decorations series


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

]]>
https://css-tricks.com/fancy-image-decorations-single-element-magic/feed/ 3 373965
How to Create Wavy Shapes & Patterns in CSS https://css-tricks.com/how-to-create-wavy-shapes-patterns-in-css/ https://css-tricks.com/how-to-create-wavy-shapes-patterns-in-css/#comments Mon, 26 Sep 2022 13:13:30 +0000 https://css-tricks.com/?p=373403 The wave is probably one of the most difficult shapes to make in CSS. We always try to approximate it with properties like border-radius and lots of magic numbers until we get something that feels kinda close. And that’s before …


How to Create Wavy Shapes & Patterns in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
The wave is probably one of the most difficult shapes to make in CSS. We always try to approximate it with properties like border-radius and lots of magic numbers until we get something that feels kinda close. And that’s before we even get into wavy patterns, which are more difficult.

“SVG it!” you might say, and you are probably right that it’s a better way to go. But we will see that CSS can make nice waves and the code for it doesn’t have to be all crazy. And guess what? I have an online generator to make it even more trivial!

If you play with the generator, you can see that the CSS it spits out is only two gradients and a CSS mask property — just those two things and we can make any kind of wave shape or pattern. Not to mention that we can easily control the size and the curvature of the waves while we’re at it.

Some of the values may look like “magic numbers” but there’s actually logic behind them and we will dissect the code and discover all the secrets behind creating waves.

This article is a follow-up to a previous one where I built all kinds of different zig-zag, scoped, scalloped, and yes, wavy border borders. I highly recommend checking that article as it uses the same technique we will cover here, but in greater detail.

The math behind waves

Strictly speaking, there isn’t one magic formula behind wavy shapes. Any shape with curves that go up and down can be called a wave, so we are not going to restrict ourselves to complex math. Instead, we will reproduce a wave using the basics of geometry.

Let’s start with a simple example using two circle shapes:

Two gray circles.

We have two circles with the same radius next to each other. Do you see that red line? It covers the top half of the first circle and the bottom half of the second one. Now imagine you take that line and repeat it.

A squiggly red line in the shape of waves.

We already see the wave. Now let’s fill the bottom part (or the top one) to get the following:

Red wave pattern.

Tada! We have a wavy shape, and one that we can control using one variable for the circle radii. This is one of the easiest waves we can make and it’s the one I showed off in this previous article

Let’s add a bit of complexity by taking the first illustration and moving the circles a little:

Two gray circles with two bisecting dashed lines indicating spacing.

We still have two circles with the same radii but they are no longer horizontally aligned. In this case, the red line no longer covers half the area of each circle, but a smaller area instead. This area is limited by the dashed red line. That line crosses the point where both circles meet.

Now take that line and repeat it and you get another wave, a smoother one.

A red squiggly line.
A red wave pattern.

I think you get the idea. By controlling the position and size of the circles, we can create any wave we want. We can even create variables for them, which I will call P and S, respectively.

You have probably noticed that, in the online generator, we control the wave using two inputs. They map to the above variables. S is the “Size of the wave” and P is the “curvature of the wave”.

I am defining P as P = m*S where m is the variable you adjust when updating the curvature of the wave. This allows us to always have the same curvature, even if we update S.

m can be any value between 0 and 2. 0 will give us the first particular case where both circles are aligned horizontally. 2 is a kind of maximum value. We can go bigger, but after a few tests I found that anything above 2 produces bad, flat shapes.

Let’s not forget the radius of our circle! That can also be defined using S and P like this:

R = sqrt(P² + S²)/2

When P is equal to 0, we will have R = S/2.

We have everything to start converting all of this into gradients in CSS!

Creating gradients

Our waves use circles, and when talking about circles we talk about radial gradients. And since two circles define our wave, we will logically be using two radial gradients.

We will start with the particular case where P is equal to 0. Here is the illustration of the first gradient:

This gradient creates the first curvature while filling in the entire bottom area —the “water” of the wave so to speak.

.wave {
  --size: 50px;

  mask: radial-gradient(var(--size) at 50% 0%, #0000 99%, red 101%) 
    50% var(--size)/calc(4 * var(--size)) 100% repeat-x;
}

The --size variable defines the radius and the size of the radial gradient. If we compare it with the S variable, then it’s equal to S/2.

Now let’s add the second gradient:

The second gradient is nothing but a circle to complete our wave:

radial-gradient(var(--size) at 50% var(--size), blue 99%, #0000 101%) 
  calc(50% - 2*var(--size)) 0/calc(4 * var(--size)) 100%

If you check the previous article you will see that I am simply repeating what I already did there.

I followed both articles but the gradient configurations are not the same.

That’s because we can reach the same result using different gradient configurations. You will notice a slight difference in the alignment if you compare both configurations, but the trick is the same. This can be confusing if you are unfamiliar with gradients, but don’t worry. With some practice, you get used to them and you will find by yourself that different syntax can lead to the same result.

Here is the full code for our first wave:

.wave {
  --size: 50px;

  mask:
    radial-gradient(var(--size) at 50% var(--size),#000 99%, #0000 101%) 
      calc(50% - 2*var(--size)) 0/calc(4 * var(--size)) 100%,
    radial-gradient(var(--size) at 50% 0px, #0000 99%, #000 101%) 
      50% var(--size)/calc(4 * var(--size)) 100% repeat-x;
}

Now let’s take this code and adjust it to where we introduce a variable that makes this fully reusable for creating any wave we want. As we saw in the previous section, the main trick is to move the circles so they are no more aligned so let’s update the position of each one. We will move the first one up and the second down.

Our code will look like this:

.wave {
  --size: 50px;
  --p: 25px;

  mask:
    radial-gradient(var(--size) at 50% calc(var(--size) + var(--p)), #000 99%, #0000 101%) 
      calc(50% - 2*var(--size)) 0/calc(4 * var(--size)) 100%,
    radial-gradient(var(--size) at 50% calc(-1*var(--p)), #0000 99%, #000 101%) 
      50% var(--size) / calc(4 * var(--size)) 100% repeat-x;
}

I have introduced a new --p variable that’s used it to define the center position of each circle. The first gradient is using 50% calc(-1*var(--p)), so its center moves up while the second one is using calc(var(--size) + var(--p)) to move it down.

A demo is worth a thousand words:

The circles are neither aligned nor touch one another. We spaced them far apart without changing their radii, so we lost our wave. But we can fix things up by using the same math we used earlier to calculate the new radius. Remember that R = sqrt(P² + S²)/2. In our case, --size is equal to S/2; the same for --p which is also equal to P/2 since we are moving both circles. So, the distance between their center points is double the value of --p for this:

R = sqrt(var(--size) * var(--size) + var(--p) * var(--p))

That gives us a result of 55.9px.

Our wave is back! Let’s plug that equation into our CSS:

.wave {
  --size: 50px;
  --p: 25px;
  --R: sqrt(var(--p) * var(--p) + var(--size)*var(--size));

  mask:
    radial-gradient(var(--R) at 50% calc(var(--size) + var(--p)), #000 99%, #0000 101%) 
      calc(50% - 2*var(--size)) 0 / calc(4 * var(--size)) 100%,
    radial-gradient(var(--R) at 50% calc(-1*var(--p)), #0000 99%, #000 101%) 
      50% var(--size)/calc(4 * var(--size)) 100% repeat-x;
}

This is valid CSS code. sqrt() is part of the specification, but at the time I’m writing this, there is no browser support for it. That means we need a sprinkle of JavaScript or Sass to calculate that value until we get broader sqrt() support.

This is pretty darn cool: all it takes is two gradients to get a cool wave that you can apply to any element using the mask property. No more trial and error — all you need is to update two variables and you’re good to go!

Reversing the wave

What if we want the waves going the other direction, where we’re filling in the “sky” instead of the “water”. Believe it or not, all we have to do is to update two values:

.wave {
  --size: 50px;
  --p: 25px;
  --R: sqrt(var(--p) * var(--p) + var(--size) * var(--size));

  mask:
    radial-gradient(var(--R) at 50% calc(100% - (var(--size) + var(--p))), #000 99%, #0000 101%)
      calc(50% - 2 * var(--size)) 0/calc(4 * var(--size)) 100%,
    radial-gradient(var(--R) at 50% calc(100% + var(--p)), #0000 99%, #000 101%) 
      50% calc(100% - var(--size)) / calc(4 * var(--size)) 100% repeat-x;
}

All I did there is add an offset equal to 100%, highlighted above. Here’s the result:

We can consider a more friendly syntax using keyword values to make it even easier:

.wave {
  --size: 50px;
  --p: 25px;
  --R: sqrt(var(--p)*var(--p) + var(--size) * var(--size));

  mask:
    radial-gradient(var(--R) at left 50% bottom calc(var(--size) + var(--p)), #000 99%, #0000 101%) 
      calc(50% - 2 * var(--size)) 0/calc(4 * var(--size)) 100%,
    radial-gradient(var(--R) at left 50% bottom calc(-1 * var(--p)), #0000 99%, #000 101%) 
      left 50% bottom var(--size) / calc(4 * var(--size)) 100% repeat-x;
}

We’re using the left and bottom keywords to specify the sides and the offset. By default, the browser defaults to left and top — that’s why we use 100% to move the element to the bottom. In reality, we are moving it from the top by 100%, so it’s really the same as saying bottom. Much easier to read than math!

With this updated syntax, all we have to do is to swap bottom for top — or vice versa — to change the direction of the wave.

And if you want to get both top and bottom waves, we combine all the gradients in a single declaration:

.wave {
  --size: 50px;
  --p: 25px;
  --R: sqrt(var(--p)*var(--p) + var(--size)*var(--size));

  mask:
    /* Gradient 1 */
    radial-gradient(var(--R) at left 50% bottom calc(var(--size) + var(--p)), #000 99%, #0000 101%) 
      left calc(50% - 2*var(--size)) bottom 0 / calc(4 * var(--size)) 51% repeat-x,
    /* Gradient 2 */
    radial-gradient(var(--R) at left 50% bottom calc(-1 * var(--p)), #0000 99%, #000 101%) 
      left 50% bottom var(--size) / calc(4 * var(--size)) calc(51% - var(--size)) repeat-x,
    /* Gradient 3 */
    radial-gradient(var(--R) at left 50% top calc(var(--size) + var(--p)), #000 99%, #0000 101%) 
      left calc(50% - 2 * var(--size)) top 0 / calc(4 * var(--size)) 51% repeat-x,
    /* Gradient 4 */
    radial-gradient(var(--R) at left 50% top calc(-1 * var(--p)), #0000 99%, #000 101%) 
      left 50% top var(--size) / calc(4 * var(--size)) calc(51% - var(--size)) repeat-x;
}

If you check the code, you will see that in addition to combining all the gradients, I have also reduced their height from 100% to 51% so that they both cover half of the element. Yes, 51%. We need that little extra percent for a small overlap that avoid gaps.

What about the left and right sides?

It’s your homework! Take what we did with the top and bottom sides and try to update the values to get the right and left values. Don’t worry, it’s easy and the only thing you need to do is to swap values.

If you have trouble, you can always use the online generator to check the code and visualize the result.

Wavy lines

Earlier, we made our first wave using a red line then filled the bottom portion of the element. How about that wavy line? That’s a wave too! Even better is if we can control its thickness with a variable so we can reuse it. Let’s do it!

We are not going to start from scratch but rather take the previous code and update it. The first thing to do is to update the color stops of the gradients. Both gradients start from a transparent color to an opaque one, or vice versa. To simulate a line or border, we need to start from transparent, go to opaque, then back to transparent again:

#0000 calc(99% - var(--b)), #000 calc(101% - var(--b)) 99%, #0000 101%

I think you already guessed that the --b variable is what we’re using to control the line thickness. Let’s apply this to our gradients:

Yeah, the result is far from a wavy line. But looking closely, we can see that one gradient is correctly creating the bottom curvature. So, all we really need to do is rectify the second gradient. Instead of keeping a full circle, let’s make partial one like the other gradient.

Still far, but we have both curvatures we need! If you check the code, you will see that we have two identical gradients. The only difference is their positioning:

.wave {
  --size: 50px;
  --b: 10px;
  --p: 25px;
  --R: sqrt(var(--p)*var(--p) + var(--size)*var(--size));

  --_g: #0000 calc(99% - var(--b)), #000 calc(101% - var(--b)) 99%, #0000 101%;
  mask:
    radial-gradient(var(--R) at left 50% bottom calc(-1*var(--p)), var(--_g)) 
      calc(50% - 2*var(--size)) 0/calc(4*var(--size)) 100%,
    radial-gradient(var(--R) at left 50% top    calc(-1*var(--p)), var(--_g)) 
      50% var(--size)/calc(4*var(--size)) 100%;
}

Now we need to adjust the size and position for the final shape. We no longer need the gradient to be full-height, so we can replace 100% with this:

/* Size plus thickness */
calc(var(--size) + var(--b))

There is no mathematical logic behind this value. It only needs to be big enough for the curvature. We will see its effect on the pattern in just a bit. In the meantime, let’s also update the position to vertically center the gradients:

.wave {
  --size: 50px;
  --b: 10px;
  --p: 25px;
  --R: sqrt(var(--p)*var(--p) + var(--size)*var(--size));

  --_g: #0000 calc(99% - var(--b)), #000 calc(101% - var(--b)) 99%, #0000 101%;  
  mask:
    radial-gradient(var(--R) at left 50% bottom calc(-1*var(--p)), var(--_g)) 
      calc(50% - 2*var(--size)) 50%/calc(4 * var(--size)) calc(var(--size) + var(--b)) no-repeat,
    radial-gradient(var(--R) at left 50% top calc(-1 * var(--p)), var(--_g)) 50%
      50%/calc(4 * var(--size)) calc(var(--size) + var(--b)) no-repeat;
}

Still not quite there:

One gradient needs to move a bit down and the other a bit up. Both need to move by half of their height.

We are almost there! We need a small fix for the radius to have a perfect overlap. Both lines need to offset by half the border (--b) thickness:

We got it! A perfect wavy line that we can easily adjust by controlling a few variables:

.wave {
  --size: 50px;
  --b: 10px;
  --p: 25px;
  --R: calc(sqrt(var(--p) * var(--p) + var(--size) * var(--size)) + var(--b) / 2);

  --_g: #0000 calc(99% - var(--b)), #000 calc(101% - var(--b)) 99%, #0000 101%;
  mask:
    radial-gradient(var(--R) at left 50% bottom calc(-1 * var(--p)), var(--_g)) 
     calc(50% - 2*var(--size)) calc(50% - var(--size)/2 - var(--b)/2) / calc(4 * var(--size)) calc(var(--size) + var(--b)) repeat-x,
    radial-gradient(var(--R) at left 50% top calc(-1*var(--p)),var(--_g)) 
     50%  calc(50% + var(--size)/2 + var(--b)/2) / calc(4 * var(--size)) calc(var(--size) + var(--b)) repeat-x;
}

I know that the logic takes a bit to grasp. That’s fine and as I said, creating a wavy shape in CSS is not easy, not to mention the tricky math behind it. That’s why the online generator is a lifesaver — you can easily get the final code even if you don’t fully understand the logic behind it.

Wavy patterns

We can make a pattern from the wavy line we just created!

Oh no, the code of the pattern will be even more difficult to understand!

Not at all! We already have the code. All we need to do is to remove repeat-x from what we already have, and tada. 🎉

A nice wavy pattern. Remember the equation I said we’d revisit?

/* Size plus thickness */
calc(var(--size) + var(--b))

Well, this is what controls the distance between the lines in the pattern. We can make a variable out of it, but there’s no need for more complexity. I’m not even using a variable for that in the generator. Maybe I’ll change that later.

Here is the same pattern going in a different direction:

I am providing you with the code in that demo, but I’d for you to dissect it and understand what changes I made to make that happen.

Simplifying the code

In all the previous demos, we always define the --size and --p independently. But do you recall how I mentioned earlier that the online generator evaluates P as equal to m*S, where m controls the curvature of the wave? By defining a fixed multiplier, we can work with one particular wave and the code can become easier. This is what we will need in most cases: a specific wavy shape and a variable to control its size.

Let’s update our code and introduce the m variable:

.wave {
  --size: 50px;
  --R: calc(var(--size) * sqrt(var(--m) * var(--m) + 1));

  mask:
    radial-gradient(var(--R) at 50% calc(var(--size) * (1 + var(--m))), #000 99%, #0000 101%) 
      calc(50% - 2*var(--size)) 0/calc(4 * var(--size)) 100%,
    radial-gradient(var(--R) at 50% calc(-1 * var(--size) * var(--m)), #0000 99%, #000 101%) 
      50% var(--size) / calc(4 * var(--size)) 100% repeat-x;
  }

As you can see, we no longer need the --p variable. I replaced it with var(--m)*var(--size), and optimized some of the math accordingly. Now, If we want to work with a particular wavy shape, we can omit the --m variable and replace it with a fixed value. Let’s try .8 for example.

--size: 50px;
--R: calc(var(--size) * 1.28);

mask:
  radial-gradient(var(--R) at 50% calc(1.8 * var(--size)), #000 99%, #0000 101%) 
    calc(50% - 2*var(--size)) 0/calc(4 * var(--size)) 100%,
  radial-gradient(var(--R) at 50% calc(-.8 * var(--size)), #0000 99%, #000 101%) 
    50% var(--size) / calc(4 * var(--size)) 100% repeat-x;

See how the code is easier now? Only one variable to control your wave, plus you no more need to rely on sqrt() which has no browser support!

You can apply the same logic to all the demos we saw even for the wavy lines and the pattern. I started with a detailed mathmatical explanation and gave the generic code, but you may find yourself needing easier code in a real use case. This is what I am doing all the time. I rarely use the generic code, but I always consider a simplified version especially that, in most of the cases, I am using some known values that don’t need to be stored as variables. (Spoiler alert: I will be sharing a few examples at the end!)

Limitations to this approach

Mathematically, the code we made should give us perfect wavy shapes and patterns, but in reality, we will face some strange results. So, yes, this method has its limitations. For example, the online generator is capable of producing poor results, especially with wavy lines. Part of the issue is due to a particular combination of values where the result gets scrambled, like using a big value for the border thickness compared to the size:

For the other cases, it’s the issue related to some rounding that will results in misalignment and gaps between the waves:

That said, I still think the method we covered remains a good one because it produces smooth waves in most cases, and we can easily avoid the bad results by playing with different values until we get it perfect.

Wrapping up

I hope that after this article, you will no more to fumble around with trial and error to build a wavy shape or pattern. In addition to the online generator, you have all the math secrets behind creating any kind of wave you want!

The article ends here but now you have a powerful tool to create fancy designs that use wavy shapes. Here’s inspiration to get you started…

What about you? Use my online generator (or write the code manually if you already learned all the math by heart) and show me your creations! Let’s have a good collection in the comment section.


How to Create Wavy Shapes & Patterns in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/how-to-create-wavy-shapes-patterns-in-css/feed/ 1 373403
How I Made a Pure CSS Puzzle Game https://css-tricks.com/how-i-made-a-pure-css-puzzle-game/ https://css-tricks.com/how-i-made-a-pure-css-puzzle-game/#comments Fri, 09 Sep 2022 13:21:29 +0000 https://css-tricks.com/?p=372952 I recently discovered the joy of creating CSS-only games. It’s always fascinating how HTML and CSS are capable of handling the logic of an entire online game, so I had to try it! Such games usually rely on the ol’ …


How I Made a Pure CSS Puzzle Game originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I recently discovered the joy of creating CSS-only games. It’s always fascinating how HTML and CSS are capable of handling the logic of an entire online game, so I had to try it! Such games usually rely on the ol’ Checkbox Hack where we combine the checked/unchecked state of a HTML input with the :checked pseudo-class in CSS. We can do a lot of magic with that one combination!

In fact, I challenged myself to build an entire game without Checkbox. I wasn’t sure if it would be possible, but it definitely is, and I’m going to show you how.

In addition to the puzzle game we will study in this article, I have made a collection of pure CSS games, most of them without the Checkbox Hack. (They are also available on CodePen.)

Want to play before we start?

I personally prefer playing the game in full screen mode, but you can play it below or open it up over here.

Cool right? I know, it’s not the Best Puzzle Game You Ever Saw™ but it’s also not bad at all for something that only uses CSS and a few lines of HTML. You can easily adjust the size of the grid, change the number of cells to control the difficulty level, and use whatever image you want!

We’re going to remake that demo together, then put a little extra sparkle in it at the end for some kicks.

The drag and drop functionality

While the structure of the puzzle is fairly straightforward with CSS Grid, the ability to drag and drop puzzle pieces is a bit trickier. I had to relying on a combination of transitions, hover effects, and sibling selectors to get it done.

If you hover over the empty box in that demo, the image moves inside of it and stays there even if you move the cursor out of the box. The trick is to add a big transition duration and delay — so big that the image takes lots of time to return to its initial position.

img {
  transform: translate(200%);
  transition: 999s 999s; /* very slow move on mouseout */
}
.box:hover img {
  transform: translate(0);
  transition: 0s; /* instant move on hover */
}

Specifying only the transition-delay is enough, but using big values on both the delay and the duration decreases the chance that a player ever sees the image move back. If you wait for 999s + 999s — which is approximately 30 minutes — then you will see the image move. But you won’t, right? I mean, no one’s going to take that long between turns unless they walk away from the game. So, I consider this a good trick for switching between two states.

Did you notice that hovering the image also triggers the changes? That’s because the image is part of the box element, which is not good for us. We can fix this by adding pointer-events: none to the image but we won’t be able to drag it later.

That means we have to introduce another element inside the .box:

That extra div (we’re using a class of .a) will take the same area as the image (thanks to CSS Grid and grid-area: 1 / 1) and will be the element that triggers the hover effect. And that is where the sibling selector comes into play:

.a {
  grid-area: 1 / 1;
}
img {
  grid-area: 1 / 1;
  transform: translate(200%);
  transition: 999s 999s;
}
.a:hover + img {
  transform: translate(0);
  transition: 0s;
}

Hovering on the .a element moves the image, and since it is taking up all space inside the box, it’s like we are hovering over the box instead! Hovering the image is no longer a problem!

Let’s drag and drop our image inside the box and see the result:

Did you see that? You first grab the image and move it to the box, nothing fancy. But once you release the image you trigger the hover effect that moves the image, and then we simulate a drag and drop feature. If you release the mouse outside the box, nothing happens.

Hmm, your simulation isn’t perfect because we can also hover the box and get the same effect.

True and we will rectify this. We need to disable the hover effect and allow it only if we release the image inside the box. We will play with the dimension of our .a element to make that happen.

Now, hovering the box does nothing. But if you start dragging the image, the .a element appears, and once released inside the box, we can trigger the hover effect and move the image.

Let’s dissect the code:

.a {
  width: 0%;
  transition: 0s .2s; /* add a small delay to make sure we catch the hover effect */
}
.box:active .a { /* on :active increase the width */
  width: 100%;
  transition: 0s; /* instant change */
}
img {
  transform: translate(200%);
  transition: 999s 999s;
}
.a:hover + img {
  transform: translate(0);
  transition: 0s;
}

Clicking on the image fires the :active pseudo-class that makes the .a element full-width (it is initially equal to 0). The active state will remain active until we release the image. If we release the image inside the box, the .a element goes back to width: 0, but we will trigger the hover effect before it happens and the image will fall inside the box! If you release it outside the box, nothing happens.

There is a little quirk: clicking the empty box also moves the image and breaks our feature. Currently, :active is linked to the .box element, so clicking on it or any of its children will activate it; and by doing this, we end up showing the .a element and triggering the hover effect.

We can fix that by playing with pointer-events. It allows us to disable any interaction with the .box while maintaining the interactions with the child elements.

.box {
  pointer-events: none;
}
.box * {
  pointer-events: initial;
}

Now our drag and drop feature is perfect. Unless you can find how to hack it, the only way to move the image is to drag it and drop it inside the box.

Building the puzzle grid

Putting the puzzle together is going to feel easy peasy compared to what we just did for the drag and drop feature. We are going to rely on CSS grid and background tricks to create the puzzle.

Here’s our grid, written in Pug for convenience:

- let n = 4; /* number of columns/rows */
- let image = "https://picsum.photos/id/1015/800/800";

g(style=`--i:url(${image})`)
  - for(let i = 0; i < n*n; i++)
    z
      a
      b(draggable="true") 

The code may look strange but it compiles into plain HTML:

<g style="--i: url(https://picsum.photos/id/1015/800/800)">
 <z>
   <a></a>
   <b draggable="true"></b>
 </z>
 <z>
   <a></a>
   <b draggable="true"></b>
 </z>
 <z>
   <a></a>
   <b draggable="true"></b>
 </z>
  <!-- etc. -->
</g>

I bet you’re wondering what’s up with those tags. None of these elements have any special meaning — I just find that the code is much easier to write using <z> than a bunch of <div class="z"> or whatever.

This is how I’ve mapped them out:

  • <g> is our grid container that contains N*N <z> elements.
  • <z> represents our grid items. It plays the role of the .box element we saw in the previous section.
  • <a> triggers the hover effect.
  • <b> represents a portion of our image. We apply the draggable attribute on it because it cannot be dragged by default.

Alright, let’s register our grid container on <g>. This is in Sass instead of CSS:

$n : 4; /* number of columns/rows */

g {
  --s: 300px; /* size of the puzzle */

  display: grid;
  max-width: var(--s);
  border: 1px solid;
  margin: auto;
  grid-template-columns: repeat($n, 1fr);
}

We’re actually going to make our grid children — the <z> elements — grids as well and have both <a> and <b> within the same grid area:

z {
  aspect-ratio: 1;
  display: grid;
  outline: 1px dashed;
}
a {
  grid-area: 1/1;
}
b {
  grid-area: 1/1;
}

As you can see, nothing fancy — we created a grid with a specific size. The rest of the CSS we need is for the drag and drop feature, which requires us to randomly place the pieces around the board. I’m going to turn to Sass for this, again for the convenience of being able to loop through and style all the puzzle pieces with a function:

b {
  background: var(--i) 0/var(--s) var(--s);
}

@for $i from 1 to ($n * $n + 1) {
  $r: (random(180));
  $x: (($i - 1)%$n);
  $y: floor(($i - 0.001) / $n);
  z:nth-of-type(#{$i}) b{
    background-position: ($x / ($n - 1)) * 100% ($y / ($n - 1)) * 100%;
    transform: 
      translate((($n - 1) / 2 - $x) * 100%, (($n - 1)/2 - $y) * 100%) 
      rotate($r * 1deg) 
      translate((random(100)*1% + ($n - 1) * 100%)) 
      rotate((random(20) - 10 - $r) * 1deg)
   }
}

You may have noticed that I’m using the Sass random() function. That’s how we get the randomized positions for the puzzle pieces. Remember that we will disable that position when hovering over the <a> element after dragging and dropping its corresponding <b> element inside the grid cell.

z a:hover ~ b {
  transform: translate(0);
  transition: 0s;
}

In that same loop, I am also defining the background configuration for each piece of the puzzle. All of them will logically share the same image as the background, and its size should be equal to the size of the whole grid (defined with the --s variable). Using the same background-image and some math, we update the background-position to show only a piece of the image.

That’s it! Our CSS-only puzzle game is technically done!

But we can always do better, right? I showed you how to make a grid of puzzle piece shapes in another article. Let’s take that same idea and apply it here, shall we?

Puzzle piece shapes

Here’s our new puzzle game. Same functionality but with more realistic shapes!

This is an illustration of the shapes on the grid:

If you look closely you’ll notice that we have nine different puzzle-piece shapes: the four corners, the four edges, and one for everything else.

The grid of puzzle pieces I made in the other article I referred to is a little more straightforward:

We can use the same technique that combines CSS masks and gradients to create the different shapes. In case you are unfamiliar with mask and gradients, I highly recommend checking that simplified case to better understand the technique before moving to the next part.

First, we need to use specific selectors to target each group of elements that shares the same shape. We have nine groups, so we will use eight selectors, plus a default selector that selects all of them.

z  /* 0 */

z:first-child  /* 1 */

z:nth-child(-n + 4):not(:first-child) /* 2 */

z:nth-child(5) /* 3 */

z:nth-child(5n + 1):not(:first-child):not(:nth-last-child(5)) /* 4 */

z:nth-last-child(5)  /* 5 */

z:nth-child(5n):not(:nth-child(5)):not(:last-child) /* 6 */

z:last-child /* 7 */

z:nth-last-child(-n + 4):not(:last-child) /* 8 */

Here is a figure that shows how that maps to our grid:

Now let’s tackle the shapes. Let’s focus on learning just one or two of the shapes because they all use the same technique — and that way, you have some homework to keep learning!

For the puzzle pieces in the center of the grid, 0:

mask: 
  radial-gradient(var(--r) at calc(50% - var(--r) / 2) 0, #0000 98%, #000) var(--r)  
    0 / 100% var(--r) no-repeat,
  radial-gradient(var(--r) at calc(100% - var(--r)) calc(50% - var(--r) / 2), #0000 98%, #000) 
    var(--r) 50% / 100% calc(100% - 2 * var(--r)) no-repeat,
  radial-gradient(var(--r) at var(--r) calc(50% - var(--r) / 2), #000 98%, #0000),
  radial-gradient(var(--r) at calc(50% + var(--r) / 2) calc(100% - var(--r)), #000 98%, #0000);

The code may look complex, but let’s focus on one gradient at a time to see what’s happening:

Two gradients create two circles (marked green and purple in the demo), and two other gradients create the slots that other pieces connect to (the one marked blue fills up most of the shape while the one marked red fills the top portion). A CSS variable, --r, sets the radius of the circular shapes.

The shape of the puzzle pieces in the center (marked 0 in the illustration) is the hardest to make as it uses four gradients and has four curvatures. All the others pieces juggle fewer gradients.

For example, the puzzle pieces along the top edge of the puzzle (marked 2 in the illustration) uses three gradients instead of four:

mask: 
  radial-gradient(var(--r) at calc(100% - var(--r)) calc(50% + var(--r) / 2), #0000 98%, #000) var(--r) calc(-1 * var(--r)) no-repeat,
  radial-gradient(var(--r) at var(--r) calc(50% - var(--r) / 2), #000 98%, #0000),
  radial-gradient(var(--r) at calc(50% + var(--r) / 2) calc(100% - var(--r)), #000 98%, #0000);

We removed the first (top) gradient and adjusted the values of the second gradient so that it covers the space left behind. You won’t notice a big difference in the code if you compare the two examples. It should be noted that we can find different background configurations to create the same shape. If you start playing with gradients you will for sure come up with something different than what I did. You may even write something that’s more concise — if so, share it in the comments!

In addition to creating the shapes, you will also find that I am increasing the width and/or the height of the elements like below:

height: calc(100% + var(--r));
width: calc(100% + var(--r));

The pieces of the puzzle need to overflow their grid cell to connect.

Final demo

Here is the full demo again. If you compare it with the first version you will see the same code structure to create the grid and the drag-and-drop feature, plus the code to create the shapes.

Possible enhancements

The article ends here but we could keep enhancing our puzzle with even more features! How about a a timer? Or maybe some sort of congratulations when the player finishes the puzzle?

I may consider all these features in a future version, so keep an eye on my GitHub repo.

Wrapping up

And CSS isn’t a programming language, they say. Ha!

I’m not trying to spark some #HotDrama by that. I say it because we did some really tricky logic stuff and covered a lot of CSS properties and techniques along the way. We played with CSS Grid, transitions, masking, gradients, selectors, and background properties. Not to mention the few Sass tricks we used to make our code easy to adjust.

The goal was not to build the game, but to explore CSS and discover new properties and tricks that you can use in other projects. Creating an online game in CSS is a challenge that pushes you to explore CSS features in great detail and learn how to use them. Plus, it’s just a lot of fun that we get something to play with when all is said and done.

Whether CSS is a programming language or not, doesn’t change the fact that we always learn by building and creating innovative stuff.


How I Made a Pure CSS Puzzle Game originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/how-i-made-a-pure-css-puzzle-game/feed/ 9 372952
CSS Grid and Custom Shapes, Part 2 https://css-tricks.com/css-grid-and-custom-shapes-part-2/ https://css-tricks.com/css-grid-and-custom-shapes-part-2/#comments Mon, 22 Aug 2022 14:08:39 +0000 https://css-tricks.com/?p=372396 Alright, so the last time we checked in, we were using CSS Grid and combining them with CSS clip-path and mask techniques to create grids with fancy shapes.

Here’s just one of the fantastic grids we made together:

CodePen…


CSS Grid and Custom Shapes, Part 2 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Alright, so the last time we checked in, we were using CSS Grid and combining them with CSS clip-path and mask techniques to create grids with fancy shapes.

Here’s just one of the fantastic grids we made together:

CSS Grid and Custom Shapes series

Ready for the second round? We are still working with CSS Grid, clip-path, and mask, but by the end of this article, we’ll end up with different ways to arrange images on the grid, including some rad hover effects that make for an authentic, interactive experience to view pictures.

And guess what? We’re using the same markup that we used last time. Here’s that again:

<div class="gallery">
  <img src="..." alt="...">
  <img src="..." alt="...">
  <img src="..." alt="...">
  <img src="..." alt="...">
  <!-- as many times as we want -->
</div>

Like the previous article, we only need a container with images inside. Nothing more!

Nested Image Grid

Last time, our grids were, well, typical image grids. Other than the neat shapes we masked them with, they were pretty standard symmetrical grids as far as how we positioned the images inside.

Let’s try nesting an image in the center of the grid:

We start by setting a 2✕2 grid for four images:

.gallery {
  --s: 200px; /* controls the image size */
  --g: 10px; /* controls the gap between images */

  display: grid;
  gap: var(--g);
  grid-template-columns: repeat(2, auto);
}
.gallery > img {
  width: var(--s);
  aspect-ratio: 1;
  object-fit: cover;
}

Nothing complex yet. The next step is to cut the corner of our images to create the space for the nested image. I already have a detailed article on how to cut corners using clip-path and mask. You can also use my online generator to get the CSS for masking corners.

What we need here is to cut out the corners at an angle equal to 90deg. We can use the same conic-gradient technique from that article to do that:

.gallery > img {
   mask: conic-gradient(from var(--_a), #0000 90deg, #000 0);
}
.gallery > img:nth-child(1) { --_a: 90deg; }
.gallery > img:nth-child(2) { --_a: 180deg; }
.gallery > img:nth-child(3) { --_a: 0deg; }
.gallery > img:nth-child(4) { --_a:-90deg; }

We could use the clip-path method for cutting corners from that same article, but masking with gradients is more suitable here because we have the same configuration for all the images — all we need is a rotation (defined with the variable --_a) get the effect, so we’re masking from the inside instead of the outside edges.

Two by two grid of images with a white square stacked on top in the center.

Now we can place the nested image inside the masked space. First, let’s make sure we have a fifth image element in the HTML:

<div class="gallery">
  <img src="..." alt="...">
  <img src="..." alt="...">
  <img src="..." alt="...">
  <img src="..." alt="...">
  <img src="..." alt="...">
</div>

We are going to rely on the good ol’ absolute positioning to place it in there:

.gallery > img:nth-child(5) {
  position: absolute;
  inset: calc(50% - .5*var(--s));
  clip-path: inset(calc(var(--g) / 4));
}

The inset property allows us to place the image at the center using a single declaration. We know the size of the image (defined with the variable --s), and we know that the container’s size equals 100%. We do some math, and the distance from each edge should be equal to (100% - var(--s))/2.

Diagram of the widths needed to complete the design.

You might be wondering why we’re using clip-path at all here. We’re using it with the nested image to have a consistent gap. If we were to remove it, you would notice that we don’t have the same gap between all the images. This way, we’re cutting a little bit from the fifth image to get the proper spacing around it.

The complete code again:

.gallery {
  --s: 200px; /* controls the image size */
  --g: 10px;  /* controls the gap between images */
  
  display: grid;
  gap: var(--g);
  grid-template-columns: repeat(2, auto);
  position: relative;
}

.gallery > img {
  width: var(--s);
  aspect-ratio: 1;
  object-fit: cover;
  mask: conic-gradient(from var(--_a), #0000 90deg, #000 0);
}

.gallery > img:nth-child(1) {--_a: 90deg}
.gallery > img:nth-child(2) {--_a:180deg}
.gallery > img:nth-child(3) {--_a:  0deg}
.gallery > img:nth-child(4) {--_a:-90deg}
.gallery > img:nth-child(5) {
  position: absolute;
  inset: calc(50% - .5*var(--s));
  clip-path: inset(calc(var(--g) / 4));
}

Now, many of you might also be wondering: why all the complex stuff when we can place the last image on the top and add a border to it? That would hide the images underneath the nested image without a mask, right?

That’s true, and we will get the following:

No mask, no clip-path. Yes, the code is easy to understand, but there is a little drawback: the border color needs to be the same as the main background to make the illusion perfect. This little drawback is enough for me to make the code more complex in exchange for real transparency independent of the background. I am not saying a border approach is bad or wrong. I would recommend it in most cases where the background is known. But we are here to explore new stuff and, most important, build components that don’t depend on their environment.

Let’s try another shape this time:

This time, we made the nested image a circle instead of a square. That’s an easy task with border-radius But we need to use a circular cut-out for the other images. This time, though, we will rely on a radial-gradient() instead of a conic-gradient() to get that nice rounded look.

.gallery > img {
  mask: 
    radial-gradient(farthest-side at var(--_a),
      #0000 calc(50% + var(--g)/2), #000 calc(51% + var(--g)/2));
}
.gallery > img:nth-child(1) { --_a: calc(100% + var(--g)/2) calc(100% + var(--g)/2); }
.gallery > img:nth-child(2) { --_a: calc(0%   - var(--g)/2) calc(100% + var(--g)/2); }
.gallery > img:nth-child(3) { --_a: calc(100% + var(--g)/2) calc(0%   - var(--g)/2); }
.gallery > img:nth-child(4) { --_a: calc(0%   - var(--g)/2) calc(0%   - var(--g)/2); }

All the images use the same configuration as the previous example, but we update the center point each time.

Diagram showing the center values for each quadrant of the grid.

The above figure illustrates the center point for each circle. Still, in the actual code, you will notice that I am also accounting for the gap to ensure all the points are at the same position (the center of the grid) to get a continuous circle if we combine them.

Now that we have our layout let’s talk about the hover effect. In case you didn’t notice, a cool hover effect increases the size of the nested image and adjusts everything else accordingly. Increasing the size is a relatively easy task, but updating the gradient is more complicated since, by default, gradients cannot be animated. To overcome this, I will use a font-size hack to be able to animate the radial gradient.

If you check the code of the gradient, you can see that I am adding 1em:

mask: 
    radial-gradient(farthest-side at var(--_a),
      #0000 calc(50% + var(--g)/2 + 1em), #000 calc(51% + var(--g)/2 + 1em));

It’s known that em units are relative to the parent element’s font-size, so changing the font-size of the .gallery will also change the computed em value — this is the trick we are using. We are animating the font-size from a value of 0 to a given value and, as a result, the gradient is animated, making the cut-out part larger, following the size of the nested image that is getting bigger.

Here is the code that highlights the parts involved in the hover effect:

.gallery {
  --s: 200px; /* controls the image size */
  --g: 10px; /* controls the gaps between images */

  font-size: 0; /* initially we have 1em = 0 */
  transition: .5s;
}
/* we increase the cut-out by 1em */
.gallery > img {
  mask: 
    radial-gradient(farthest-side at var(--_a),
      #0000 calc(50% + var(--g)/2 + 1em), #000 calc(51% + var(--g)/2 + 1em));
}
/* we increase the size by 2em */
.gallery > img:nth-child(5) {
  width: calc(var(--s) + 2em);
}
/* on hover 1em = S/5 */
.gallery:hover {
  font-size: calc(var(--s) / 5);
}

The font-size trick is helpful if we want to animate gradients or other properties that cannot be animated. Custom properties defined with @property can solve such a problem, but support for it is still lacking at the time of writing.

I discovered the font-size trick from @SelenIT2 while trying to solve a challenge on Twitter.

Another shape? Let’s go!

This time we clipped the nested image into the shape of a rhombus. I’ll let you dissect the code as an exercise to figure out how we got here. You will notice that the structure is the same as in our examples. The only differences are how we’re using the gradient to create the shape. Dig in and learn!

Circular Image Grid

We can combine what we’ve learned here and in previous articles to make an even more exciting image grid. This time, let’s make all the images in our grid circular and, on hover, expand an image to reveal the entire thing as it covers the rest of the photos.

The HTML and CSS structure of the grid is nothing new from before, so let’s skip that part and focus instead on the circular shape and hover effect we want.

We are going to use clip-path and its circle() function to — you guessed it! — cut a circle out of the images.

Showing the two states of an image, the natural state on the left, and the hovered state on the right, including the clip-path values to create them.

That figure illustrates the clip-path used for the first image. The left side shows the image’s initial state, while the right shows the hovered state. You can use this online tool to play and visualize the clip-path values.

For the other images, we can update the center of the circle (70% 70%) to get the following code:

.gallery > img:hover {
  --_c: 50%; /* same as "50% at 50% 50%" */
}
.gallery > img:nth-child(1) {
  clip-path: circle(var(--_c, 55% at 70% 70%));
}
.gallery > img:nth-child(2) {
  clip-path: circle(var(--_c, 55% at 30% 70%));
}
.gallery > img:nth-child(3) {
  clip-path: circle(var(--_c, 55% at 70% 30%));
}
.gallery > img:nth-child(4) {
  clip-path: circle(var(--_c, 55% at 30% 30%));
}

Note how we are defining the clip-path values as a fallback inside var(). This way allows us to more easily update the value on hover by setting the value of the --_c variable. When using circle(), the default position of the center point is 50% 50%, so we get to omit that for more concise code. That’s why you see that we are only setting 50% instead of 50% at 50% 50%.

Then we increase the size of our image on hover to the overall size of the grid so we can cover the other images. We also ensure the z-index has a higher value on the hovered image, so it is the top one in our stacking context.

.gallery {
  --s: 200px; /* controls the image size */
  --g: 8px;   /* controls the gap between images */

  display: grid;
  grid: auto-flow var(--s) / repeat(2, var(--s));
  gap: var(--g);
}

.gallery > img {
  width: 100%; 
  aspect-ratio: 1;
  cursor: pointer;
  z-index: 0;
  transition: .25s, z-index 0s .25s;
}
.gallery > img:hover {
  --_c: 50%; /* change the center point on hover */
  width: calc(200% + var(--g));
  z-index: 1;
  transition: .4s, z-index 0s;
}

.gallery > img:nth-child(1){
  clip-path: circle(var(--_c, 55% at 70% 70%));
  place-self: start;
}
.gallery > img:nth-child(2){
  clip-path: circle(var(--_c, 55% at 30% 70%));
  place-self: start end;
}
.gallery > img:nth-child(3){
  clip-path: circle(var(--_c, 55% at 70% 30%));
  place-self: end start;
}
.gallery > img:nth-child(4){
  clip-path: circle(var(--_c, 55% at 30% 30%));
  place-self: end;
}

What’s going on with the place-self property? Why do we need it and why does each image have a specific value?

Do you remember the issue we had in the previous article when creating the grid of puzzle pieces? We increased the size of the images to create an overflow, but the overflow of some images was incorrect. We fixed them using the place-self property.

Same issue here. We are increasing the size of the images so each one overflows its grid cells. But if we do nothing, all of them will overflow on the right and bottom sides of the grid. What we need is:

  1. the first image to overflow the bottom-right edge (the default behavior),
  2. the second image to overflow the bottom-left edge,
  3. the third image to overflow the top-right edge, and
  4. the fourth image to overflow the top-left edge.

To get that, we need to place each image correctly using the place-self property.

Diagram showing the place-self property values for each quadrant of the grid.

In case you are not familiar with place-self, it’s the shorthand for justify-self and align-self to place the element horizontally and vertically. When it takes one value, both alignments use that same value.

Expanding Image Panels

In a previous article, I created a cool zoom effect that applies to a grid of images where we can control everything: number of rows, number of columns, sizes, scale factor, etc.

A particular case was the classic expanding panels, where we only have one row and a full-width container.

We will take this example and combine it with shapes!

Before we continue, I highly recommend reading my other article to understand how the tricks we’re about to cover work. Check that out, and we’ll continue here to focus on creating the panel shapes.

First, let’s start by simplifying the code and removing some variables

We only need one row and the number of columns should adjust based on the number of images. That means we no longer need variables for the number of rows (--n) and columns (--m ) but we need to use grid-auto-flow: column, allowing the grid to auto-generate columns as we add new images. We will consider a fixed height for our container; by default, it will be full-width.

Let’s clip the images into a slanted shape:

A headshot of a calm red wolf looking downward with vertices overlayed showing the clip-path property points.
clip-path: polygon(S 0%, 100% 0%, (100% - S) 100%, 0% 100%);

Once again, each image is contained in its grid cell, so there’s more space between the images than we’d like:

A six-panel grid of slanted images of various wild animals showing the grid lines and gaps.

We need to increase the width of the images to create an overlap. We replace min-width: 100% with min-width: calc(100% + var(--s)), where --s is a new variable that controls the shape.

Now we need to fix the first and last images, so they sort of bleed off the page without gaps. In other words, we can remove the slant from the left side of the first image and the slant from the right side of the last image. We need a new clip-path specifically for those two images.

We also need to rectify the overflow. By default, all the images will overflow on both sides, but for the first one, we need an overflow on the right side while we need a left overflow for the last image.

.gallery > img:first-child {
  min-width: calc(100% + var(--s)/2);
  place-self: start;
  clip-path: polygon(0 0,100% 0,calc(100% - var(--s)) 100%,0 100%);
}
.gallery > img:last-child {
  min-width: calc(100% + var(--s)/2);
  place-self: end;
  clip-path: polygon(var(--s) 0,100% 0,100% 100%,0 100%);
}

The final result is a nice expanding panel of slanted images!

We can add as many images as you want, and the grid will adjust automatically. Plus, we only need to control one value to control the shape!

We could have made this same layout with flexbox since we are dealing with a single row of elements. Here is my implementation.

Sure, slanted images are cool, but what about a zig-zag pattern? I already teased this one at the end of the last article.

All I’m doing here is replacing clip-path with mask… and guess what? I already have a detailed article on creating that zig-zag shape — not to mention an online generator to get the code. See how all everything comes together?

The trickiest part here is to make sure the zig-zags are perfectly aligned, and for this, we need to add an offset for every :nth-child(odd) image element.

.gallery > img {
  mask: 
    conic-gradient(from -135deg at right, #0000, #000 1deg 89deg, #0000 90deg) 
      100% calc(50% + var(--_p, 0%))/51% calc(2*var(--s)) repeat-y,
    conic-gradient(from   45deg at left,  #0000, #000 1deg 89deg, #0000 90deg) 
      0%   calc(50% + var(--_p, 0%))/51% calc(2*var(--s)) repeat-y;
}
/* we add an offset to the odd elements */
.gallery > img:nth-child(odd) {
  --_p: var(--s);
}
.gallery > img:first-child {
  mask: 
    conic-gradient(from -135deg at right, #0000, #000 1deg 89deg, #0000 90deg) 
      0 calc(50% + var(--_p, 0%))/100% calc(2*var(--s));
}
.gallery > img:last-child {
  mask: 
    conic-gradient(from 45deg at left, #0000, #000 1deg 89deg, #0000 90deg) 
      0 calc(50% + var(--_p, 0%)) /100% calc(2*var(--s));
}

Note the use of the --_p variable, which will fall back to 0% but will be equal to --_s for the odd images.

Here is a demo that illustrates the issue. Hover to see how the offset — defined by --_p — is fixing the alignment.

Also, notice how we use a different mask for the first and last image as we did in the previous example. We only need a zig-zag on the right side of the first image and the left side of the last image.

And why not rounded sides? Let’s do it!

I know that the code may look scary and tough to understand, but all that’s going on is a combination of different tricks we’ve covered in this and other articles I’ve already shared. In this case, I use the same code structure as the zig-zag and the slanted shapes. Compare it with those examples, and you will find no difference! Those are the same tricks in my previous article about the zoom effect. Then, I am using my other writing and my online generator to get the code for the mask that creates those rounded shapes.

If you recall what we did for the zig-zag, we had used the same mask for all the images but then had to add an offset to the odd images to create a perfect overlap. In this case, we need a different mask for the odd-numbered images.

The first mask:

mask: 
  linear-gradient(-90deg,#0000 calc(2*var(--s)),#000 0) var(--s),
  radial-gradient(var(--s),#000 98%,#0000) 50% / calc(2*var(--s)) calc(1.8*var(--s)) space repeat;

The second one:

mask:
  radial-gradient(calc(var(--s) + var(--g)) at calc(var(--s) + var(--g)) 50%,#0000 98% ,#000) 
  calc(50% - var(--s) - var(--g)) / 100% calc(1.8*var(--s))

The only effort I did here is update the second mask to include the gap variable (--g) to create that space between the images.

The final touch is to fix the first and last image. Like all the previous examples, the first image needs a straight left edge while the last one needs a straight right edge.

For the first image, we always know the mask it needs to have, which is the following:

.gallery > img:first-child {
  mask: 
    radial-gradient(calc(var(--s) + var(--g)) at right, #0000 98%, #000) 50% / 100% calc(1.8 * var(--s));
}
A brown bear headshot with a wavy pattern for the right border.

For the last image, it depends on the number of elements, so it matters if that element is :nth-child(odd) or :nth-child(even).

The complete grid of wild animal photos with all of the correct borders and gaps between images.
.gallery > img:last-child:nth-child(even) {
  mask: 
    linear-gradient(to right,#0000 var(--s),#000 0),
    radial-gradient(var(--s),#000 98%,#0000) left / calc(2*var(--s)) calc(1.8*var(--s)) repeat-y
}
A single-row grid of three wild animal photos with wavy borders where the last image is an odd-numbered element.
.gallery > img:last-child:nth-child(odd) {
  mask: 
    radial-gradient(calc(var(--s) + var(--g)) at left,#0000 98%,#000) 50% / 100% calc(1.8*var(--s))
}

That’s all! Three different layouts but the same CSS tricks each time:

  • the code structure to create the zoom effect
  • a mask or clip-path to create the shapes
  • a separate configuration for the odd elements in some cases to make sure we have a perfect overlap
  • a specific configuration for the first and last image to keep the shape on only one side.

And here is a big demo with all of them together. All you need is to add a class to activate the layout you want to see.

And here is the one with the Flexbox implementation

Wrapping up

Oof, we are done! I know there are many CSS tricks and examples between this article and the last one, not to mention all of the other tricks I’ve referenced here from other articles I’ve written. It took me time to put everything together, and you don’t have to understand everything at once. One reading will give you a good overview of all the layouts, but you may need to read the article more than once and focus on each example to grasp all the tricks.

Did you notice that we didn’t touch the HTML at all other than perhaps the number of images in the markup? All the layouts we made share the same HTML code, which is nothing but a list of images.

Before I end, I will leave you with one last example. It’s a “versus” between two anime characters with a cool hover effect.

What about you? Can you create something based on what you have learned? It doesn’t need to be complex — imagine something cool or funny like I did with that anime matchup. It can be a good exercise for you, and we may end with an excellent collection in the comment section.


CSS Grid and Custom Shapes, Part 2 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/css-grid-and-custom-shapes-part-2/feed/ 4 372396
CSS Grid and Custom Shapes, Part 1 https://css-tricks.com/css-grid-and-custom-shapes-part-1/ https://css-tricks.com/css-grid-and-custom-shapes-part-1/#comments Mon, 15 Aug 2022 13:13:47 +0000 https://css-tricks.com/?p=367324 In a previous article, I looked at CSS Grid’s ability to create complex layouts using its auto-placement powers. I took that one step further in another article that added a zooming hover effect to images in a grid layout


CSS Grid and Custom Shapes, Part 1 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
In a previous article, I looked at CSS Grid’s ability to create complex layouts using its auto-placement powers. I took that one step further in another article that added a zooming hover effect to images in a grid layout. This time, I want to dive into another type of grid, one that works with shapes.

Like, what if the images aren’t perfectly square but instead are shaped like hexagons or rhombuses? Spoiler alert: we can do it. In fact, we’re going to combine CSS Grid techniques we’ve looked at and drop in some CSS clip-path and mask magic to create fancy grids of images for just about any shape you can imagine!

CSS Grid and Custom Shapes series

Let’s start with some markup

Most of the layouts we are going to look at may look easy to achieve at first glance, but the challenging part is to achieve them with the same HTML markup. We can use a lot of wrappers, divs, and whatnot, but the goal of this post is to use the same and smallest amount of HTML code and still get all the different grids we want. After all, what’s CSS but a way to separate styling and markup? Our styling should not depend on the markup, and vice versa.

This said, let’s start with this:

<div class="gallery">
  <img src="..." alt="...">
  <img src="..." alt="...">
  <img src="..." alt="...">
  <img src="..." alt="...">
  <!-- as many times as we want -->
</div>

A container with images is all that we need here. Nothing more!

CSS Grid of Hexagons

This is also sometimes referred to as a “honeycomb” grid.

There are already plenty of other blog posts out there that show how to make this. Heck, I wrote one here on CSS-Tricks! That article is still good and goes way deep on making a responsive layout. But for this specific case, we are going to rely on a much simpler CSS approach.

First, let’s use clip-path on the images to create the hexagon shape and we place all of them in the same grid area so they overlap.

.gallery {
  --s: 150px; /* controls the size */
  display: grid;
}

.gallery > img {
  grid-area: 1/1;
  width: var(--s);
  aspect-ratio: 1.15;
  object-fit: cover;
  clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0 50%);
}
clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0 50%)

Nothing fancy yet. All the images are hexagons and above each other. So it looks like all we have is a single hexagon-shaped image element, but there are really seven.

The next step is to apply a translation to the images to correctly place them on the grid.

Notice that we still want one of the images to remain in the center. The rest are placed around it using CSS translate and good ol’ fashioned geometry. Here’s are the mock formulas I came up with for each image in the grid:

translate((height + gap)*sin(0deg), (height + gap)*cos(0))
translate((height + gap)*sin(60deg), (height + gap)*cos(60deg))
translate((height + gap)*sin(120deg), (height + gap)*cos(120deg))
translate((height + gap)*sin(180deg), (height + gap)*cos(180deg))
translate((height + gap)*sin(240deg), (height + gap)*cos(240deg))
translate((height + gap)*sin(300deg), (height + gap)*cos(300deg))

A few calculations and optimization later (let’s skip that boring part, right?) we get the following CSS:

.gallery {
  --s: 150px; /* control the size */
  --g: 10px;  /* control the gap */
  display: grid;
}
.gallery > img {
  grid-area: 1/1;
  width: var(--s);
  aspect-ratio: 1.15;
  object-fit: cover;
  clip-path: polygon(25% 0%, 75% 0%, 100% 50% ,75% 100%, 25% 100%, 0 50%);
  transform: translate(var(--_x,0), var(--_y,0));
}
.gallery > img:nth-child(1) { --_y: calc(-100% - var(--g)); }
.gallery > img:nth-child(7) { --_y: calc( 100% + var(--g)); }
.gallery > img:nth-child(3),
.gallery > img:nth-child(5) { --_x: calc(-75% - .87*var(--g)); }
.gallery > img:nth-child(4),
.gallery > img:nth-child(6) { --_x: calc( 75% + .87*var(--g)); }
.gallery > img:nth-child(3),
.gallery > img:nth-child(4) { --_y: calc(-50% - .5*var(--g)); }
.gallery > img:nth-child(5), 
.gallery > img:nth-child(6) { --_y: calc( 50% + .5*var(--g)); }

Maybe that’ll be easier when we get real trigonometry functions in CSS!

Each image is translated by the --_x and --_y variables that are based on those formulas. Only the second image (nth-child(2)) is undefined in any selector because it’s the one in the center. It can be any image if you decide to use a different order. Here’s the order I’m using:

With only a few lines of code, we get a cool grid of images. To this, I added a little hover effect to the images to make things fancier.

Guess what? We can get another hexagon grid by simply updating a few values.

If you check the code and compare it with the previous one you will notice that I have simply swapped the values inside clip-path and I switched between --x and --y. That’s all!

CSS Grid of Rhombuses

Rhombus is such a fancy word for a square that’s rotated 45 degrees.

Same HTML, remember? We first start by defining a 2×2 grid of images in CSS:

.gallery {
  --s: 150px; /* controls the size */

  display: grid;
  gap: 10px;
  grid: auto-flow var(--s) / repeat(2, var(--s));
  place-items: center;
}
.gallery > img {
  width: 100%; 
  aspect-ratio: 1;
  object-fit: cover;
}

The first thing that might catch your eye is the grid property. It’s pretty uncommonly used but is super helpful in that it’s a shorthand that lets you define a complete grid in one declaration. It’s not the most intuitive — and not to mention readable — property, but we are here to learn and discover new things, so let’s use it rather than writing out all of the individual grid properties.

grid: auto-flow var(--s) / repeat(2,var(--s));

/* is equivalent to this: */
grid-template-columns: repeat(2, var(--s));
grid-auto-rows: var(--s);

This defines two columns equal to the --s variable and sets the height of all the rows to --s as well. Since we have four images, we will automatically get a 2×2 grid.

Here’s another way we could have written it:

grid-template-columns: repeat(2, var(--s));
grid-template-rows: repeat(2, var(--s));

…which can be reduced with the grid shorthand:

grid: repeat(2,var(--s)) / repeat(2,var(--s));

After setting the grid, we rotate it and the images with CSS transforms and we get this:

Note how I rotate them both by 45deg, but in the opposite direction.

.gallery {
  /* etc. */
  transform: rotate(45deg);
}
.gallery > img {
  /* etc. */
  transform: rotate(-45deg);
}

Rotating the images in the negative direction prevents them from getting rotated with the grid so they stay straight. Now, we apply a clip-path to clip a rhombus shape out of them.

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

We are almost done! We need to rectify the size of the image to make them fit together. Otherwise, they’re spaced far apart to the point where it doesn’t look like a grid of images.

The image is within the boundary of the green circle, which is the inscribed circle of the grid area where the image is placed. What we want is to make the image bigger to fit inside the red circle, which is the circumscribed circle of the grid area.

Don’t worry, I won’t introduce any more boring geometry. All you need to know is that the relationship between the radius of each circle is the square root of 2 (sqrt(2)). This is the value we need to increase the size of our images to fill the area. We will use 100%*sqrt(2) = 141% and be done!

.gallery {
  --s: 150px; /* control the size */

  display: grid;
  grid: auto-flow var(--s) / repeat(2,var(--s));
  gap: 10px;
  place-items: center;
  transform: rotate(45deg);
}
.gallery > img {
  width: 141%; /* 100%*sqrt(2) = 141% */
  aspect-ratio: 1;
  object-fit: cover;
  transform: rotate(-45deg);
  clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}

Like the hexagon grid, we can make things fancier with that nice zooming hover effect:

CSS Grid of Triangular Shapes

You probably know by now that the big trick is figuring out the clip-path to get the shapes we want. For this grid, each element has its own clip-path value whereas the last two grids worked with a consistent shape. So, this time around, it’s like we’re working with a few different triangular shapes that come together to form a rectangular grid of images.

The three images at the top
The three images at the bottom

We place them inside a 3×2 grid with the following CSS:

.gallery {
  display: grid;
  gap: 10px; 
  grid-template-columns: auto auto auto; /* 3 columns */
  place-items: center;
}
.gallery > img {
  width: 200px; /* controls the size */
  aspect-ratio: 1;
  object-fit: cover;
}
/* the clip-path values */
.gallery > img:nth-child(1) { clip-path: polygon(0 0, 50% 0, 100% 100% ,0 100%); }
.gallery > img:nth-child(2) { clip-path: polygon(0 0, 100% 0, 50% 100%); }
.gallery > img:nth-child(3) { clip-path: polygon(50% 0, 100% 0, 100% 100%, 0 100%); }
.gallery > img:nth-child(4) { clip-path: polygon(0 0, 100% 0, 50% 100%, 0 100%); }
.gallery > img:nth-child(5) { clip-path: polygon(50% 0, 100% 100%, 0% 100%); }
.gallery > img:nth-child(6) { clip-path: polygon(0 0, 100% 0 ,100% 100%, 50% 100%); } }

Here’s what we get:

The final touch is to make the width of the middle column equal 0 to get rid of the spaces between the images. The same sort of spacing problem we had with the rhombus grid, but with a different approach for the shapes we’re using:

grid-template-columns: auto 0 auto;

I had to fiddle with the clip-path values to make sure they would all appear to fit together nicely like a puzzle. The original images overlap when the middle column has zero width, but after slicing the images, the illusion is perfect:

CSS Pizza Pie Grid

Guess what? We can get another cool grid by simply adding border-radius and overflow to our grid or triangular shapes. 🎉

CSS Grid of Puzzle Pieces

This time we are going to play with the CSS mask property to make the images look like pieces of a puzzle.

If you haven’t used mask with CSS gradients, I highly recommend this other article I wrote on the topic because it’ll help with what comes next. Why gradients? Because that’s what we’re using to get the round notches in the puzzle piece shapes.

Setting up the grid should be a cinch by now, so let’s focus instead on the mask part.

As illustrated in the above demo, we need two gradients to create the final shape. One gradient creates a circle (the green part) and the other creates the right curve while filling in the top part.

--g: 6px; /* controls the gap */
--r: 42px;  /* control the circular shapes */

background: 
  radial-gradient(var(--r) at left 50% bottom var(--r), green 95%, #0000),
  radial-gradient(calc(var(--r) + var(--g)) at calc(100% + var(--g)) 50%, #0000 95%, red)
  top/100% calc(100% - var(--r)) no-repeat;

Two variables control the shape. The --g variable is nothing but the grid gap. We need to account for the gap to correctly place our circles so they overlap perfectly when the whole puzzle is assembled. The --r variable controls the size of circular parts of the puzzle shape.

Now we take the same CSS and update a few values in it to create the three other shapes:

We have the shapes, but not the overlapping edges we need to make them fit together. Each image is limited to the grid cell it’s in, so it makes sense why the shapes are sort of jumbled at the moment:

We need to create an overflow by increasing the height/width of the images. From the above figure, we have to increase the height of the first and fourth images while we increase the width of the second and third ones. You have probably already guessed that we need to increase them using the --r variable.

.gallery > img:is(:nth-child(1),:nth-child(4)) {
  width: 100%;
  height: calc(100% + var(--r));
}
.gallery > img:is(:nth-child(2),:nth-child(3)) {
  height: 100%;
  width: calc(100% + var(--r));
}

We are getting closer!

We created the overlap but, by default, our images either overlap on the right (if we increase the width) or the bottom (if we increase the height). But that’s not what we want for the second and fourth images. The fix is to use place-self: end on those two images and our full code becomes this:

Here is another example where I am using a conic gradient instead of a radial gradient. This gives us triangular puzzle pieces while keeping the same underlying HTML and CSS.

A last one! This time I am using clip-path and since it’s a property we can animate, we get a cool hover by simply updating the custom property that controls the shape.

Wrapping up

That’s all for this first part! By combining the things we’ve already learned about CSS Grid with some added clip-path and mask magic, we were able to make grid layouts featuring different kinds of shapes. And we used the same HTML markup each time! And the markup itself is nothing more than a container with a handful of image elements!

In the second part, we are going to explore more complex-looking grids with more fancy shapes and hover effects.

I’m planning to take the demo of expanding image panels we made together in this other article:

…and transform it into a zig-zag image panels! And this is only one example among the many we will discover in the next article.


CSS Grid and Custom Shapes, Part 1 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/css-grid-and-custom-shapes-part-1/feed/ 12 367324
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.

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.


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 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.

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.


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 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
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
Fancy CSS Borders Using Masks https://css-tricks.com/css-borders-using-masks/ https://css-tricks.com/css-borders-using-masks/#comments Wed, 26 Jan 2022 14:26:49 +0000 https://css-tricks.com/?p=362018 Have you ever tried to make CSS borders in a repeating zig-zag pattern? Like where a colored section of a website ends and another differently colored section begins — not with a straight line, but angled zig zags, rounded humps, …


Fancy CSS Borders Using Masks originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Have you ever tried to make CSS borders in a repeating zig-zag pattern? Like where a colored section of a website ends and another differently colored section begins — not with a straight line, but angled zig zags, rounded humps, or waves. There are a number of ways you could do this sort of CSS border, dating all the way back to using a background-image. But we can get more modern and programmatic with it. In this article, we’ll look at some modern CSS mask techniques to achieve the look.

Before we dig into the technical parts, though, let’s take a look at what we are building. I have made a CSS border generator where you can easily generate any kind of border within a few seconds and get the CSS code.

Did you see that? With the CSS mask property and a few CSS gradients, we get a responsive and cool-looking border — all with CSS by itself. Not only this, but such effect can be applied to any element where we can have any kind of coloration (e.g. image, gradient, etc). We get all this without extra elements, pseudo elements, or magic numbers coming from nowhere!

Oh great! All I have to do is to copy/paste code and it’s done!

True, but it’s good to understand the logic to be able to manually adjust the code if you need to.

Masking things

Since all our effects rely on the CSS mask property, let’s take a quick refresh on how it works. Straight from the spec:

The effect of applying a mask to a graphical object is as if the graphical object will be painted onto the background through a mask, thus completely or partially masking out parts of the graphical object.

If we check the formal syntax of the mask property we can see it accepts an <image> as a value, meaning either a URL of an image or a color gradient. Gradients are what we’ll be using here. Let’s start with basic examples:

In the first example of this demo, a gradient is used to make it appear as though the image is fading away. The second example, meanwhile, also uses a gradient, but rather than a soft transition between colors, a hard color stop is used to hide (or mask) half of the image. That second example illustrates the technique we will be using to create our fancy borders.

Oh, and the CSS mask property can take multiple gradients as long as they are comma-separated. That means we have even more control to mask additional parts of the image.

That example showing multiple masking gradients may look a bit tricky at first glance, but what’s happening is the same as applying the multiple gradients on the background property. But instead of using a color that blends in with the page background, we use a “transparent” black value (#0000) for the hidden parts of the image and full black (#000) for the visible parts.

That’s it! Now we can tackle our fancy borders.

Zig-Zag CSS borders

As we saw in the video at the start of this article, the generator can apply borders on one side, two sides, or all sides. Let’s start with the bottom side using a step-by-step illustration:

  1. We start by adding a conic-gradient() with a specific dimension placed in the middle.
  2. Next, we repeat that gradient (by removing no-repeat ) and we already see the zig-zag shape!
  3. Gradients are known to have anti-aliasing issues creating jagged edges (especially on Chrome). To avoid this, we add a slight transition between the colors, changing blue 90deg, green 0 to green, blue 1deg 89deg, green 90deg.
  4. Last, we use everything inside the mask property!

We can extract two variables from those steps to define our shape: size (40px) and angle (90deg). Here’s how we can express that using placeholders for those variables. I will be using JavaScript to replace those variables with their final values.

mask:
  conic-gradient(
    from {-angle/2} at bottom,
    #0000, #000 1deg {angle - 1} ,#0000 {angle}
  ) 50%/{2*size*tan(angle/2)} 100%;

We can use CSS custom properties for the size and the angle, but trigonometric functions are unsupported features at this moment. In the future, we’ll be able to do something like this:

--size: 40px;
--angle: 90deg;
mask:
  conic-gradient(
    from calc(var(--angle)/-2) at bottom,
    #0000, #000 1deg calc(var(--angle) - 1deg), #0000 var(--angle)
  ) 50%/calc(2*var(--size)*tan(var(--angle)/2)) 100%;

Similar to the bottom border, the top one will have almost the same code with a few adjustments:

mask:
  conic-gradient(
    from {180deg - angle/2} at top,
    #0000, #000 1deg {angle - 1}, #0000 {angle}
  ) 50%/{2*size*tan(angle/2)} 100%;

We changed bottom with top, then updated the rotation of the gradient to 180deg - angle/2 instead of -angle/2. As simple as that!

That’s the pattern we can use for the rest of the sides, like the left:

mask:
  conic-gradient(
    from {90deg - angle/2} at left,
    #0000, #000 1deg {angle - 1}, #0000 {angle}
  ) 50%/100% {2*size*tan(angle/2)};

…and the right:

mask:
  conic-gradient(
    from {-90deg - angle/2} at right,
    #0000, #000 1deg {angle - 1}, #0000 {angle}
  ) 50%/100% {2*size*tan(angle/2)};

Let’s make the borders for when they’re applied to two sides at once. We can actually reuse the same code. To get both the top and bottom borders, we simply combine the code of both the top and bottom border.

mask:
  conic-gradient(
    from {-angle/2} at bottom,
    #0000, #000 1deg {angle - 1}, #0000 {angle}
  ) bottom/{2*size*tan(angle/2)} 51% repeat-x;
  conic-gradient(
    from {180deg - angle/2} at top, 
    #0000, #000 1deg {angle - 1}, #0000 {angle}
  ) top   /{2*size*tan(angle/2)} 51% repeat-x;

The same goes when applying borders to the left and right sides together:

mask:
  conic-gradient(
    from {90deg - angle/2} at left,
    #0000, #000 1deg {angle - 1}, #0000 {angle}
  ) left /51% {2*size*tan(angle/2)} repeat-y,
  conic-gradient(
    from {-90deg - angle/2} at right,
    #0000, #000 1deg {angle - 1}, #0000 {angle}
  ) right/51% {2*size*tan(angle/2)} repeat-y;

So, if we want to apply borders to all of the sides at once, we add all the gradients together, right?

Exactly! We have four conic gradients (one on each side) and one linear-gradient() in the middle. We set a fixed angle equal to 90deg because it the only one that results in nicer corners without weird overlapping. Note that I’m also using space instead of repeat-x or repeat-y to avoid bad result on corners like this:

Resizing a container with four sides configuration

Scooped CSS borders

Now let’s tackle scooped borders!

Oh no! another long explanation with a lot of calculation?!

Not at all! There is nothing to explain here. We take everything from the zig-zag example and update the conic-gradient() with a radial-gradient(). It’s even easier because we don’t have any angles to deal with — only the size variable.

Here is an illustration for one side to see how little we need to do to switch from the zig-zag border to the scooped border:

Again, all I did there was replace the conic-gradient() with this (using placeholders for size):

mask: radial-gradient({size} at bottom,#0000 98%,#000) 50% / {1.85*size} 100%;

What is the logic behind the magic numbers 1.85 and 98%?

Logically, we should use 100% instead of 98% to have a circle that touches the edges of the background area; but again, it’s the anti-aliasing issue and those jagged edges. We use a slightly smaller value to prevent weird overlapping.

The 1.85 value is more of a personal preference than anything. I initially used 2 which is the logical value to get a perfect circle, but the result doesn’t look quite as nice, so the smaller value creates a more seamless overlap between the circles.

Here’s the difference:

Now we need to replicate this for the rest of the sides, just as we did with the zig-zag CSS border, and guess what? We also need one gradient even for the case where we want to have the top/bottom or left/right version.

The only case where we need more than one gradient is the all sides configuration. Similar to the zig-zag border, We have to use four radial gradients and one linear gradient, but I had to introduce the CSS clip-path property to correct an overlapping issue at the corners. You can see the difference between with and without clip-path in the following demo:

It’s an eight-point path to cut the corners:

clip-path: polygon(
   {2*size} 0,calc(100% - {2*size}) 0,
   100% {2*size},100% calc(100% - {2*size}),
   calc(100% - {2*size}) 100%,{2*size} 100%,
   0 calc(100% - {2*size}),0 {2*size}
);

Scalloped CSS borders

For this border, we always need two gradients whatever the sides configuration. We use a radial gradient to create a repeated pattern of circles and a linear gradient to cover them and show only the sides we want.

Here is a demo to illustrate some of the cases:

Note how I’m using the round and space values. That’s to make sure we don’t cut off any of the circles and also avoid relying on more gradients. And, again, that 1.85 value is a personal preference value.

Wavy CSS borders

The wavy border is a kind of combination of the two previous borders. Here is an illustration to understand how we build the bottom wave.

We repeat that shape at the bottom and we are done.

mask: 
  radial-gradient({size} at 75% 100%,#0000 98%,#000) 50% calc(100% - {size})/{4*size} 100% repeat-x,
  radial-gradient({size} at 25% 50% ,#000 99%,#0000 101%) bottom/{4*size} {2*size} repeat-x;

We do the same process for the other sides as we did with the zig-zag and rounded CSS borders. All we need is to update a few variables to have a different wave for each side.

Showing part of the CSS for each side. You can find the full code over at the generator.

What about applying a wavy CSS border on all four sides? Will we have 8 gradients in total??”

Nope, and that’s because there is no demo where a wavy border is applied to all four sides. I was unable to find a combination of gradients that gives a good result on the corners. Maybe someone reading this knows a good approach? 😉

What we have done here is a simple case of a wavy border. I have another article where I go into fine detail about creating complex wavy shapes.

That’s borderline great stuff!

So, you know the ins and outs of my cool little online CSS border generator! Sure, you can use the code it spits out and do just fine — but now you have the secret sauce recipe that makes it work.

Specifically, we saw how gradients can be used to mask portions of an element. Then we went to work on multiple gradients to make certain shapes from those gradient CSS masks. And the result is a pattern that can be used along the edges of an element, creating the appearance of fancy borders that you might otherwise result to background-image for. Only this way, all it takes is swapping out some values to change the appearance rather than replace an entire raster image file or something.


Fancy CSS Borders Using Masks originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/css-borders-using-masks/feed/ 1 362018
CSS-ing Candy Ghost Buttons https://css-tricks.com/css-ing-candy-ghost-buttons/ https://css-tricks.com/css-ing-candy-ghost-buttons/#comments Mon, 01 Nov 2021 01:04:53 +0000 https://css-tricks.com/?p=354804 Recently, while looking for some ideas on what to code as I have zero artistic sense so the only thing I can do is find pretty things that other people have come up with and remake them with clean and …


CSS-ing Candy Ghost Buttons originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Recently, while looking for some ideas on what to code as I have zero artistic sense so the only thing I can do is find pretty things that other people have come up with and remake them with clean and compact code… I came across these candy ghost buttons!

They seemed like the perfect choice for a cool little thing I could quickly code. Less than fifteen minutes later, this was my Chromium result:

Chrome screenshot. Shows a four row, five column grid of candy ghost buttons with text and an icon following it. These buttons have an elongated pill-like shape, a transparent background and a continuous sweet pastel gradient for the border and the text and icon inside.
The pure CSS candy ghost buttons.

I thought the technique was worth sharing, so in this article, we’ll be going through how I first did it and what other options we have.

The starting point

A button is created with… are you ready for this? A button element! This button element has a data-ico attribute in which we drop an emoji. It also has a stop list custom property, --slist, set in the style attribute.

<button data-ico="👻" style="--slist: #ffda5f, #f9376b">boo!</button>

After writing the article, I learned that Safari has a host of problems with clipping to text, namely, it doesn’t work on button elements, or on elements with display: flex (and perhaps grid too?), not to mention the text of an element’s children. Sadly, this means all of the techniques presented here fail in Safari. The only workaround is to apply all the button styles from here on a span element nested inside the button, covering its parent’s border-box. And, in case this helps anyone else who, like me, is on Linux without physical access to an Apple device (unless you count the iPhone 5 someone on the fourth floor — who you don’t want to bother with stuff like this more than twice a month anyway — bought recently), I’ve also learned to use Epiphany in the future. Thanks to Brian for the suggestion!

For the CSS part, we add the icon in an ::after pseudo-element and use a grid layout on the button in order to have nice alignment for both the text and the icon. On the button, we also set a border, a padding, a border-radius, use the stop list, --slist, for a diagonal gradient and prettify the font.

button {
  display: grid;
  grid-auto-flow: column;
  grid-gap: .5em;
  border: solid .25em transparent;
  padding: 1em 1.5em;
  border-radius: 9em;
  background: 
    linear-gradient(to right bottom, var(--slist)) 
      border-box;
  font: 700 1.5em/ 1.25 ubuntu, sans-serif;
  text-transform: uppercase;
  
  &::after { content: attr(data-ico) }
}

There’s one thing to clarify about the code above. On the highlighted line, we set both the background-origin and the background-clip to border-box. background-origin both puts the 0 0 point for background-position in the top-left corner of the box it’s set to and gives us the box whose dimensions the background-size is relative to.

That is, if background-origin is set to padding-box, the 0 0 point for background-position is in the top left-corner of the padding-box. If background-origin is set to border-box, the 0 0 point for background-position is in the top-left corner of the border-box. If background-origin is set to padding-box, a background-size of 50% 25% means 50% of the padding-box width and 25% of the padding-box height. If background-origin is set to border-box, the same background-size of 50% 25% means 50% of the border-box width and 25% of the border-box height.

The default value for background-origin is padding-box, meaning that a default-sized 100% 100% gradient will cover the padding-box and then repeat itself underneath the border (where we cannot see it if the border is fully opaque). However, in our case, the border is fully transparent and we want our gradient to stretch across the entire border-box. This means we need to change the background-origin value to border-box.

Screenshot collage. Chrome on the left, Firefox on the right, showing differences between ghost emojis. The button has a pastel gradient background going along the main diagonal, the text 'Boo!' in black and a ghost emoji, which is going to look different depending on the OS and browser.
The result after applying the base styles (live demo).

The simple, but sadly non-standard Chromium solution

This involves using three mask layers and compositing them. If you need a refresher on mask compositing, you can check out this crash course.

Note that in the case of CSS mask layers, only the alpha channel matters, as every pixel of the masked element gets the alpha of the corresponding mask pixel, while the RGB channels don’t influence the result in any way, so they may be any valid value. Below, you can see the effect of a purple to transparent gradient overlay versus the effect of using the exact same gradient as a mask.

Screenshot. Shows two Halloween-themed cat pictures (the cat is protectively climbed on top of a Halloween pumpkin) side by side. The first one has a purple to transparent linear gradient overlay on top. The second one uses the exact same linear gradient as a mask. By default, CSS masks are alpha masks, meaning that every pixel of the masked element gets the alpha of the corresponding mask pixel.
Gradient overlay vs. the same gradient mask (live demo).

We’re going to start with the bottom two layers. The first one is a fully opaque layer, fully covering the entire border-box, meaning that it has an alpha of 1 absolutely everywhere. The other one is also fully opaque, but restricted (by using mask-clip) to the padding-box, which means that, while this layer has an alpha of 1 all across the padding-box, it’s fully transparent in the border area, having an alpha of 0 there.

If you have a hard time picturing this, a good trick is to think of an element’s layout boxes as nested rectangles, the way they’re illustrated below.

Illustration showing the layout boxes. The outermost box is the border-box. Inside it, a border-width away from the border limit, we have the padding-box. And finally, inside the padding-box, a padding away from the padding limit, we have the content-box.
The layout boxes (live demo).

In our case, the bottom layer is fully opaque (the alpha value is 1) across the entire orange box (the border-box). The second layer, that we place on top of the first one, is fully opaque (the alpha value is 1) all across the red box (the padding-box) and fully transparent (with an alpha of 0) in the area between the padding limit and the border limit.

One really cool thing about the limits of these boxes is that corner rounding is determined by the border-radius (and, in the case of the padding-box, by the border-width as well). This is illustrated by the interactive demo below, where we can see how the corner rounding of the border-box is given by the border-radius value, while the corner rounding of the padding-box is computed as the border-radius minus the border-width (limited at 0 in case the difference is a negative value).

Now let’s get back to our mask layers, one being fully opaque all across the entire border-box, while the one on top of it is fully opaque across the padding-box area and fully transparent in the border area (between the padding limit and the border limit). These two layers get composited using the exclude operation (called xor in the non-standard WebKit version).

Illustration. Shows the bottom two background layers in 3D. The first one from the bottom has an alpha of 1 all across the entire border-box. The second one, layered on top of it, has an alpha of 1 across the padding box, within the padding limit; it also has an alpha of 0 in the border area, outside the padding limit, but inside the border limit.
The two base layers (live demo).

The name of this operation is pretty suggestive in the situation where the alphas of the two layers are either 0 or 1, as they are in our case — the alpha of the first layer is 1 everywhere, while the alpha of the second layer (that we place on top of the first) is 1 inside the padding-box and 0 in the border area between the padding limit and the border limit.

In this situation, it’s pretty intuitive that the rules of boolean logic apply — XOR-ing two identical values gives us 0, while XOR-ing two different ones gives us 1.

All across the padding-box, both the first layer and the second one have an alpha of 1, so compositing them using this operation gives us an alpha of 0 for the resulting layer in this area. However, in the border area (outside the padding limit, but inside the border limit), the first layer has an alpha of 1, while the second one has an alpha of 0, so we get an alpha of 1 for the resulting layer in this area.

This is illustrated by the interactive demo below, where you can switch between viewing the two mask layers separated in 3D and viewing them stacked and composited using this operation.

Putting things into code, we have:

button {
  /* same base styles */
  --full: linear-gradient(red 0 0);
  -webkit-mask: var(--full) padding-box, var(--full);
  -webkit-mask-composite: xor;
  mask: var(--full) padding-box exclude, var(--full);
}

Before we move further, let’s discuss a few fine-tuning details about the CSS above.

First off, since the fully opaque layers may be anything (the alpha channel is fixed, always 1 and the RGB channels don’t mater), I usually make them red — only three characters! In the same vein, using a conic gradient instead of a linear one would also save us one character, but I rarely ever do that since we still have mobile browsers that support masking, but not conic gradients. Using a linear one ensures we have support all across the board. Well, save for IE and pre-Chromium Edge but, then again, not much cool shiny stuff works in those anyway.

Second, we’re using gradients for both layers. We’re not using a plain background-color for the bottom one because we cannot set a separate background-clip for the background-color itself. If we were to have the background-image layer clipped to the padding-box, then this background-clip value would also apply to the background-color underneath — it would be clipped to the padding-box too and we’d have no way to make it cover the entire border-box.

Third, we’re not explicitly setting a mask-clip value for the bottom layer since the default for this property is precisely the value we want in this case, border-box.

Fourth, we can include the standard mask-composite (supported by Firefox) in the mask shorthand, but not the non-standard one (supported by WebKit browsers).

And finally, we always set the standard version last so it overrides any non-standard version that may also be supported.

The result of our code so far (still cross-browser at this point) looks like below. We’ve also added a background-image on the root so that it’s obvious we have real transparency across the padding-box area.

Screenshot. The pastel gradient button is just a shadow of its former self. Well, just a border, that's all we can see of it. The entire area inside the padding limit has been masked out and we can now see through to the image background behind the button.
The result after masking out the entire padding-box (live demo).

This is not what we want. While we have a nice gradient border (and by the way, this is my preferred method of getting a gradient border since we have real transparency all across the padding-box and not just a cover), we are now missing the text.

So the next step is to add back the text using yet another mask layer on top of the previous ones, this time one that’s restricted to text (while also making the actual text fully transparent so that we can see the gradient background through it) and XOR this third mask layer with the result of XOR-ing the first two (result that can be seen in the screenshot above).

The interactive demo below allows viewing the three mask layers both separated in 3D as well as stacked and composited.

Note that the text value for mask-clip is non-standard, so, sadly, this only works in Chrome. In Firefox, we just don’t get any masking on the button anymore and having made the text transparent, we don’t even get graceful degradation.

button {
  /* same base styles */
  -webkit-text-fill-color: transparent;
  --full: linear-gradient(red 0 0);
  -webkit-mask: var(--full) text, var(--full) padding-box, var(--full);
  -webkit-mask-composite: xor;
  /* sadly, still same result as before :( */
  mask: var(--full) padding-box exclude, var(--full);
}

If we don’t want to make our button unusable this way, we should put the code that applies the mask and makes the text transparent in a @supports block.

button {
  /* same base styles */

  @supports (-webkit-mask-clip: text) {
    -webkit-text-fill-color: transparent;
    --full: linear-gradient(red 0 0);
    -webkit-mask: var(--full) text, var(--full) padding-box, var(--full);
    -webkit-mask-composite: xor;
  }
}
Screenshot collage. Chrome (left) vs. Firefox (right). In Chrome, we have a real pill-shaped pastel gradient ghost button. It has a transparent background that lets us see through to the image background behind our button and a continuous sweet pastel gradient for the border and the text and icon inside. In Firefox, we have the same pill-shaped, pastel background, black text and normal emoji button we had after setting the base styles. The ghost emoji is going to look different depending on the OS and browser - here it can be seen it has different looks in Chrome and Firefox.
The final result using the masking-only method (live demo).

I really like this method, it’s the simplest we have at this point and I’d really wish text was a standard value for mask-clip and all browsers supported it properly.

However, we also have a few other methods of achieving the candy ghost button effect, and although they’re either more convoluted or more limited than the non-standard Chromium-only one we’ve just discussed, they’re also better supported. So let’s take a look at those.

The extra pseudo-element solution

This involves setting the same initial styles as before, but, instead of using a mask, we clip the background to the text area.

button {
  /* same base styles */
  background: 
    linear-gradient(to right bottom, var(--slist)) 
    border-box;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent
}

Just like before, we need to also make the actual text transparent, so we can see through it to the pastel gradient background behind it that is now clipped to its shape.

Screenshot collage. Chrome (left) vs. Firefox (right), highlighting the differences in emoji shapes when they're part of knockout text. This is entirely normal and fine, as emojis look different depending on OS and browser.
Knockout button text (live demo).

Alright, we have the gradient text, but now we’re missing the gradient border. So we’re going to add it using an absolutely positioned ::before pseudo-element that covers the entire border-box area of the button and inherits the border, border-radius and background from its parent (save for the background-clip, which gets reset to border-box).

$b: .25em;

button {
  /* same as before */
  position: relative;
  border: solid $b transparent;
  
  &::before { 
    position: absolute;
    z-index: -1;
    inset: -$b;
    border: inherit;
    border-radius: inherit;
    background: inherit;
    background-clip: border-box;
    content: '';
  }
}

inset: -$b is a shorthand for:

top: -$b;
right: -$b;
bottom: -$b;
left: -$b

Note that we’re using the border-width value ($b) with a minus sign here. The 0 value would make the margin-box of the pseudo (identical to the border-box in this case since we have no margin on the ::before) only cover the padding-box of its button parent and we want it to cover the entire border-box. Also, the positive direction is inwards, but we need to go outwards by a border-width to get from the padding limit to the border limit, hence the minus sign — we’re going in the negative direction.

We’ve also set a negative z-index on this absolutely positioned element since we don’t want it to be on top of the button text and prevent us from selecting it. At this point, text selection is the only way we can distinguish the text from the background, but we’ll soon fix that!

Screenshot. Shows how text selection is the only way of still distinguishing the transparent text and gradient background clipped to text area button from its gradient background ::before pseudo that covers it fully.
The result after adding the gradient pseudo (live demo).

Note that since pseudo-element content isn’t selectable, the selection only includes the button’s actual text content and not the emoji in the ::after pseudo-element as well.

The next step is to add a two layer mask with an exclude compositing operation between them in order to leave just the border area of this pseudo-element visible.

button {
  /* same as before */
    
  &::before { 
    /* same as before */
    --full: linear-gradient(red 0 0);
    -webkit-mask: var(--full) padding-box, var(--full);
    -webkit-mask-composite: xor;
    mask: var(--full) padding-box exclude, var(--full);
  }
}

This is pretty much what we did for the actual button in one of the intermediate stages of the previous method.

Screenshot collage. Chrome (left) vs. Firefox (right). Both display a pill-shaped pastel gradient ghost button. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The final result using the extra pseudo method (live demo).

I find this to be the best approach in most cases when we want something cross-browser and that doesn’t include IE or pre-Chromium Edge, none of which ever supported masking.

The border-image solution

At this point, some of you may be screaming at the screen that there’s no need to use the ::before pseudo-element when we could use a gradient border-image to create this sort of a ghost button — it’s a tactic that has worked for over three quarters of a decade!

However, there’s a very big problem with using border-image for pill-shaped buttons: this property doesn’t play nice with border-radius, as it can be seen in the interactive demo below. As soon as we set a border-image on an element with border-radius, we lose the corner rounding of the border, even through, funny enough, the background will still respect this rounding.

Still, this may be a simple solution in the case where don’t need corner rounding or the desired corner rounding is at most the size of the border-width.

In the no corner rounding case, save for dropping the now pointless border-radius, we don’t need to change the initial styles much:

button {
  /* same base styles */
  --img: linear-gradient(to right bottom, var(--slist));
  border: solid .25em;
  border-image: var(--img) 1;
  background: var(--img) border-box;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}

The result can be seen below, cross-browser (should be supported supported even in pre-Chromium Edge).

Screenshot collage. Chrome (left) vs. Firefox (right). Both display a pastel gradient ghost button with no rounded corners. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The no corner rounding result using the border-image method (live demo).

The trick with the desired corner rounding being smaller than the border-width relies on the way border-radius works. When we set this property, the radius we set represents the rounding for the corners of the border-box. The rounding for the corners of the padding-box (which is the inner rounding of the border) is the border-radius minus the border-width if this difference is positive and 0 (no rounding) otherwise. This means we have no inner rounding for the border if the border-radius is smaller than or equal to the border-width.

In this situation, we can use the inset() function as a clip-path value since it also offers the possibility of rounding the corners of the clipping rectangle. If you need a refresher on the basics of this function, you can check out the illustration below:

Illustration of how inset(d round r) works. Shows the clipping rectangle inside the element's border-box, its edges all a distance d away from the border limit. The corners of this clipping rectangle all have a rounding r along both axes.
How the inset() function works.

inset() cuts out everything outside a clipping rectangle defined by the distances to the edges of the element’s border-box, specified the same way we’d specify margin, border or padding (with one, two, three or four values) and the corner rounding for this rectangle, specified the same way we’d specify border-radius (any valid border-radius value is also valid here).

In our case, the distances to the edges of the border-box are all 0 (we don’t want to chop anything off any of the edges of the button), but we have a rounding that has to be at most at big as the border-width so that not having any inner border rounding makes sense.

$b: .25em;

button {
  /* same as before */
  border: solid $b transparent;
  clip-path: inset(0 round $b)
}

Note that the clip-path is also going to cut out any outer shadows we may add on the button element, whether they’re added via box-shadow or filter: drop-shadow().

Screenshot collage. Chrome (left) vs. Firefox (right). Both display a pastel gradient ghost button with small rounded corners, the rounding radius being the same size as the border-width. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The small corner rounding result using the border-image method (live demo).

While this technique cannot achieve the pill shape look, it does have the advantage of having great support nowadays and it may be all we need in certain situations.

The three solutions discussed so far can be seen compiled in the demo below, which also comes with a YouTube link where you can see me code each of them from scratch if you prefer to learn by watching things being built on video rather than reading about them.

All these methods create real transparency in the padding-box outside the text, so they work for any background we may have behind the button. However, we also have a couple of other methods that may be worth mentioning, even though they come with restrictions in this department.

The cover solution

Just like the border-image approach, this is a pretty limited tactic. It doesn’t work unless we have a solid or a fixed background behind the button.

It involves layering backgrounds with different background-clip values, just like the cover technique for gradient borders. The only difference is that here we add one more gradient layer on top of the one emulating the background behind our button element and we clip this top layer to text.

$c: #393939;

html { background: $c; } 

button {
  /* same as before */
  --grad: linear-gradient(to right bottom, var(--slist));
  border: solid .25em transparent;
  border-radius: 9em;
  background: var(--grad) border-box, 
              linear-gradient($c 0 0) /* emulate bg behind button */, 
              var(--grad) border-box;
  -webkit-background-clip: text, padding-box, border-box;
  -webkit-text-fill-color: transparent;
}

Sadly, this approach fails in Firefox due to an old bug — just not applying any background-clip while also making the text transparent produces a pill-shaped button with no visible text.

Screenshot collage. Chrome (left) vs. Firefox (right). Chrome displays a pill-shaped pastel gradient ghost button. Firefox sadly only displays a pill-shaped button with no visible text.
The all background-clip cover solution (live demo).

We could still make it cross-browser by using the cover method for the gradient border on a ::before pseudo and background-clip: text on the actual button, which is basically just a more limited version of the second solution we discussed — we still need to use a pseudo, but, since we use a cover, not a mask, it only works if we have a solid or fixed background behind the button.

$b: .25em;
$c: #393939;

html { background: $c; } 

button {
  /* same base styles */
  --grad: linear-gradient(to right bottom, var(--slist));
  border: solid $b transparent;
  background: var(--grad) border-box;
  -webkit-background-clip: text;
          background-clip: text;
  -webkit-text-fill-color: transparent;
  
  &::before {
    position: absolute;
    z-index: -1;
    inset: -$b;
    border: inherit;
    border-radius: inherit;
    background: linear-gradient($c 0 0) padding-box, 
                var(--grad) border-box;
    content: '';
  }
}

On the bright side, this more limited version should also work in pre-Chromium Edge.

Screenshot collage. Chrome (left) vs. Firefox (right). Both display a pill-shaped pastel gradient ghost button that has a solid background behind. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The cover solution on a pseudo for a solid background behind the button (live demo).

Below, there’s also the fixed background version.

$f: url(balls.jpg) 50%/ cover fixed;

html { background: $f; } 

button {
  /* same as before */
  
  &::before {
    /* same as before */
    background: $f padding-box, 
                var(--grad) border-box
  }
}
Screenshot collage. Chrome (left) vs. Firefox (right). Both display a pill-shaped pastel gradient ghost button that has a fixed image background behind. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The cover solution on a pseudo for a fixed background behind the button (live demo).

Overall, I don’t think this is the best tactic unless we both fit into the background limitation and we need to reproduce the effect in browsers that don’t support masking, but support clipping the background to the text, such as pre-Chromium Edge.

The blending solution

This approach is another limited one as it won’t work unless, for each and every gradient pixel that’s visible, its channels have values that are either all bigger or all smaller than than the corresponding pixel of the background underneath the button. However, this is not the worst limitation to have as it should probably lead to our page having better contrast.

Here, we start by making the parts where we want to have the gradient (i.e. the text, icon and border) either white or black, depending on whether we have a dark theme with a light gradient or a light theme with a dark gradient, respectively. The rest of the button (the area around the text and icon, but inside the border) is the inverse of the previously chosen color (white if we set the color value to black and black otherwise).

In our case, we have a pretty light gradient button on a dark background, so we start with white for the text, icon and border, and black for the background. The hex channel values of our two gradient stops are ff (R), da (G), 5f (B) and f9 (R), 37 (G), 6b (B), so we’d be safe with any background pixels whose channel values are at most as big as min(ff, f9) = f9 for red, min(da, 37) = 37 for green and min(5f, 6b) = 5f for blue.

This means having a background-color behind our button with channel values that are smaller or equal to f9, 37 and 5f, either on its own as a solid background, or underneath a background-image layer we blend with using the multiply blend mode (which always produces a result that’s at least as dark as the darker of the two layers). We’re setting this background on a pseudo-element since blending with the actual body or the html doesn’t work in Chrome.

$b: .25em;

body::before {
  position: fixed;
  inset: 0;
  background: url(fog.jpg) 50%/ cover #f9375f;
  background-blend-mode: multiply;
  content: '';
}

button {
  /* same base styles */
  position: relative; /* so it shows on top of body::before */
  border: solid $b;
  background: #000;
  color: #fff;
  
  &::after {
    filter: brightness(0) invert(1);
    content: attr(data-ico);
  }
}

Note that making the icon fully white means making it first black with brightness(0) and then inverting this black with invert(1).

Screenshot collage. Chrome (left) vs. Firefox (right). Both show a pill-shaped black and white (white border, white text, white emoji and black everything in between) button on top of a dark image background. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The black and white button (live demo).

We then add a gradient ::before pseudo-element, just like we did for the first cross-browser method.

button {
  /* same styles as before */
  position: relative;
  
  &::before {
    position: absolute;
    z-index: 2;
    inset: -$b;
    border-radius: inherit;
    background: linear-gradient(to right bottom, var(--slist);
    pointer-events: none;
    content: '';
  }
}

The only difference is that here, instead of giving it a negative z-index, we give it a positive z-index. That way it’s not just over the actual button, but also over the ::after pseudo and we set pointer-events to none in order to allow the mouse to interact with the actual button content underneath.

Screenshot. Shows a pill-shaped gradient button with no visible text on top of a dark image background.
The result after adding a gradient pseudo on top of the black and white button (live demo).

Now the next step is to keep the black parts of our button, but replace the white parts (i.e., the text, icon and border) with the gradient. We can do this with a darken blend mode, where the two layers are the black and white button with the ::after icon and the gradient pseudo on top of it.

For each of the RGB channels, this blend mode takes the values of the two layers and uses the darker (smaller) one for the result. Since everything is darker than white, the resulting layer uses the gradient pixel values in that area. Since black is darker than everything, the resulting layer is black everywhere the button is black.

button {
  /* same styles as before */
  
  &::before {
    /* same styles as before */
    mix-blend-mode: darken;
  }
}
Screenshot collage.  Chrome (left) vs. Firefox (right). Both show a pill-shaped black and pastel gradient (pastel gradient border, text, emoji and black everything in between) button on top of a dark image background. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The “almost there” result (live demo).

Alright, but we’d only be done at this point if the background behind the button was pure black. Otherwise, in the case of a background whose every pixel is darker than the corresponding pixel of the gradient on our button, we can apply a second blend mode, this time lighten on the actual button (previously, we had darken on the ::before pseudo).

For each of the RGB channels, this blend mode takes the values of the two layers and uses the lighter (bigger) one for the result. Since anything is lighter than black, the resulting layer uses the background behind the button everywhere the button is black. And since a requirement is that every gradient pixel of the button is lighter than the corresponding pixel of the background behind it, the resulting layer uses the gradient pixel values in that area.

button {
  /* same styles as before */
  mix-blend-mode: lighten;
}
Screenshot collage. Chrome (left) vs. Firefox (right). Both show a pill-shaped pastel gradient ghost with a 'BOO!' text and a ghost emoji button on top of a dark image background. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The light ghost button on top of a dark background (live demo).

For a dark gradient button on a light background, we need to switch up the blend modes. That is, use lighten on the ::before pseudo and darken on the button itself. And first of all, we need to ensure the background behind the button is light enough.

Let’s say our gradient is between #602749 and #b14623. The channel values of our gradient stops are 60 (R), 27 (G), 49 (B) and b1 (R), 46 (G), 23 (R), so the background behind the button needs to have channel values that are at least max(60, b1) = b1 for red, max(27, 46) = 46 for green and max(49, 23) = 49 for blue.

This means having a background-color on our button with channel values that are bigger or equal to b1, 46 and 49, either on its own as a solid background, or underneath a background-image layer, uses a screen blend mode (which always produces a result that’s at least as light as the lighter of the two layers).

We also need to make the button border, text and icon black, while setting its background to white:

$b: .25em;

section {
  background: url(fog.jpg) 50%/ cover #b14649;
  background-blend-mode: screen;
}

button {
  /* same as before */
  border: solid $b;
  background: #fff;
  color: #000;
  mix-blend-mode: darken;

  &::before {
    /* same as before */
    mix-blend-mode: lighten
  }
  
  &::after {
    filter: brightness(0);
    content: attr(data-ico);
  }
}

The icon in the ::after pseudo-element is made black by setting filter: brightness(0) on it.

Screenshot collage. Chrome (left) vs. Firefox (right). Both show a pill-shaped dark gradient ghost with a 'BOO!' text and a ghost emoji button on top of a light image background. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The dark ghost button on top of a light background (live demo).

We also have the option of blending all the button layers as a part of its background, both for the light and dark theme, but, as mentioned before, Firefox just ignores any background-clip declaration where text is a part of a list of values and not the single value.

Well, that’s it! I hope you’re having (or had) a scary Halloween. Mine was definitely made horrific by all the bugs I got to discover… or rediscover, along with the reality that they haven’t been fixed in the meanwhile.


CSS-ing Candy Ghost Buttons originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/css-ing-candy-ghost-buttons/feed/ 5 354804
Image Fragmentation Effect With CSS Masks and Custom Properties https://css-tricks.com/image-fragmentation-effect-with-css-masks-and-custom-properties/ https://css-tricks.com/image-fragmentation-effect-with-css-masks-and-custom-properties/#comments Tue, 23 Mar 2021 14:56:10 +0000 https://css-tricks.com/?p=336563 Geoff shared this idea of a checkerboard where the tiles disappear one-by-one to reveal an image. In it, an element has a background image, then a CSS Grid layout holds the “tiles” that go from a filled background color to …


Image Fragmentation Effect With CSS Masks and Custom Properties originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Geoff shared this idea of a checkerboard where the tiles disappear one-by-one to reveal an image. In it, an element has a background image, then a CSS Grid layout holds the “tiles” that go from a filled background color to transparent, revealing the image. A light touch of SCSS staggers the animation.

I have a similar idea, but with a different approach. Instead of revealing the image, let’s start with it fully revealed, then let it disappear one tile at a time, as if it’s floating away in tiny fragments.

Here’s a working demo of the result. No JavaScript handling, no SVG trickery. Only a single <img> and some SCSS magic.

Cool, right? Sure, but here’s the rub. You’re going to have to view this in Chrome, Edge or Opera because those are the only browsers with support for @property at the moment and that’s a key component to this idea. We won’t let that stop us because this is a great opportunity to get our hands wet with cool CSS features, like masks and animating linear gradients with the help of @property.

Masking things

Masking is sometimes hard to conceptualize and often gets confused with clipping. The bottom line: masks are images. When an image is applied as mask to an element, any transparent parts of the image allow us see right through the element. Any opaque parts will make the element fully visible.

Masks work the same way as opacity, but on different portions of the same element. That’s different from clipping, which is a path where everything outside the path is simply hidden. The advantages of masking is that we can have as many mask layers as we want on the same element — similar to how we can chain multiple images on background-image.

And since masks are images, we get to use CSS gradients to make them. Let’s take an easy example to better understand the trick.

img {
  mask:
    linear-gradient(rgba(0,0,0,0.8) 0 0) left,  /* 1 */
    linear-gradient(rgba(0,0,0,0.5) 0 0) right; /* 2 */
  mask-size: 50% 100%;
  mask-repeat: no-repeat;
}

Here, we’re defining two mask layers on an image. They are both a solid color but the alpha transparency values are different. The above syntax may look strange but it’s a simplified way of writing linear-gradient(rgba(0,0,0,0.8), rgba(0,0,0,0.8)).

It’s worth noting that the color we use is irrelevant since the default mask-mode is alpha. The alpha value is the only relevant thing. Our gradient can be linear-gradient(rgba(X,Y,Z,0.8) 0 0) where X, Y and Z are random values.

Each mask layer is equal to 50% 100% (or half width and full height of the image). One mask covers the left and the other covers the right. At the end, we have two non-overlapping masks covering the whole area of the image and, as we discussed earlier, each one has a differently defined alpha transparency value.

We’re looking at two mask layers created with two linear gradients. The first gradient, left, has an alpha value of 0.8. The second gradient, right, has an alpha value of 0.5. The first gradient is more opaque meaning more of the image shows through. The second gradient is more transparent meaning more of the of background shows through.

Animating linear gradients

What we want to do is apply an animation to the linear gradient alpha values of our mask to create a transparency animation. Later on, we’ll make these into asynchronous animations that will create the fragmentation effect.

Animating gradients is something we’ve been unable to do in CSS. That is, until we got limited support for @property. Jhey Tompkins did a deep dive into the awesome animating powers of @property, demonstrating how it can be used to transition gradients. Again, you’ll want to view this in Chrome or another Blink-powered browser:

In short, @property lets us create custom CSS properties where we’re able to define the syntax by specifying a type. Let’s create two properties, --c-0 and--c-1 , that take a number with an initial value of 1.

@property --c-0 {
   syntax: "<number>";
   initial-value: 1;
   inherits: false;
}
@property --c-1 {
   syntax: "<number>";
   initial-value: 1;
   inherits: false;
}

Those properties are going to represent the alpha values in our CSS mask. And since they both default to fully opaque (i.e. 1 ), the entire image shows through the mask. Here’s how we can rewrite the mask using the custom properties:

/* Omitting the @property blocks above for brevity */

img {
  mask:
    linear-gradient(rgba(0,0,0,var(--c-0)) 0 0) left,  /* 1 */
    linear-gradient(rgba(0,0,0,var(--c-1)) 0 0) right; /* 2 */
  mask-size: 50% 100%;
  mask-repeat: no-repeat;
  transition: --c-0 0.5s, --c-1 0.3s 0.4s;
}

img:hover {
  --c-0:0;
  --c-1:0;
}

All we’re doing here is applying a different transition duration and delay for each custom variable. Go ahead and hover the image. The first gradient of the mask will fade out to an alpha value of 0 to make the image totally see through, followed but the second gradient.

More masking!

So far, we’ve only been working with two linear gradients on our mask and two custom properties. To create a tiling or fragmentation effect, we’ll need lots more tiles, and that means lots more gradients and a lot of custom properties!

SCSS makes this a fairly trivial task, so that’s what we’re turning to for writing styles from here on out. As we saw in the first example, we have a kind of matrix of tiles. We can think of those as rows and columns, so let’s define two SCSS variables, $x and $y to represent them.

Custom properties

We’re going to need @property definitions for each one. No one wants to write all those out by hand, though, so let’s allow SCSS do the heavy lifting for us by running our properties through a loop:

@for $i from 0 through ($x - 1) {
  @for $j from 0 through ($y - 1) {
    @property --c-#{$i}-#{$j} {
      syntax: "<number>";
      initial-value: 1;
      inherits: false;
    }
  }
}

Then we make all of them go to 0 on hover:

img:hover {
  @for $i from 0 through ($x - 1) {
    @for $j from 0 through ($y - 1) {
      --c-#{$i}-#{$j}: 0;
    }
  }
}

Gradients

We’re going to write a @mixin that generates them for us:

@mixin image() {
  $all_t: (); // Transition
  $all_m: (); // Mask
  @for $i from 0 through ($x - 1) {
    @for $j from 0 through ($y - 1) {
      $all_t: append($all_t, --c-#{$i}-#{$j} transition($i,$j), comma);
      $all_m: append($all_m, linear-gradient(rgba(0,0,0,var(--c-#{$i}-#{$j})) 0 0) calc(#{$i}*100%/(#{$x} - 1)) calc(#{$j}*100%/(#{$y} - 1)), comma);
    }
  }
  transition: $all_t;
  mask: $all_m;
}

All our mask layers equally-sized, so we only need one property for this, relying on the $x and $y variables and calc():

mask-size: calc(100%/#{$x}) calc(100%/#{$y})

You may have noticed this line as well:

$all_t: append($all_t, --c-#{$i}-#{$j} transition($i,$j), comma);

Within the same mixing, we’re also generating the transition property that contains all the previously defined custom properties.

Finally, we generate a different duration/delay for each property, thanks to the random() function in SCSS.

@function transition($i,$j) {
  @return $s*random()+s $s*random()+s;
}

Now all we have to do is to adjust the $x and $y variables to control the granularity of our fragmentation.

Playing with the animations

We can also change the random configuration to consider different kind of animations.

In the code above, I defined the transition() function like below:

// Uncomment one to use it
@function transition($i,$j) {
  // @return (($s*($i+$j))/($x+$y))+s (($s*($i+$j))/($x+$y))+s; /* diagonal */
  // @return (($s*$i)/$x)+s (($s*$j)/$y)+s; /* left to right */
  // @return (($s*$j)/$y)+s (($s*$i)/$x)+s; /* top to bottom */
  // @return  ($s*random())+s (($s*$j)/$y)+s; /* top to bottom random */
  @return  ($s*random())+s (($s*$i)/$y)+s; /* left to right random */
  // @return  ($s*random())+s (($s*($i+$j))/($x+$y))+s; /* diagonal random */
  // @return ($s*random())+s ($s*random())+s; /* full random*/
}

By adjusting the formula, we can get different kinds of animation. Simply uncomment the one you want to use. This list is non-exhaustive — we can have any combination by considering more forumlas. (I’ll let you imagine what’s possible if we add advanced math functions, like sin(), sqrt(), etc.)

Playing with the gradients

We can still play around with our code by adjusting the gradient so that, instead of animating the alpha value, we animate the color stops. Our gradient will look like this:

linear-gradient(white var(--c-#{$i}-#{$j}),transparent 0)

Then we animate the variable from 100% to 0%. And, hey, we don’t have to stick with linear gradients. Why not radial?

Like the transition, we can define any kind of gradient we want — the combinations are infinite!

Playing with the overlap

Let’s introduce another variable to control the overlap between our gradient masks. This variable will set the mask-size like this:

calc(#{$o}*100%/#{$x}) calc(#{$o}*100%/#{$y})

There is no overlap if it’s equal to 1. If it’s bigger, then we do get an overlap. This allows us to make even more kinds of animations:

That’s it!

All we have to do is to find the perfect combination between variables and formulas to create astonishing and crazy image fragmentation effects.


Image Fragmentation Effect With CSS Masks and Custom Properties originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/image-fragmentation-effect-with-css-masks-and-custom-properties/feed/ 5 336563