Single Element Loaders: The Dots

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

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

Article series

  • Single Element Loaders: The Spinner
  • Single Element Loaders: The Dots — you are here
  • Single Element Loaders: The Bars — coming June 24
  • Single Element Loaders: Going 3D — coming July 1

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

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

The background animation

Let’s start with the background animation:

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

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

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

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

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

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

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

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

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

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

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

The loader dots

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

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

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

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

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

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

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

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

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

Dot loader dots with labels showing their changing positions.

Here’s what we get:

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

Dot loader variations

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

What about another movement for our dots?

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

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

Why not a loader with one dot?

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

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

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

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

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

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

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

More examples!

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

Next up…

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

Article series

  • Single Element Loaders: The Spinner
  • Single Element Loaders: The Dots — you are here
  • Single Element Loaders: The Bars — coming June 24
  • Single Element Loaders: Going 3D — coming July 1

Single Element Loaders: The Dots originally published on CSS-Tricks. You should get the newsletter.

Single Element Loaders: The Spinner

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 — coming June 17
  3. Single Element Loaders: The Bars — coming June 24
  4. Single Element Loaders: Going 3D — coming July 1

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 — coming June 17
  3. Single Element Loaders: The Bars — coming June 24
  4. Single Element Loaders: Going 3D — coming July 1

Single Element Loaders: The Spinner originally published on CSS-Tricks. You should get the newsletter.

Cool CSS Hover Effects That Use Background Clipping, Masks, and 3D

We’ve walked through a series of posts now about interesting approaches to CSS hover effects. We started with a bunch of examples that use CSS background properties, then moved on to the text-shadow property where we technically didn’t use any shadows. We also combined them with CSS variables and calc() to optimize the code and make it easy to manage.

In this article, we will build off those two articles to create even more complex CSS hover animations. We’re talking about background clipping, CSS masks, and even getting our feet wet with 3D perspectives. In other words, we are going to explore advanced techniques this time around and push the limits of what CSS can do with hover effects!

Cool Hover Effects series:

  1. Cool Hover Effects That Use Background Properties
  2. Cool Hover Effects That Use CSS Text Shadow
  3. Cool Hover Effects That Use Background Clipping, Masks, and 3D (you are here!)

Here’s just a taste of what we’re making:

Hover effects using background-clip

Let’s talk about background-clip. This CSS property accepts a text keyword value that allows us to apply gradients to the text of an element instead of the actual background.

So, for example, we can change the color of the text on hover as we would using the color property, but this way we animate the color change:

All I did was add background-clip: text to the element and transition the background-position. Doesn’t have to be more complicated than that!

But we can do better if we combine multiple gradients with different background clipping values.

In that example, I use two different gradients and two values with background-clip. The first background gradient is clipped to the text (thanks to the text value) to set the color on hover, while the second background gradient creates the bottom underline (thanks to the padding-box value). Everything else is straight up copied from the work we did in the first article of this series.

How about a hover effect where the bar slides from top to bottom in a way that looks like the text is scanned, then colored in:

This time I changed the size of the first gradient to create the line. Then I slide it with the other gradient that update the text color to create the illusion! You can visualize what’s happening in this pen:

We’ve only scratched the surface of what we can do with our background-clipping powers! However, this technique is likely something you’d want to avoid using in production, as Firefox is known to have a lot of reported bugs related to background-clip. Safari has support issues as well. That leaves only Chrome with solid support for this stuff, so maybe have it open as we continue.

Let’s move on to another hover effect using background-clip:

You’re probably thinking this one looks super easy compared to what we’ve just covered — and you are right, there’s nothing fancy here. All I am doing is sliding one gradient while increasing the size of another one.

But we’re here to look at advanced hover effects, right? Let’s change it up a bit so the animation is different when the mouse cursor leaves the element. Same hover effect, but a different ending to the animation:

Cool right? let’s dissect the code:

.hover {
  --c: #1095c1; /* the color */

  color: #0000;
  background: 
    linear-gradient(90deg, #fff 50%, var(--c) 0) calc(100% - var(--_p, 0%)) / 200%, 
    linear-gradient(var(--c) 0 0) 0% 100% / var(--_p, 0%) no-repeat,
    var(--_c, #0000);
  -webkit-background-clip: text, padding-box, padding-box;
          background-clip: text, padding-box, padding-box;
  transition: 0s, color .5s, background-color .5s;
}
.hover:hover {
  color: #fff;
  --_c: var(--c);
  --_p: 100%;
  transition: 0.5s, color 0s .5s, background-color 0s .5s;
}

We have three background layers — two gradients and the background-color defined using --_c variable which is initially set to transparent (#0000). On hover, we change the color to white and the --_c variable to the main color (--c).

Here’s what is happening on that transition: First, we apply a transition to everything but we delay the color and background-color by 0.5s to create the sliding effect. Right after that, we change the color and the background-color. You might notice no visual changes because the text is already white (thanks to the first gradient) and the background is already set to the main color (thanks to the second gradient).

Then, on mouse out, we apply an instant change to everything (notice the 0s delay), except for the color and background-color that have a transition. This means that we put all the gradients back to their initial states. Again, you will probably see no visual changes because the text color and background-color already changed on hover.

Lastly, we apply the fading to color and a background-color to create the mouse-out part of the animation. I know, it may be tricky to grasp but you can better visualize the trick by using different colors:

Hover the above a lot of times and you will see the properties that are animating on hover and the ones animating on mouse out. You can then understand how we reached two different animations for the same hover effect.

Let’s not forget the DRY switching technique we used in the previous articles of this series to help reduce the amount of code by using only one variable for the switch:

.hover {
  --c: 16 149 193; /* the color using the RGB format */

  color: rgb(255 255 255 / var(--_i, 0));
  background:
    /* Gradient #1 */
    linear-gradient(90deg, #fff 50%, rgb(var(--c)) 0) calc(100% - var(--_i, 0) * 100%) / 200%,
    /* Gradient #2 */
    linear-gradient(rgb(var(--c)) 0 0) 0% 100% / calc(var(--_i, 0) * 100%) no-repeat,
    /* Background Color */
    rgb(var(--c)/ var(--_i, 0));
  -webkit-background-clip: text, padding-box, padding-box;
          background-clip: text, padding-box, padding-box;
  --_t: calc(var(--_i,0)*.5s);
  transition: 
    var(--_t),
    color calc(.5s - var(--_t)) var(--_t),
    background-color calc(.5s - var(--_t)) var(--_t);
}
.hover:hover {
  --_i: 1;
}

If you’re wondering why I reached for the RGB syntax for the main color, it’s because I needed to play with the alpha transparency. I am also using the variable --_t to reduce a redundant calculation used in the transition property.

Before we move to the next part here are more examples of hover effects I did a while ago that rely on background-clip. It would be too long to detail each one but with what we have learned so far you can easily understand the code. It can be a good inspiration to try some of them alone without looking at the code.

I know, I know. These are crazy and uncommon hover effects and I realize they are too much in most situations. But this is how to practice and learn CSS. Remember, we pushing the limits of CSS hover effects. The hover effect may be a novelty, but we’re learning new techniques along the way that can most certainly be used for other things.

Hover effects using CSS mask

Guess what? The CSS mask property uses gradients the same way the background property does, so you will see that what we’re making next is pretty straightforward.

Let’s start by building a fancy underline.

I’m using background to create a zig-zag bottom border in that demo. If I wanted to apply an animation to that underline, it would be tedious to do it using background properties alone.

Enter CSS mask.

The code may look strange but the logic is still the same as we did with all the previous background animations. The mask is composed of two gradients. The first gradient is defined with an opaque color that covers the content area (thanks to the content-box value). That first gradient makes the text visible and hides the bottom zig-zag border. content-box is the mask-clip value which behaves the same as background-clip

linear-gradient(#000 0 0) content-box

The second gradient will cover the whole area (thanks to padding-box). This one has a width that’s defined using the --_p variable, and it will be placed on the left side of the element.

linear-gradient(#000 0 0) 0 / var(--_p, 0%) padding-box

Now, all we have to do is to change the value of --_p on hover to create a sliding effect for the second gradient and reveal the underline.

.hover:hover {
  --_p: 100%;
  color: var(--c);
}

The following demo uses with the mask layers as backgrounds to better see the trick taking place. Imagine that the green and red parts are the visible parts of the element while everything else is transparent. That’s what the mask will do if we use the same gradients with it.

With such a trick, we can easily create a lot of variation by simply using a different gradient configuration with the mask property:

Each example in that demo uses a slightly different gradient configuration for the mask. Notice, too, the separation in the code between the background configuration and the mask configuration. They can be managed and maintained independently.

Let’s change the background configuration by replacing the zig-zag underline with a wavy underline instead:

Another collection of hover effects! I kept all the mask configurations and changed the background to create a different shape. Now, you can understand how I was able to reach 400 hover effects without pseudo-elements — and we can still have more!

Like, why not something like this:

Here’s a challenge for you: The border in that last demo is a gradient using the mask property to reveal it. Can you figure out the logic behind the animation? It may look complex at first glance, but it’s super similar to the logic we’ve looked at for most of the other hover effects that rely on gradients. Post your explanation in the comments!

Hover effects in 3D

You may think it’s impossible to create a 3D effect with a single element (and without resorting to pseudo-elements!) but CSS has a way to make it happen.

What you’re seeing there isn’t a real 3D effect, but rather a perfect illusion of 3D in the 2D space that combines the CSS background, clip-path, and transform properties.

Breakdown of the CSS hover effect in four stages.
The trick may look like we’re interacting with a 3D element, but we’re merely using 2D tactics to draw a 3D box

The first thing we do is to define our variables:

--c: #1095c1; /* color */
--b: .1em; /* border length */
--d: 20px; /* cube depth */

Then we create a transparent border with widths that use the above variables:

--_s: calc(var(--d) + var(--b));
color: var(--c);
border: solid #0000; /* fourth value sets the color's alpha */
border-width: var(--b) var(--b) var(--_s) var(--_s);

The top and right sides of the element both need to equal the --b value while the bottom and left sides need to equal to the sum of --b and --d (which is the --_s variable).

For the second part of the trick, we need to define one gradient that covers all the border areas we previously defined. A conic-gradient will work for that:

background: conic-gradient(
  at left var(--_s) bottom var(--_s),
  #0000 90deg,var(--c) 0
 ) 
 0 100% / calc(100% - var(--b)) calc(100% - var(--b)) border-box;
Diagram of the sizing used for the hover effect.

We add another gradient for the third part of the trick. This one will use two semi-transparent white color values that overlap the first previous gradient to create different shades of the main color, giving us the illusion of shading and depth.

conic-gradient(
  at left var(--d) bottom var(--d),
  #0000 90deg,
  rgb(255 255 255 / 0.3) 0 225deg,
  rgb(255 255 255 / 0.6) 0
) border-box
Showing the angles used to create the hover effect.

The last step is to apply a CSS clip-path to cut the corners for that long shadow sorta feel:

clip-path: polygon(
  0% var(--d), 
  var(--d) 0%, 
  100% 0%, 
  100% calc(100% - var(--d)), 
  calc(100% - var(--d)) 100%, 
  0% 100%
)
Showing the coordinate points of the three-dimensional cube used in the CSS hover effect.

That’s all! We just made a 3D rectangle with nothing but two gradients and a clip-path that we can easily adjust using CSS variables. Now, all we have to do is to animate it!

Notice the coordinates from the previous figure (indicated in red). Let’s update those to create the animation:

clip-path: polygon(
  0% var(--d), /* reverses var(--d) 0% */
  var(--d) 0%, 
  100% 0%, 
  100% calc(100% - var(--d)), 
  calc(100% - var(--d)) 100%, /* reverses 100% calc(100% - var(--d)) */ 
  0% 100% /* reverses var(--d) calc(100% - var(--d)) */
)

The trick is to hide the bottom and left parts of the element so all that’s left is a rectangular element with no depth whatsoever.

This pen isolates the clip-path portion of the animation to see what it’s doing:

The final touch is to move the element in the opposite direction using translate — and the illusion is perfect! Here’s the effect using different custom property values for varying depths:

The second hover effect follows the same structure. All I did is to update a few values to create a top left movement instead of a top right one.

Combining effects!

The awesome thing about everything we’ve covered is that they all complement each other. Here is an example where I am adding the text-shadow effect from the second article in the series to the background animation technique from the first article while using the 3D trick we just covered:

The actual code might be confusing at first, but go ahead and dissect it a little further — you’ll notice that it’s merely a combination of those three different effects, pretty much smushed together.

Let me finish this article with a last hover effect where I am combining background, clip-path, and a dash of perspective to simulate another 3D effect:

I applied the same effect to images and the result was quite good for simulating 3D with a single element:

Want a closer look at how that last demo works? I wrote something up on it.

Wrapping up

Oof, we are done! I know, it’s a lot of tricky CSS but (1) we’re on the right website for that kind of thing, and (2) the goal is to push our understanding of different CSS properties to new levels by allowing them to interact with one another.

You may be asking what the next step is from here now that we’re closing out this little series of advanced CSS hover effects. I’d say the next step is to take all that we learned and apply them to other elements, like buttons, menu items, links, etc. We kept things rather simple as far as limiting our tricks to a heading element for that exact reason; the actual element doesn’t matter. Take the concepts and run with them to create, experiment with, and learn new things!


Cool CSS Hover Effects That Use Background Clipping, Masks, and 3D originally published on CSS-Tricks. You should get the newsletter.

Cool Hover Effects That Use CSS Text Shadow

In my last article we saw how CSS background properties allow us to create cool hover effects. This time, we will focus on the CSS text-shadow property to explore even more interesting hovers. You are probably wondering how adding shadow to text can possibly give us a cool effect, but here’s the catch: we’re not actually going to make any shadows for these text hover effects.

text-shadow but no text shadows?

Let me clear the confusion by showing the hover effects we are going to build in the following demo:

Without looking at the code many of you will, intuitively, think that for each hover effect we are duplicating the text and then independently animating them. Now, if you check the code you will see that none of the text is actually duplicated in the HTML. And did you notice that there is no use of content: "text" in the CSS?

The text layers are completely made with text-shadow!

Hover effect #1

Let’s pick apart the CSS:

.hover-1 {
  line-height: 1.2em;
  color: #0000;
  text-shadow: 
    0 0 #000, 
    0 1.2em #1095c1;
  overflow: hidden;
  transition: .3s;
}
.hover-1:hover {
  text-shadow: 
    0 -1.2em #000, 
    0 0 #1095c1;
}

The first thing to notice is that I am making the color of the actual text transparent (using #0000) in order to hide it. After that, I am using text-shadow to create two shadows where I am defining only two length values for each one. That means there’s no blur radius, making for a sharp, crisp shadow that effectively produces a copy of the text with the specified color.

That’s why I was able to claim in the introduction that there are no shadows in here. What we’re doing is less of a “classic” shadow than it is a simple way to duplicate the text.

Diagram of the start and end of the hover effect.

We have two text layers that we move on hover. If we hide the overflow, then the duplicated text is out of view and the movement makes it appear as though the actual text is being replaced by other text. This is the main trick that that makes all of the examples in this article work.

Let’s optimize our code. I am using the value 1.2em a lot to define the height and the offset of the shadows, making it an ideal candidate for a CSS custom property (which we’re calling --h):

.hover-1 {
  --h: 1.2em;

  line-height: var(--h);
  color: #0000;
  text-shadow: 
    0 0 #000, 
    0 var(--h) #1095c1;
  overflow: hidden;
  transition: .3s;
}
.hover-1:hover {
  text-shadow: 
    0 calc(-1 * var(--h)) #000, 
    0 0 #1095c1;
}

We can still go further and apply more calc()-ulations to streamline things to where we only use the text-shadow once. (We did the same in the previous article.)

.hover-1 {
  --h: 1.2em;   

  line-height: var(--h);
  color: #0000;
  text-shadow: 
    0 calc(-1*var(--_t, 0em)) #000, 
    0 calc(var(--h) - var(--_t, 0em)) #1095c1;
  overflow: hidden;
  transition: .3s;
}
.hover-1:hover {
  --_t: var(--h);
}

In case you are wondering why I am adding an underscore to the --_t variable, it’s just a naming convention I am using to distinguish between the variables we use to control the effect that the user can update (like --h) and the internal variables that are only used for optimization purposes that we don’t need to change (like --_t ). In other words, the underscore is part of the variable name and has no special meaning.

We can also update the code to get the opposite effect where the duplicated text slides in from the top instead:

All we did is a small update to the text-shadow property — we didn’t touch anything else!

Hover effect #2

For this one, we will animate two properties: text-shadow and background. Concerning the text-shadow, we still have two layers like the previous example, but this time we will move only one of them while making the color of the other one transparent during the swap.

.hover-2 {
  /* the height */
  --h: 1.2em;

  line-height: var(--h);
  color: #0000;
  text-shadow: 
    0 var(--_t, var(--h)) #fff,
    0 0 var(--_c, #000);
  transition: 0.3s;
}
.hover-2:hover {
  --_t: 0;
  --_c: #0000;
}

On hover, we move the white text layer to the top while changing the color of the other one to transparent. To this, we add a background-size animation applied to a gradient:

And finally, we add overflow: hidden to keep the animation only visible inside the element’s boundaries:

.hover-2 {
  /* the height */
  --h: 1.2em;

  line-height: var(--h);
  color: #0000;
  text-shadow: 
    0 var(--_t,var(--h)) #fff,
    0 0 var(--_c, #000);
  background: 
    linear-gradient(#1095c1 0 0) 
    bottom/100% var(--_d, 0) no-repeat;
  overflow: hidden;
  transition: 0.3s;
}
.hover-2:hover {
  --_d: 100%;
  --_t: 0;
  --_c: #0000;
}

What we’ve done here is combine the CSS text-shadow and background properties to create a cool hover effect. Plus, we were able to use CSS variables to optimize the code.

If the background syntax looks strange to you, I highly recommend reading my previous article. The next hover effect also relies on an animation I detailed in that article. Unless you are comfortable with CSS background trickery, I’d suggest reading that article before continuing this one for more context.

In the previous article, you show us how to use only one variable to create the hover effect — is it possible to do that here?

Yes, absolutely! We can indeed use that same DRY switching technique so that we’re only working with a single CSS custom property that merely switches values on hover:

.hover-2 {
  /* the height */
  --h: 1.2em;

  line-height: var(--h);
  color: #0000;
  text-shadow: 
    0 var(--_i, var(--h)) #fff,
    0 0 rgb(0 0 0 / calc(var(--_i, 1) * 100%) );
  background: 
    linear-gradient(#1095c1 0 0) 
    bottom/100% calc(100% - var(--_i, 1) * 100%) no-repeat;
  overflow: hidden;
  transition: 0.3s;
}
.hover-2:hover {
  --_i: 0;
}

Hover effect #3

This hover effect is nothing but a combination of two effects we’ve already made: the second hover effect of the previous article and the first hover effect in this article.

.hover-3 {
  /* the color  */
  --c: #1095c1;
  /* the height */
  --h: 1.2em;

  /* The first hover effect in this article */
  line-height: var(--h);  
  color: #0000;
  overflow: hidden;
  text-shadow: 
    0 calc(-1 * var(--_t, 0em)) var(--c), 
    0 calc(var(--h) - var(--_t, 0em)) #fff;
  /* The second hover effect from the previous article */
  background: 
    linear-gradient(var(--c) 0 0) no-repeat 
    calc(200% - var(--_p, 0%)) 100% / 200% var(--_p, .08em);
  transition: .3s var(--_s, 0s), background-position .3s calc(.3s - var(--_s, 0s));
}
.hover-3:hover {
  --_t: var(--h);
  --_p: 100%;
  --_s: .3s
}

All I did was copy and paste the effects from those other examples and make minor adjustments to the variable names. They make for a neat hover effect when they’re combined! At first glance, such an effect may look complex and difficult but, in the end, it’s merely two relatively easy effects made into one.

Optimizing the code with the DRY switching variable technique should also be an easy task if we consider the previous optimizations we’ve already done:

.hover-3 {
  /* the color  */
  --c: #1095c1;
  /* the height */
  --h: 1.2em;

  line-height: var(--h);  
  color: #0000;
  overflow: hidden;
  text-shadow: 
    0 calc(-1 * var(--h) * var(--_i, 0)) var(--c), 
    0 calc(var(--h) * (1 - var(--_i, 0))) #fff;
  background: 
    linear-gradient(var(--c) 0 0) no-repeat
    calc(200% - var(--_i, 0) * 100%) 100% / 200% calc(100% * var(--_i, 0) + .08em);
  transition: .3s calc(var(--_i, 0) * .3s), background-position .3s calc(.3s - calc(var(--_i, 0) * .3s));
}
.hover-3:hover {
  --_i: 1;
}

Hover effect #4

This hover effect is an improvement of the second one. First, let’s introduce a clip-path animation to reveal one of the text layers before it moves:

Here’s another illustration to better understand what is happening:

Diagram of the start and end of the text hover.

Initially, we use inset(0 0 0 0) which is similar to overflow: hidden in that all we see is the actual text. On hover, we update the the third value (which represent the bottom offset) using a negative value equal to the height to reveal the text layer placed at the bottom.

From there, we can add this to the second hover effect we made in this article, and this is what we get:

We are getting closer! Note that we need to first run the clip-path animation and then everything else. For this reason, we can add a delay to all of the properties on hover, except clip-path:

transition: 0.4s 0.4s, clip-path 0.4s;

And on mouse out, we do the opposite:

transition: 0.4s, clip-path 0.4s 0.4s;

The final touch is to add a box-shadow to create the sliding effect of the blue rectangle. Unfortunately, background is unable to produce the effect since backgrounds are clipped to the content area by default. Meanwhile, box-shadow can go outside the content area.

.hover-4 {
  /* the color  */
  --c: #1095c1;
  /* the height */
  --h: 1.2em;
  
  line-height: var(--h);
  color: #0000;
  text-shadow: 
    0 var(--_t, var(--h)) #fff,
    0 0 var(--_c, #000);
  box-shadow: 0 var(--_t, var(--h)) var(--c);
  clip-path: inset(0 0 0 0);
  background: linear-gradient(var(--c) 0 0) 0 var(--_t, var(--h)) no-repeat;
  transition: 0.4s, clip-path 0.4s 0.4s;
}
.hover-4:hover {
  --_t: 0;
  --_c: #0000;
  clip-path: inset(0 0 calc(-1 * var(--h)) 0);
  transition: 0.4s 0.4s, clip-path 0.4s;
}

If you look closely at the box-shadow, you will see it has the same values as the white text layer inside text-shadow. This is logical since both need to move the same way. Both will slide to the top. Then the box-shadow goes behind the element while text-shadow winds up on the top.

Here is a demo with some modified values to visualize how the layers move:

Wait, The background syntax is a bit different from the one used in the second hover effect!

Good catch! Yes, we are using a different technique with background that produces the same effect. Instead of animating the size from 0% to 100%, we are animating the position.

If we don’t specify a size on our gradient, then it take up the full width and height by default. Since we know the height of our element (--h) we can create a sliding effect by updating the position from 0 var(--h) to 0 0.

.hover-4 {
  /* ... */
  background: linear-gradient(var(--c) 0 0) 0 var(--_t, var(--h)) no-repeat;
}
.hover-4:hover {
  --_t: 0;
}

We could have used the background-size animation to get the same effect, but we just added another trick to our list!

In the demos, you also used inset(0 0 1px 0)… why?

I sometimes add or remove a few pixels or percentages here and there to refine anything that looks off. In this case, a bad line was appearing at the bottom and adding 1px removed it.

What about the DRY switch variable optimization?

I am leaving this task for you! After those four hover effects and the previous article, you should be able to update the code so it only uses one variable. I’d love to see you attempt it in the comments!

Your turn!

Let me share one last hover effect which is another version of the previous one. Can you find out how it’s done without looking at the code? It’s a good exercise, so don’t cheat!

Wrapping up

We looked at a bunch of examples that show how one element and few lines of CSS are enough to create some pretty complex-looking hover effects on text elements — no pseudo elements needed! We were even able to combine techniques to achieve even more complex animations with a small amount of effort.

If you’re interested in going deeper than the four text-shadow hover effects in this article, check my collection of 500 hover effects where I am exploring all kinds of different techniques.


Cool Hover Effects That Use CSS Text Shadow originally published on CSS-Tricks. You should get the newsletter.

Tricks to Cut Corners Using CSS Mask and Clip-Path Properties

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:

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.

To overcome the rounding issue, we can combine the last trick with a pseudo-element. Here is a step-by-step illustration to understand the idea:

Here’s what going on in there:

  1. We define a pseudo-element that behaves as our background layer. Logically, we should use inset:0 to make it cover the entire area, but we will create a small overflow by using inset: -10% meaning that the pseudo element will overflow each side by 10%.
  2. We set our CSS mask to the pseudo-element. The mask size needs to match the size of the main element, not the pseudo-element. In other words, it will be smaller than the size of the pseudo-element and this is what we want to be able to move using percentage values. After we do the math, the size needs to be 100%/1.2. Notice in the demo above that the CSS mask is within the green border so that it matches the size of the container.
  3. Now, we need to move it in a way that simulates cutting the corner of the main element. The center of the hole needs to be in the corner of the main element, as illustrated in the demo. To do this, we use mask-position: 300% 300% ( 300% = 50%/(1 - 1/1.2) ).
  4. We remove no-repeat to activate the repetition and get the same effect for every corner.
  5. We clip the overflow and we get our final result!

I know it’s a bit overkill, but it does work and it requires only one gradient instead of four.

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. I recommend using this one.
  • The second method uses one gradient and works with any element, 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 requires a pseudo-element. It won’t work with <img> and other elements that unable to support a pseudo-element.

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. The same can be done with the third method.

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.

Diagram zoomed in on two corners of a rectangle and another where the CSS mask is placed.
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 two radial gradients, 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 for the gradients, 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.

Three cut corners

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

Four corners cut

It takes four radial gradients 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.

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

Before we end, 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. You should get the newsletter.

Exploring the CSS Paint API: Rounding Shapes

Adding borders to complex shapes is a pain, but rounding the corner of complex shapes is a nightmare! Luckily, the CSS Paint API is here to the rescue! That’s what we’re going to look at as part of this “Exploring the CSS Paint API” series.

Exploring the CSS Paint API series:


Here’s what we’re aiming for. Like everything else we’ve looked at in this series, note that only Chrome and Edge support this for now.

Live Demo

You may have noticed a pattern forming if you’ve followed along with the rest of the articles. In general, when we work with the CSS Paint API:

  • We write some basic CSS that we can easily adjust.
  • All the complex logic is done behind the scene inside the paint() function.

We can actually do this without the Paint API

There are probably a lot of ways to put rounded corners on complex shapes, but I will share with you three methods I’ve used in my own work.

I already hear you saying: If you already know three methods, then why are you using the Paint API? Good question. I’m using it because the three methods I’m aware of are difficult, and two of them are specifically related to SVG. I have nothing against SVG, but a CSS-only solution makes thing easier to maintain, plus it’s easier for someone to walk into and understand when grokking the code.

Onto those three methods…

Using clip-path: path()

If you are an SVG guru, this method is for you. the clip-path property accepts SVG paths. That means we can easily pass in the path for a complex rounded shape and be done. This approach is super easy if you already have the shape you want, but it’s unsuitable if you want an adjustable shape where, for example, you want to adjust the radius.

Below an example of a rounded hexagon shape. Good luck trying to adjust the curvature and the shape size! You’re gonna have to edit that crazy-looking path to do it.

I suppose you could refer to this illustrated guide to SVG paths that Chris put together. But it’s still going to be a lot of work to plot the points and curves just how you want it, even referencing that guide.

Using an SVG filter

I discovered this technique from Lucas Bebber’s post about creating a gooey effect. You can find all the technical details there, but the idea is to apply an SVG filter to any element to round its corners.

We simply use clip-path to create the shape we want then apply the SVG filter on a parent element. To control the radius, we adjust the stdDeviation variable.

This is a good technique, but again, it requires a deep level of SVG know-how to make adjustments on the spot.

Using Ana Tudor’s CSS-only approach

Yes, Ana Tudor found a CSS-only technique for a gooey effect that we can use to round the corner of complex shapes. She’s probably writing an article about it right now. Until then, you can refer to the slides she made where she explain how it works.

Below a demo where I am replacing the SVG filter with her technique:

Again, another neat trick! But as far as being easy to work with? Not so much here, either, especially if we’re considering more complex situations where we need transparency, images, etc. It’s work finding the correct combination of filter, mix-blend-mode and other properties to get things just right.

Using the CSS Paint API instead

Unless you have a killer CSS-only way to put rounded borders on complex shapes that you’re keeping from me (share it already!), you can probably see why I decided to reach for the CSS Paint API.

The logic behind this relies on the same code structure I used in the article covering the polygon border. I’m using the --path variable that defines our shape, the cc() function to convert our points, and a few other tricks we’ll cover along the way. I highly recommend reading that article to better understand what we’re doing here.

First, the CSS setup

We first start with a classic rectangular element and define our shape inside the --path variable (shape 2 above). The --path variable behaves the same way as the path we define inside clip-path: polygon()Use Clippy to generate it. 

.box {
  display: inline-block;
  height: 200px;
  width: 200px;

  --path: 50% 0,100% 100%,0 100%;
  --radius: 20px;
  -webkit-mask: paint(rounded-shape);
}

Nothing complex so far. We apply the custom mask and we define both the --path and a --radius variable. The latter will be used to control the curvature.

Next, the JavaScript setup

In addition to the points defined by the path variable (pictured as red points above), we’re adding even more points (pictured as green points above) that are simply the midpoints of each segment of the shape. Then we use the arcTo() function to build the final shape (shape 4 above).

Adding the midpoints is pretty easy, but using arcTo() is a bit tricky because we have to understand how it works. According to MDN:

[It] adds a circular arc to the current sub-path, using the given control points and radius. The arc is automatically connected to the path’s latest point with a straight line, if necessary for the specified parameters.

This method is commonly used for making rounded corners.

The fact that this method requires control points is the main reason for the extra midpoints points. It also require a radius (which we are defining as a variable called --radius).

If we continue reading MDN’s documentation:

One way to think about arcTo() is to imagine two straight segments: one from the starting point to a first control point, and another from there to a second control point. Without arcTo(), these two segments would form a sharp corner: arcTo() creates a circular arc that fits this corner and smooths is out. In other words, the arc is tangential to both segments.

Each arc/corner is built using three points. If you check the figure above, notice that for each corner we have one red point and two green points on each side. Each red-green combination creates one segment to get the two segments detailed above.

Let’s zoom into one corner to better understand what is happening:

We have both segments illustrated in black.
The circle in blue illustrates the radius.

Now imagine that we have a path that goes from the first green point to the next green point, moving around that circle. We do this for each corner and we have our rounded shape.

Here’s how that looks in code:

// We first read the variables for the path and the radius.
const points = properties.get('--path').toString().split(',');
const r = parseFloat(properties.get('--radius').value);

var Ppoints = [];
var Cpoints = [];
const w = size.width;
const h = size.height;
var N = points.length;
var i;
// Then we loop through the points to create two arrays.
for (i = 0; i < N; i++) {
  var j = i-1;
  if(j<0) j=N-1;
  
  var p = points[i].trim().split(/(?!\(.*)\s(?![^(]*?\))/g);
  // One defines the red points (Ppoints)
  p = cc(p[0],p[1]);
  Ppoints.push([p[0],p[1]]);
  var pj = points[j].trim().split(/(?!\(.*)\s(?![^(]*?\))/g);
  pj = cc(pj[0],pj[1]);
  // The other defines the green points (Cpoints)
  Cpoints.push([p[0]-((p[0]-pj[0])/2),p[1]-((p[1]-pj[1])/2)]);
}

/* ... */

// Using the arcTo() function to create the shape
ctx.beginPath();
ctx.moveTo(Cpoints[0][0],Cpoints[0][1]);
for (i = 0; i < (Cpoints.length - 1); i++) {
  ctx.arcTo(Ppoints[i][0], Ppoints[i][1], Cpoints[i+1][0],Cpoints[i+1][1], r);
}
ctx.arcTo(Ppoints[i][0], Ppoints[i][1], Cpoints[0][0],Cpoints[0][1], r);
ctx.closePath();

/* ... */

ctx.fillStyle = '#000';
ctx.fill();

The last step is to fill our shape with a solid color. Now we have our rounded shape and we can use it as a mask on any element.

That’s it! Now all we have to do is to build our shape and control the radius like we want — a radius that we can animate, thanks to @property which will make things more interesting!

Live Demo

Are there any drawbacks with this method?

Yes, there are drawbacks, and you probably noticed them in the last example. The first drawback is related to the hover-able area. Since we are using mask, we can still interact with the initial rectangular shape. Remember, we faced the same issue with the polygon border and we used clip-path to fix it. Unfortunately, clip-path does not help here because it also affects the rounded corner.

Let’s take the last example and add clip-path. Notice how we are losing the “inward” curvature.

There’s no issue with the hexagon and triangle shapes, but the others are missing some curves. It could be an interesting feature to keep only the outward curvature — thanks to clip-path— and at the same time we fix the hover-able area. But we cannot keep all the curvatures and reduce the hover-able area at the same time.

The second issue? It’s related to the use of a big radius value. Hover over the shapes below and see the crazy results we get:

It’s actually not a “major” drawback since we have control over the radius, but it sure would be good to avoid such a situation in case we wrongly use an overly large radius value. We could fix this by limiting the value of the radius to within a range that caps it at a maximum value. For each corner, we calculate the radius that allows us to have the biggest arc without any overflow. I won’t dig into the math logic behind this (😱), but here is the final code to cap the radius value:

var angle = 
Math.atan2(Cpoints[i+1][1] - Ppoints[i][1], Cpoints[i+1][0] - Ppoints[i][0]) -
Math.atan2(Cpoints[i][1]   - Ppoints[i][1], Cpoints[i][0]   - Ppoints[i][0]);
if (angle < 0) {
  angle += (2*Math.PI)
}
if (angle > Math.PI) {
  angle = 2*Math.PI - angle
}
var distance = Math.min(
  Math.sqrt(
    (Cpoints[i+1][1] - Ppoints[i][1]) ** 2 + 
    (Cpoints[i+1][0] - Ppoints[i][0]) ** 2),
  Math.sqrt(
    (Cpoints[i][1] - Ppoints[i][1]) ** 2 + 
    (Cpoints[i][0] - Ppoints[i][0]) ** 2)
  );
var rr = Math.min(distance * Math.tan(angle/2),r);

r is the radius we are defining and rr is the radius we’re actually using. It equal either to r or the maximum value allowed without overflow.

If you hover the shapes in that demo, we no longer get strange shapes but the “maximum rounded shape” (I just coined this) instead. Notice that the regular polygons (like the triangle and hexagon) logically have a circle as their “maximum rounded shape” so we can have cool transitions or animations between different shapes.

Can we have borders?

Yes! All we have to do is to use stroke() instead of fill() inside our paint() function. So, instead of using:

ctx.fillStyle = '#000';
ctx.fill();

…we use this:

ctx.lineWidth = b;
ctx.strokeStyle = '#000';
ctx.stroke();

This introduces another variable, b, that controls the border’s thickness.

Did you notice that we have some strange overflow? We faced the same issue in the previous article, and that due to how stroke() works. I quoted MDN in that article and will do it again here as well:

Strokes are aligned to the center of a path; in other words, half of the stroke is drawn on the inner side, and half on the outer side.

Again, it’s that “half inner side, half outer side” that’s getting us! In order to fix it, we need to hide the outer side using another mask, the first one where we use the fill(). First, we need to introduce a conditional variable to the paint() function in order to choose if we want to draw the shape or only its border.

Here’s what we have:

if(t==0) {
  ctx.fillStyle = '#000';
  ctx.fill();
} else {
  ctx.lineWidth = 2*b;
  ctx.strokeStyle = '#000';
  ctx.stroke();
}

Next, we apply the first type of mask (t=0) on the main element, and the second type (t=1) on a pseudo-element. The mask applied on the pseudo-element produces the border (the one with the overflow issue). The mask applied on the main element addresses the overflow issue by hiding the outer part of the border. And if you’re wondering, that’s why we are adding twice the border thickness to lineWidth.

Live Demo

See that? We have perfect rounded shapes as outlines and we can adjust the radius on hover. And can use any kind of background on the shape.

And we did it all with a bit of CSS:

div {
  --radius: 5px; /* Defines the radius */
  --border: 6px; /* Defines the border thickness */
  --path: /* Define your shape here */;
  --t: 0; /* The first mask on the main element */
  
  -webkit-mask: paint(rounded-shape);
  transition: --radius 1s;
}
div::before {
  content: "";
   background: ..; /* Use any background you want */
  --t: 1; /* The second mask on the pseudo-element */
  -webkit-mask: paint(rounded-shape); /* Remove this if you want the full shape */
}
div[class]:hover {
  --radius: 80px; /* Transition on hover */
}

Let’s not forget that we can easily introduce dashes using setLineDash() the same way we did in the previous article.

Live Demo

Controlling the radius

In all the examples we’ve looked at, we always consider one radius applied to all the corners of each shape. It would be interesting if we could control the radius of each corner individually, the same way the border-radius property takes up to four values. So let’s extend the --path variable to consider more parameters.

Actually, our path can be expressed as a list of [x y] values. We’ll make a list of [x y r] values where we introduce a third value for the radius. This value isn’t mandatory; if omitted, it falls back to the main radius.

.box {
  display: inline-block;
  height: 200px;
  width: 200px;

  --path: 50% 0 10px,100% 100% 5px,0 100%;
  --radius: 20px;
  -webkit-mask: paint(rounded-shape);
}

Above, we have a 10px radius for the first corner, 5px for the second, and since we didn’t specify a value for the third corner, it inherits the 20px defined by the --radius variable.

Here’s our JavaScript for the values:

var Radius = [];
// ...
var p = points[i].trim().split(/(?!\(.*)\s(?![^(]*?\))/g);
if(p[2])
  Radius.push(parseInt(p[2]));
else
  Radius.push(r);

This defines an array that stores the radius of each corner. Then, after splitting the value of each point, we test whether we have a third value (p[2]). If it’s defined, we use it; if not, we use the default radius. Later on, we’re using Radius[i] instead of r.

Live Demo

This minor addition is a nice feature for when we want to disable the radius for a specific corner of the shape. In fact, let’s look at a few different examples next.

More examples!

I made a series of demos using this trick. I recommend setting the radius to 0 to better see the shape and understand how the path is created. Remember that the --path variable behaves the same way as the path we define inside clip-path: polygon(). If you’re looking for a path to play with, try using Clippy to generate one for you.

Example 1: CSS shapes

A lot of fancy shapes can be created using this technique. Here are a few of them done without any extra elements, pseudo-elements, or hack-y code.

Example 2: Speech bubble

In the previous article, we added border to a speech bubble element. Now we can improve it and round the corners using this new method.

If you compare with this example with the original implementation, you may notice the exact same code. I simply made two or three changes to the CSS to use the new Worklet.

Example 3: Frames

Find below some cool frames for your content. No more headaches when we need gradient borders!

Simply play with the --path variable to create your own responsive frame with any coloration your want.

Example 4: Section divider

SVG is no longer needed to create those wavy section dividers that are popular these days.

Notice that the CSS is light and relatively simple. I only updated the path to generate new instances of the divider.

Example 5: Navigation menu

Here’s a classic design pattern that I’m sure many of us have bumped into at some time: How the heck do we invert the radius? You’ve likely seen it in navigation designs.

A slightly different take on it:

Example 6: Gooey effect

If we play with the path values we can reach for some fancy animation.
Below an idea where I am applying a transition to only one value of the path and yet we get a pretty cool effect

This one’s inspired by Ana Tudor’s demo.

Another idea with a different animation

Another example with a more complex animation:

What about a bouncing ball

Example 7: Shape morphing

Playing with big radius values allows us to create cool transitions between different shapes, especially between a circle and a regular polygon.

If we add some border animation, we get “breathing” shapes!

Let’s round this thing up

I hope you’ve enjoyed getting nerdy with the CSS Paint API. Throughout this series, we’ve applied paint() to a bunch of real-life examples where having the API allows us to manipulate elements in a way we’ve never been able to do with CSS — or without resorting to hacks or crazy magic numbers and whatnot. I truly believe the CSS Paint API makes seemingly complicated problems a lot easier to solve in a straightforward way and will be a feature we reach for time and again. That is, when browser support catches up to it.

If you’ve followed along with this series, or even just stumbled into this one article, I’d love to know what you think of the CSS Paint API and how you imagine using it in your work. Are there any current design trends that would benefit from it, like the wavy section dividers? Or blobby designs? Experiment and have fun!

This one’s taken from my previous article


Exploring the CSS Paint API series:


The post Exploring the CSS Paint API: Rounding Shapes appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

Exploring the CSS Paint API: Blob Animation

After the fragmentation effect, I am going to tackle another interesting animation: the blob! We all agree that such effect is hard to achieve with CSS, so we generally reach for SVG to make those gooey shapes. But now that the powerful Paint API is available, using CSS is not only possible, but maybe even a preferable approach once browser support comes around.

Here’s what we’re making. It’s just Chrome and Edge support for now, so check this out on one of those browsers as we go along.

Live demo (Chrome and Edge only)

Building the blob

Let’s understand the logic behind drawing a blob using a classic <canvas> element to better illustrate the shape:

When talking about a blob, we’re also talking about the general shape of a distorted circle, so that’s the shape we can use as our base. We define N points that we place around a circle (illustrated in green).

const CenterX = 200;
const CenterY = 200;
const Radius = 150;
const N = 10;
var point = [];

for (var i = 0; i < N; i++) {
  var x = Math.cos((i / N) * (2 * Math.PI)) * Radius + CenterX;
  var y = Math.sin((i / N) * (2 * Math.PI)) * Radius + CenterY;
  point[i] = [x, y];
}

Considering the center point (defined by CenterX/CenterY) and the radius, we calculate the coordinate of each point using some basic trigonometry.

After that, we draw a cubic Bézier curve between our points using quadraticCurveTo(). To do this, we introduce more points (illustrated in red) because a cubic Bézier curve requires a start point, a control point, and an end point.

The red points are the start and end points, and the green points can be the control points. Each red point is placed at the midpoint between two green points.

ctx.beginPath(); /* start the path */
var xc1 = (point[0][0] + point[N - 1][0]) / 2;
var yc1 = (point[0][1] + point[N - 1][1]) / 2;
ctx.moveTo(xc1, yc1);
for (var i = 0; i < N - 1; i++) {
  var xc = (point[i][0] + point[i + 1][0]) / 2;
  var yc = (point[i][1] + point[i + 1][1]) / 2;
  ctx.quadraticCurveTo(point[i][0], point[i][1], xc, yc);
}
ctx.quadraticCurveTo(point[N - 1][0], point[N - 1][1], xc1, yc1);
ctx.closePath(); /* end the path */

Now all we have to do is to update the position of our control points to create the blob shape. Let’s try with one point by adding the following:

point[3][0]= Math.cos((3 / N) * (2 * Math.PI)) * (Radius - 50) + CenterX;
point[3][1]= Math.sin((3 / N) * (2 * Math.PI)) * (Radius - 50) + CenterY;

The third point is closest to the center of our circle (by about 50px) and our cubic Bézier curve follows the movement perfectly to keep a curved shape.

Let’s do the same with all the points. We can use the same general idea, changing these existing lines:

var x = Math.cos((i / N) * (2 * Math.PI)) * Radius + CenterX;
var y = Math.sin((i / N) * (2 * Math.PI)) * Radius + CenterY;

…into:

var r = 50*Math.random();
var x = Math.cos((i / N) * (2 * Math.PI)) * (Radius - r) + CenterX;
var y = Math.sin((i / N) * (2 * Math.PI)) * (Radius - r) + CenterY;

Each point is offset by a random value between 0 and 50 pixels, bringing each point closer to the center by a slightly different amount. And we get our blob shape as a result!

Now we apply that shape as a mask on an image using the CSS Paint API. Since we are dealing with a blobby shape, it’s suitable to consider square elements (height equal to width) instead, where the radius is equal to half the width or height.

Here we go using a CSS variable (N) to control the number of points.

I highly recommend reading the first part of my previous article to understand the structure of the Paint API.

Each time the code runs, we get a new shape, thanks to the random configuration.

Let’s animate this!

Drawing a blog is good and all, but animating it is better! Animating the blob is actually the main purpose of this article, after all. We will see how to create different kinds of gooey blob animations using the same foundation of code.

The main idea is to smoothly adjust the position of the points — whether it’s all or some of them — to transition between two shapes. Let’s start with the basic one: a transition from a circle into a blob by changing the position of one point.

Animated gif shoeing a cursor hovering the right edge of a circular image. The right side of the image caves in toward the center of the shape on hover, and returns when the cursor leaves the shape.
Live demo (Chrome and Edge only)

For this, I introduced a new CSS variable, B , to which I am applying a CSS transition.

@property --b{
  syntax: '<number>';
  inherits: false;
  initial-value: 0;
}
img {
  --b:0;
  transition:--b .5s;
}
img:hover {
  --b:100
}

I get the value of this variable inside the paint() function and use it to define the position of our point.

If you check the code in the embedded linked demo, you will notice this:

if(i==0) 
  var r = RADIUS - B;
else
  var r = RADIUS

All the points have a fixed position (defined by the shape’s radius) but the first point specifically has a variable position, (RADIUS - B ). On hover, The value of B is changing from 0 to 100, moving our point closer to the middle while creating that cool effect.

Let’s do this for more points. Not all of them but only the even ones. I will define the position as follow:

var r = RADIUS - B*(i%2);
An animated gif showing a cursor hovering over a circular image. The shape of the image morphs to a sort of star-like shape when the cursor enters the image, then returns when the cursor leaves.
Live demo (Chrome and Edge only)

We have our first blob animation! We defined 20 points and are making half of them closer to the center on hover.

We can easily have different blobby variations simply by adjusting the CSS variables. We define the number of points and the final value of the B variable.

Live demo (Chrome and Edge only)

Now let’s try it with some random stuff. Instead of moving our points with a fixed value, let’s make that value random move them all around. We previously used this:

var r = RADIUS - B*(i%2);

Let’s change that to this:

var r = RADIUS - B*random();

…where random() gives us a value in the range [0 1]. In other words, each point is moved by a random value between 0 and B . Here’s what we get:

Live demo (Chrome and Edge only)

See that? We get another cool animation with the same code structure. We only changed one instruction. We can make that instruction a variable so we can decide if we want to use the uniform or the random configuration without changing our JavaScript. We introduce another variable, T, that behaves like a boolean:

if(T == 0) 
  var r = RADIUS - B*(i%2);
else 
  var r = RADIUS - B*random();

We have two animations and, thanks to the T variable, we get to decide which one to use. We can control the number of points using N and the distance using the variable V. Yes, a lot of variables but don’t worry, we will sum up everything at the end.

What is that random() function doing?

It’s the same function I used in the previous article. We saw there that we cannot rely on the default built-in function because we need a random function where we are able to control the seed to make sure we always get the same sequence of random values. So the seed value is also another variable that we can control to get a different blob shape. Go change that value manually and see the result.

In the previous article, I mentioned that the Paint API removes all of the complexity on the CSS side of things, and that gives us more flexibility to create complex animations. For example, we can combine what we have done up to this point with keyframes and cubic-bezier():

Live demo (Chrome and Edge only)

The demo includes another example using the parabolic curve I detailed in a previous article.

Controlling the movement of the points

In all the blobs we’ve created so far, we considered the same movement for our points. Whether we’re using the uniform configuration or the random one, we always move the points from the edge to the center of the circle following a line.

Now let’s see how we can control that movement in order to get even more animations. The idea behind this logic is simple: we move the x and y differently.

Previously we were doing this:

var x = Math.cos((i / N) * (2 * Math.PI)) * (Radius - F(B)) + CenterX;
var y = Math.sin((i / N) * (2 * Math.PI)) * (Radius - F(B)) + CenterY;

…where F(B) is a function based on the variable B that holds the transition.

Now we will have something like this instead:

var x = Math.cos((i / N) * (2 * Math.PI)) * (Radius - Fx(B)) + CenterX;
var y = Math.sin((i / N) * (2 * Math.PI)) * (Radius - Fy(B)) + CenterY;

…where we’ve updating the x and y variables differently to makes more animations. Let’s try a few.

One axis movement

For this one, we will make one of the functions equal to 0 and keep the other one the same as before. In other words, one coordinate remains fixed along the animation

If we do:

Fy(B) = 0

…we get:

A cursor hovers two circular images, which has points that pull inward from the left and right edges of the circle to created jagged sides along the circles.
Live demo (Chrome and Edge only)

The points are only moving horizontally to get another kind of effect. We can easily do the same for the other axis by making Fx(B)=0 (see a demo).

I think you get the idea. All we have to do is to adjust the functions for each axis to get a different animation.

Left or right movement

Let’s try another kind of movement. Instead of making the points converging into the center, let’s make them move into the same direction (either right or left). We need a condition based on the location of the point which is defined by the angle.

Illustration showing the blue outline of a circle with 8 points around the shape and thick red lines bisecting the circle to show the axes.

We have two group of points: ones in the [90deg 270deg] range (the left side), and the remaining points along the ride side of the shape. If we consider the indexes, we can express the range differently, like [0.25N 0.75N] where N is the number of points.

The trick is to have a different sign for each group:

var sign = 1;
if(i<0.75*N && i>0.25*N) 
  sign = -1; /* we invert the sign for the left group */
if(T == 0) 
  var r = RADIUS - B*sign*(i%2);
else 
  var r = RADIUS - B*sign*random();
var x = Math.cos((i / N) * (2 * Math.PI)) * r + cx;

And we get:

An animated gif showing a cursor entering the right side of a circular image, which has points the are pulled toward the center of the circle, then return once the cursor exits.
Live demo (Chrome and Edge only)

We are able to get the same direction but with one small drawback: one group of the points are going outside the mask area because we are increasing the distance on some points while decreasing the distance on others. We need to reduce the size of our circle to leave enough space for all of our points.

We simply decrease the size of our circle using the V value that defines the final value for our B variable. In other words, it’s the maximum distance that one point can reach.

Our initial shape (illustrated by the grey area and defined with the green points) will cover a smaller area since we will decrease the Radius value with the value of V:

const V = parseFloat(properties.get('--v'));
const RADIUS = size.width/2 - V;
Live demo (Chrome and Edge only)

We fixed the issue of the points getting outside but we have another small drawback: the hover-able area is the same, so the effect starts even before the cursor hits the image. It would be good if we can also reduce that area so everything is consistent.

We can use an extra wrapper and a negative margin trick. Here’s the demo. The trick is pretty simple:

.box {
  display: inline-block;
  border-radius: 50%;
  cursor: pointer;
  margin: calc(var(--v) * 1px);
  --t: 0;
}

img {
  display: block;
  margin: calc(var(--v) * -1px);
  pointer-events: none;
  -webkit-mask: paint(blob);
  --b: 0;
  transition:--b .5s;
}
.box:hover img {
  --b: var(--v)
}

The extra wrapper is an inline-block element. The image inside it has negative margins equal to the V variable which reduces the overall size of the shape’s box. Then we disable the hover effect on the image element (using pointer-events: none) so only the box element triggers the transition. Finally we add some margin to the box element to avoid any overlap.

Like the previous effect, this one can also be combined with cubic-bezier() and keyframes to get more cool animations. Below is an example using my sinusoidal curve for a wobbling effect on hover.

Live demo (Chrome and Edge only)

If we add some transforms, we can create a kind of strange (but pretty cool) sliding animation:

A circular image is distorted and slides from right to left on hover in this animated gif.
Live demo (Chrome and Edge only)

Circular movement

Let’s tackle another interesting movement that will allow us to create infinite and “realistic” blob animations. Instead of moving our points from one location to another, we will rotate them around an orbit to have a continuous movement.

The blue outline of a circle with ten green points along its edges. A gray arrow shows the circle's radius and assigns it to a point on the circle with a variable, r, that has a red circle around it showing its hover boundary.

The initial location of our points (in green) will become an orbit and the red circle is the path that our points will take. In other words, each point will rotate around its initial position following a circle having a radius r.

All we need to do is make sure there is no overlap between two adjacent paths so the radius need to have a maximum allowed value.

I will not detail the math but the max value is equal to:

const r = 2*Radius*Math.sin(Math.PI/(2*N));
A blobby shape moves in a circular motion around an image of a cougar's face.
Live demo (Chrome and Edge only)

This is the relevant part of the code:

var r = (size.width)*Math.sin(Math.PI/(N*2));
const RADIUS = size.width/2 - r;
// ...

for(var i = 0; i < N; i++) {
  var rr = r*random();
  var xx = rr*Math.cos(B * (2 * Math.PI));
  var yy = rr*Math.sin(B * (2 * Math.PI)); 
  var x = Math.cos((i / N) * (2 * Math.PI)) * RADIUS + xx + cx;
  var y = Math.sin((i / N) * (2 * Math.PI)) * RADIUS + yy + cy;
  point[i] = [x,y];
}

We get the max value of the radius and we reduce that value from the main radius. Remember that we need to have enough space for our points so we need to reduce the mask area like we did with the previous animation. Then for each point we get a random radius rr (between 0 and r). Then we calculate the position inside the circular path using xx and yy. And, finally, we place the path around its orbit and get the final position (the x, y values).

Notice the value B which is, as usual, the one with the transition. This time, we will have a transition from 0 to 1 in order to make a full turn around the orbit.

Spiral movement

One more for you! This one is a combination of the two previous ones.

We saw how to move the points around a fixed orbit and how to move a point from the edge of the circle to the center. We can combine both and have our point move around an orbit and we do the same for the orbit by moving it from the edge to the center.

Let’s add an extra variable to our existing code:

for(var i = 0; i < N; i++) {
  var rr = r*random();
  var xx = rr*Math.cos(B * (2 * Math.PI));
  var yy = rr*Math.sin(B * (2 * Math.PI)); 

  var ro = RADIUS - Bo*random();
  var x = Math.cos((i / N) * (2 * Math.PI)) * ro + xx + cx;
  var y = Math.sin((i / N) * (2 * Math.PI)) * ro + yy + cy;
  point[i] = [x,y];
}

As you can see, I used the exact same logic as the very first animation we looked at. We reduce the radius with a random value (controlled with Bo in this case).

A blob morphs shape as it moves around an image of a cougar's face.
Live demo (Chrome and Edge only)

Yet another fancy blob animation! Now each element has two animations: one animates the orbit (Bo), and the other animates the point in its circular path (B). Imagine all the effects that you can get by simply adjusting the animation value (duration, ease, etc.)!

Putting everything together

Oof, we are done with all the animations! I know that some of you may have gotten lost with all the variations and all the variables we introduced, but no worries! We will sum everything up right now and you will see that it’s easier than what might expect.

I want to also highlight that what I have done is not an exhaustive list of all the possible animations. I only tackled a few of them. We can define even more but the main purpose of this article is to understand the overall structure and be able to extend it as needed.

Let’s summarize what we have done and what are the main points:

  • The number of points (N): This variable is the one that controls the granularity of the blob’s shape. We define it in the CSS and it is later used to define the number of control points.
  • The type of movement (T): In almost all the animations we looked at, I always considered two kind of animations: a “uniform” animation and a “random” one. I am calling this the type of movement that we can control using the variable T set in the CSS. We will have somewhere in the code to do an if-else based on that T variable.
  • The random configuration: When dealing with random movement, we need to use our own random() function where we can control the seed in order to have the same random sequence for each element. The seed can also be considered a variable, one that generates different shapes.
  • The nature of movement: This is the path that the points take. We can have a lot of variations, for example:
    • From the edge of the circle to the center
    • A one axis movement (the x or y axis)
    • A circular movement
    • A spiral movement
    • and many others…

Like the type of movement, the nature of the movement can also be made conditional by introducing another variable, and there is no limit to what can be done here. All we have to do is to find the math formula to create another animation.

  • The animation variable (B): This is the CSS variable that contains the transition/animation. We generally apply a transition/animation from 0 to a certain value (defined in all the examples with the variable V). This variable is used to express the position of our points. Updating that variable logically updates the positions; hence the animations we get. In most cases, we only need to animate one variable, but we can have more based on the nature of the movement (like the spiral one where we used two variables).
  • The shape area: By default, our shape covers the entire element area, but we saw that some movement require the points to go outside the shape. That’s why we had to reduce the area. We generally do this by the maximum value of B (defined by V), or a different value based on the nature of the movement.

Our code is structured like this:

var point = []; 
/* The center of the element */
const cx = size.width/2;
const cy = size.height/2;
/* We read all of the CSS variables */
const N = parseInt(properties.get('--n')); /* number of points */
const T = parseInt(properties.get('--t')); /* type of movement  */
const Na = parseInt(properties.get('--na')); /* nature of movement  */
const B = parseFloat(properties.get('--b')); /* animation variable */
const V = parseInt(properties.get('--v'));  /* max value of B */
const seed = parseInt(properties.get('--seed')); /* the seed */
// ...

/* The radius of the shape */
const RADIUS = size.width/2 - A(V,T,Na);

/* Our random() function */
let random =  function() {
  // ...
}
/* we define the position of our points */
for(var i = 0; i < N; i++) {
   var x = Fx[N,T,Na](B) + cx;
   var y = Fy[N,T,Na](B) + cy;
   point[i] = [x,y];
}

/* We draw the shape, this part is always the same */
ctx.beginPath();
// ...
ctx.closePath();
/* We fill it with a solid color */
ctx.fillStyle = '#000';
ctx.fill();

As you can see, the code is not as complex as you might have expected. All the work is within those function Fx and Fy, which defines the movement based on N,T and Na. We also have the function A that reduces the size of the shape to prevent points overflowing the shape during the animation.

Let’s check the CSS:

@property --b {
  syntax: '<number>';
  inherits: false;
  initial-value: 0;
}

img {
  -webkit-mask:paint(blob);
  --n: 20;
  --t: 0;
  --na: 1;
  --v: 50;
  --seed: 125;
  --b: 0;
  transition: --b .5s;
}
img:hover {
  --b: var(--v);
}

I think the code is self-explanatory. You define the variables, apply the mask, and animate the B variable using either a transition or keyframes. That’s all!

I will end this article with a final demo where I put all the variations together. All you have to do is to play with the CSS variables


Exploring the CSS Paint API series:


The post Exploring the CSS Paint API: Blob Animation appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

Build Complex CSS Transitions using Custom Properties and cubic-bezier()

I recently illustrated how we can achieve complex CSS animations using cubic-bezier() and how to do the same when it comes to CSS transitions. I was able to create complex hover effect without resorting to keyframes. In this article, I will show you how to create even more complex CSS transitions.

This time, let’s use the @property feature. It’s only supported on Chrome-based browsers for now but we can still play with it and demonstrate how it, too, and can be used to build complex animations.

I highly recommend reading my previous article because I will be referring to a few concepts I explained in detail there. Also, please note that the demos in this article are best viewed in Chromium-based browsers while @property support is still limited.

Let’s start with a demo:

Click on the button (more than once) and see the “magic” curve we get. It may look trivial at first glance because we can achieve such effect using some complex keyframes. But the trick is that there is no keyframe in there! That animation is done using only a transition.

Awesome right? And this is only the beginning, so let’s dig in!

The main idea

The trick in the previous example relies on this code:

@property --d1 {
  syntax: '<number>';
  inherits: false;
  initial-value: 0;
}
@property --d2 {
  syntax: '<number>';
  inherits: false;
  initial-value: 0;
}

.box {
  top: calc((var(--d1) + var(--d2)) * 1%);
  transition:
    --d1 1s cubic-bezier(0.7, 1200, 0.3, -1200),
    --d2 1s cubic-bezier(0.5, 1200, 0.5, -1200);
}
.box:hover {
  --d1: 0.2;
  --d1: -0.2;
}

We’re defining two custom properties, --d1 and --d2. Then, we declare the top property on a .box element using the sum of both those properties. Nothing overly complex yet—just calc() applied to two variables.

The two properties are defined as <number> and I multiply those values by 1% to convert them into a percentage. We could define these as <percentage> right away to avoid the multiplication. But I’ve chosen numbers instead in favor of more flexibility for more complex operations later.

Notice that we apply a different transition to each variable—more precisely, a different timing-function with the same duration. It’s actually a different sinusoidal curve for both variables which is something I get deep into in my previous article.

From there, the property values change when the .box is hovered, triggering the animation. But why do we get the result we see in the demo?

It’s all about math. We are adding two functions to create a third one. For --d1, we have a function (let’s call it F1); for --d2 , we have another one (let’s call it F2). That means the value of top is F1 + F2.

An example to better illustrate:

The first two transitions illustrate each variable individually. The third one is the sum of them. Imagine that at in each step of the animation we take the value of both variables and we add them together to get each point along the final curve.

Let’s try another example:

This time, we combine two parabolic curve to get a… well, I don’t know its name it but it’s another complex curve!

This trick is not only limited to the parabolic and sinusoidal curve. It can work with any kind of timing function even if the result won’t always be a complex curve.

This time:

  • --d1 goes from 0 to 30 with an ease-in timing function
  • --d2 goes from 0 to -20 with an ease-out timing function

The result? The top value goes from 0 to 10 (30-20) with a custom timing function (the sum of ease-in and ease-out).

We are not getting a complex transition in this case—it’s more to illustrate the fact that it’s a generic idea not only limited to cubic-bezier().

I think it’s time for an interactive demo.

All you have to do is to adjust a few variables to build your own complex transition. I know cubic-bezier() may be tricky, so consider using this online curve generator and also refer to my previous article.

Here are some examples I made:

As you can see, we can combine two different timing functions (created using cubic-bezier() ) to create a third one, complex enough to achieve a fancy transition. The combinations (and possibilities) are unlimited!

In that last example, I wanted to demonstrate how adding two opposite functions lead to the logical result of a constant function (no transition). Hence, the flat line.

Let’s add more variables!

You thought we’d stop at only two variables? Certainly not! We can extend the logic to N variables. There is no restriction—we define each one with a timing function and sum them up.

An example with three variables:

In most cases, two variables are plenty to create a fancy curve, but it’s neat to know that the trick can be extended to more variables.

Can we subract, multiply and divide variables?

Of course! We can also extend the same idea to consider more operations. We can add, subtract, multiply, divide—and even perform a complex formula between variables.

Here, we’re multiplying values:

We can also use one variable and multiply it by itself to get a quadratic function!

Let’s add more fun in there by introducing min()/max() to simulate an abs() function:

Notice that in the second box we will never get higher than the center point on the y-axis because top is always a positive value. (I added a margin-top to make the center of box the reference for 0.)

I won’t get into all the math, but you can imagine the possibilities we have to create any kind of timing function. All we have to do is to find the right formula either using one variable or combining multiple variables.

Our initial code can be generalized:

@property --d1 { /* we do the same for d2 .. dn */
  syntax: '<number>';
  inherits: false;
  initial-value: i1; /* the initial value can be different for each variable */
}

.box {
  --duration: 1s; /* the same duration for all */
  property: calc(f(var(--d1),var(--d2), .. ,var(--dn))*[1UNIT]);
  transition:
    --d1 var(--duration) cubic-bezier( ... ),
    --d2 var(--duration) cubic-bezier( ... ),
    /* .. */
    --dn var(--duration) cubic-bezier( ... );
}
.box:hover {
  --d1:f1;
  --d2:f2;
  /* .. */
  --dn:f3;
}

This is pseudo-code to illustrate the logic:

  1. We use @property to define numeric custom properties, each with an initial value.
  2. Each variable has its own timing function but the same duration.
  3. We define an f function that is the formula used between the variables. The function provides a number that we use to multiply the relevant unit. All this runs in calc() applied to the property.
  4. We update the value of each variable on hover (or toggle, or whatever).

Given this, the property transitions from f(i1,i2,…,in) to f(f1,f2,..,fn) with a custom timing function.

Chaining timing functions

We’ve reached the point where we were able to create a complex timing function by combining basic ones. Let’s try another idea that allow us to have more complex timing function: chaining timing functions together.

The trick is to run the transitions sequentially using the transition-delay property. Let’s look back at the interactive demo and apply a delay to one of the variables:

We are chaining timing functions instead of adding them together for yet another way to create more complex timing functions! Mathematically, it’s still a sum, but since the transitions do not run at the same time, we will be summing a function with a constant, and that simulates the chaining.

Now imagine the case with N variables that we are incrementally delayed. Not only can we create complex transitions this way, but we have enough flexibility to build complex timelines.

Here is a funny hover effect I built using that technique:

You will find no keyframes there. A small action scene is made entirely using one element and a CSS transition.

Here is a realistic pendulum animation using the same idea:

Or, how about a ball that bounces naturally:

Or maybe a ball rolling along a curve:

See that? We just created complex animations without a single keyframe in the code!

That’s a wrap!

I hope you took three key points away from this article and the previous one:

  1. We can get parabolic and sinusoidal curves using cubic-bezier() that allow us to create complex transitions without keyframes.
  2. We can create more curves by combining different timing functions using custom properties and calc().
  3. We can chain the curves using the transition-delay to build a complex timeline.

Thanks to these three features, we have no limits when it comes to creating complex animations.


The post Build Complex CSS Transitions using Custom Properties and cubic-bezier() appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

Hexagons and Beyond: Flexible, Responsive Grid Patterns, Sans Media Queries

A little while back, Chris shared this nice hexagonal grid. And true to its name, it’s using —wait for it — CSS Grid to form that layout. It’s a neat trick! Combining grid columns, grid gaps, and creative clipping churns out the final result.

A similar thing could be accomplished with flexbox, too. But I’m here to resurrect our old friend float to create the same sort of complex and responsive layout — but with less complexity and without a single media query.

I know, it’s hard to believe. So let’s start with a working demo:

This is a fully responsive hexagon grid made without media queries, JavaScript, or a ton of hacky CSS. Resize the demo screen and see the magic. In addition to being responsive, the grid also scales. For example, we can chuck more hexagons in there by adding more divs, and control both the sizing and spacing using CSS variables.

Cool, right? And this is only one example among many grids we will build in the same manner.

Making a grid of hexagons

First, we create our hexagon shape. This task is fairly easy using clip-path. We will consider a variable S that will define the dimension of our element. Bennett Feely’s Clippy is a great online generator for clip paths.

Creating a hexagonal shape using clip-path

Each hexagon is an inline-block element. The markup can go something like this:

<div class="main">
  <div class="container">
    <div></div>
    <div></div>
    <div></div>
    <!--etc. -->
  </div>
</div>

…and the CSS:

.main {
  display: flex; /* we will talk about this later ... */
  --s: 100px;  /* size  */
  --m: 4px;   /* margin */
}

.container {
  font-size: 0; /* disable white space between inline block element */
}

.container div {
  width: var(--s);
  margin: var(--m);
  height: calc(var(--s) * 1.1547);
  display: inline-block;
  font-size: initial; /* we reset the font-size if we want to add some content */
  clip-path: polygon(0% 25%, 0% 75%, 50% 100%, 100% 75%, 100% 25%, 50% 0%);
}

Nothing complex so far. We have a main element that holds a container which, in turn, holds the hexagons. Since we are dealing with inline-block, we need to fight the common white space issue (using the font-size trick) and we consider some margin (defined with the variable M) to control the space.

Toggling the font-size of the first demo to illustrate the white space issue

Here’s the result so far:

Every other row needs some negative offset so the rows overlap rather than stack directly on top of each other. That offset will be equal to 25% of the element height (see Figure 1). We apply that offset to margin-bottom to get the following:

.container div {
  width: var(--s);
  margin: var(--m);
  height: calc(var(--s) * 1.1547);
  display: inline-block;
  font-size: initial;
  clip-path: polygon(0% 25%, 0% 75%, 50% 100%, 100% 75%, 100% 25%, 50% 0%);
  margin-bottom: calc(var(--m) - var(--s) * 0.2886); /* some negative margin to create overlap */
}

…and the result becomes:

Now the real trick is how we can shift the second row to get a perfect hexagon grid. We’ve already scrunched things to the point where the rows overlap each other vertically, but what we need is to push every other row toward the right so the hexagons stagger rather than overlap. Here’s where float and shape-outside come into play.

Did you wonder why we have a .main element wrapping our container and having display: flex ? That div is also a part of the trick. In a previous article, I used float and I needed that flexbox container in order to be able to use height: 100%. I will be doing the same thing here.

.container::before {
  content: "";
  width: calc(var(--s)/2 + var(--m));
  float: left;
  height: 100%;
}

I am using the container::before pseudo-element to create a float element that take up all the height at the left of the grid, and that has a width equal to half a hexagon (plus its margin). We get the following result:

The yellow area is our.container::before pseudo-element.

Now, we can reach for shape-outside. Let’s take a quick refresher on what it does. Robin defines it nicely in the CSS-Tricks Almanac. MDN describes it nicely as well:

The shape-outside CSS property defines a shape—which may be non-rectangular—around which adjacent inline content should wrap. By default, inline content wraps around its margin box; shape-outside provides a way to customize this wrapping, making it possible to wrap text around complex objects rather than simple boxes.

Emphasis mine

Notice “inline content” in the definition. This explains exactly why the hexagons need to be inline-block elements. But to understand what kind of shape we need, let’s zoom into the pattern.

What’s cool about shape-inside is that it actually works with gradients. But what kind of gradient fits our situation?

If, for example, we have 10 rows of hexagons, we only need to shift means every even row. Seen differently, we need to shift every second row so we need a kind of repetition — perfect for a repeating gradient!

We’ll create a gradient with two colors:

  • A transparent one to create the “free space” while allowing the first row to stay in place (illustrated by the blue arrow above).
  • An opaque color to shift the second row to the right so the hexagons aren’t directly stacked on top of one another (illustrated by the green arrow).

Our shape-outside value will look like this:

shape-outside: repeating-linear-gradient(#0000 0 A, #000 0 B); /* #0000 = transparent */

Now, let’s find the value of A and B. B will simply be equal to the height of two rows since our logic need to repeat each two rows.

The height of two rows is equal to the height of two hexagons (including their margins), minus twice the overlap (2*Height + 4*M - 2*Height*25% = 1.5*Height + 4*M ). Or, expressed in CSS with calc():

calc(1.732 * var(--s) + 4 * var(--m))

That’s a lot! So, let’s hold all of this in a CSS custom property, F.

The value of A (defined by the blue arrow in the previous figure) needs to be at least equal to the size of one hexagon, but it can also be bigger. In order to push the second row over to the right, we need few pixel of opaque color so A can simply be equal to B - Xpx, where X is a small value.

We end up with something like this:

shape-outside: repeating-linear-gradient(#0000 0 calc(var(--f) - 3px),#000 0 var(--f));

And the following result:

shape-outside is applied to the floated element, creating a floated area with a predating linear gradient.

See that? Our repeating linear gradient’s shape is pushing every other row to the right by one half the width of a hexagon to offset the pattern.

Let’s put that all together:

.main {
  display:flex;
  --s: 100px;  /* size  */
  --m: 4px;    /* margin */
  --f: calc(var(--s) * 1.732 + 4 * var(--m) - 1px); 
}

.container {
  font-size: 0; /* disable white space between inline block element */
}

.container div {
  width: var(--s);
  margin: var(--m);
  height: calc(var(--s) * 1.1547);
  display: inline-block;
  font-size:initial;
  clip-path: polygon(0% 25%, 0% 75%, 50% 100%, 100% 75%, 100% 25%, 50% 0%);
  margin-bottom: calc(var(--m) - var(--s) * 0.2885);
}

.container::before {
  content: "";
  width: calc(var(--s) / 2 + var(--m));
  float: left;
  height: 120%; 
  shape-outside: repeating-linear-gradient(#0000 0 calc(var(--f) - 3px), #000 0 var(--f));
}

That’s it! With no more than 15 CSS declarations, we have a responsive grid that fit nicely into all the screen sizes and we can easily adjust things by simply controling two variables.

You may have noticed that I am adding -1px to the variable F. Since we are dealing with calculation that involve decimals, the rounding may give us bad results. To avoid this we add or remove few pixels. I am also using 120% instead of 100% for the height of the floated element for similar reasons. There is no particular logic with theses values; we simply adjust them to make sure to cover most of the cases without any misaligning our shapes.

Want more shapes?

We can do more than hexagons with this approach! Let’s create a “rhombus” grid instead. Again, we start with our clip-path to create the shape:

Rhombus shape using clip-path

The code is basically the same. What’s changing are the calculations and values. Find below a table that will illustrate the changes.

Hexagon gridRhombus grid
heightcalc(var(--s)*1.1547)var(--s)
clip-pathpolygon(0% 25%, 0% 75%, 50% 100%, 100% 75%, 100% 25%, 50% 0%)polygon(50% 0, 100% 50%, 50% 100%, 0 50%)
margin-bottomcalc(var(--m) - var(--s)*0.2885)calc(var(--m) - var(--s)*0.5)
--fcalc(var(--s)*1.7324 + 4*var(--m))calc(var(--s) + 4*var(--m))

And we’re done! A mere four changes to our code gets us a completely new grid but with a different shape.

Just how flexible is this?

We saw how we were able to make the hexagon and rhombus grids using the exact same code structure, but different variables. Let me blow your mind with another idea: What about making that calculation a variable so that we can easily switch between different grids without changing the code? We can certainly do that!

We’ll use an octagonal shape because it’s more of a generic shape from that we can use to create other shapes (a hexagon, a rhombus, a rectangle, etc.) simply by changing a few values.

The points on this octagon shape are defined in the clip-path property.

Our octagon is defined with four variables:

  • S: the width.
  • R: the ratio that will help us defines the height based on the width.
  • hc and vc : both of these will control our clip-path values and the shape we want to get. hc will be based on the width while vc on the height

I know it looks hefty, but the clip-path is defined using eight points (like shown in the figure). Adding some CSS variables, we get this:

clip-path: polygon(
   var(--hc) 0, calc(100% - var(--hc)) 0, /* 2 points at the top */
   100% var(--vc),100% calc(100% - var(--vc)), /* 2 points at the right */
   calc(100% - var(--hc)) 100%, var(--hc) 100%, /* 2 points at the bottom */
   0 calc(100% - var(--vc)),0 var(--vc) /* 2 points at the left */
);

This is what we’re aiming for:

Let’s zoom in to identify the different values:

The overlap between each row (illustrated by the red arrow) can be expressed using the vc variable which gives us a margin-bottom equal to M - vc (where M is our margin).

In addition to the margin we applied between our element, we also need an additional horizontal margin (illustrated by the yellow arrow) equal to S - 2*hc. Let’s define another variable for the horizontal margin (MH) that is equal to M + (S - 2*hc)/2.

The height of two rows is equal to twice the size of a shape (plus the margin), minus twice the overlap, or 2*(S + 2*M) - 2*vc.

Let’s update our table of values to see how we’re calculating things between the different grids:

Hexagon gridRhombus gridOctagon grid
heightcalc(var(--s)*1.1547)var(--s)calc(var(--s)*var(--r)))
clip-pathpolygon(0% 25%, 0% 75%, 50% 100%, 100% 75%, 100% 25%, 50% 0%)polygon(50% 0, 100% 50%, 50% 100%, 0 50%)polygon(var(--hc) 0, calc(100% - var(--hc)) 0,100% var(--vc),100% calc(100% - var(--vc)), calc(100% - var(--hc)) 100%,var(--hc) 100%,0 calc(100% - var(--vc)),0 var(--vc))
--mhcalc(var(--m) + (var(--s) - 2*var(--hc))/2)
marginvar(--m)var(--m)var(--m) var(--mh)
margin-bottomcalc(var(--m) - var(--s)*0.2885)calc(var(--m) - var(--s)*0.5)calc(var(--m) - var(--vc))
--fcalc(var(--s)*1.7324 + 4*var(--m))calc(var(--s) + 4*var(--m))calc(2*var(--s) + 4*var(--m) - 2*var(--vc))

Alright, let’s update our CSS with those adjustments:

.main {
  display: flex;
  --s: 100px;  /* size  */
  --r: 1; /* ratio */

  /* clip-path parameter */
  --hc: 20px; 
  --vc: 30px;

  --m: 4px; /* vertical margin */
  --mh: calc(var(--m) + (var(--s) - 2*var(--hc))/2); /* horizontal margin */
  --f: calc(2*var(--s) + 4*var(--m) - 2*var(--vc) - 2px);
}

.container {
  font-size: 0; /* disable white space between inline block element */
}

.container div {
  width: var(--s);
  margin: var(--m) var(--mh);
  height: calc(var(--s)*var(--r));
  display: inline-block;
  font-size: initial;
  clip-path: polygon( ... );
  margin-bottom: calc(var(--m) - var(--vc));
}

.container::before {
  content: "";
  width: calc(var(--s)/2 + var(--mh));
  float: left;
  height: 120%; 
  shape-outside: repeating-linear-gradient(#0000 0 calc(var(--f) - 3px),#000 0 var(--f));
}

As we can see, the code structure is the same. We simply added more variable to control the shape and extend the margin property.

And below a working example. Adjust the different variables to control the shape while having a fully responsive grid:

An interactive demo, you say? You bet!

To make things easier, I am expressing the vc and hc as percetange of the width and height so we can easily scale our elements without breaking the clip-path

From the above we can easily get the initial hexagonal grid:

The rhombus grid:

And yet another hexagon grid:

A masonry-like grid:

And a checkerboard while we are at it:

A lot of possibilities to create a responsive grid with any kind of shape! All we have to do is adjust few variables.

Fixing the alignment

Let’s try to control the alignment of our shapes. Since we are dealing with inline-block elements, we’re dealing with default left alignment and some empty space at the end, depending on viewport width.

Notice that we alternate between two kind of grids based on the screen width:

Grid #1: A different number of items per row (NN-1,NN-1, etc.)
Grid #2: The same number of items per row (NNNN, etc.)

It would be good to always have one of the grid all the time (either #1 or #2) and center everything so that the free space is equally divided on both sides.

In order to get the first grid in the figure above, the container width needs to be a multiplier of the size of one shape, plus its margin, or N*(S + 2*MH), where N is an integer value.

This may sound impossible with CSS, but it’s indeed possible. I made it using CSS grid:

.main {
  display: grid;
  grid-template-columns: repeat(auto-fit, calc(var(--s) + 2*var(--mh)));
  justify-content: center;
}

.container {
  grid-column: 1/-1;
}

.main is now a grid container. Using grid-template-columns, I define the column width (as previously explained) and use the auto-fit value to get as many columns as possible into the available space. Then, the .container spans all of the grid columns using 1/-1 — which means that the width of our container will be a mutiplier of one column size.

All it takes to center things is justify-content: center.

Yes, CSS is magic!

Resize the demo and notice that not only do we have the first grid from the figure, but everything is perfectly centered as well.

But wait, we removed display: flex and swapped in display: grid… so how is the percentage-based height of the float still working? I had said that using a flex container was the key for that, no?

Well, turns out CSS grid sports that feature too. From the specification:

Once the size of each grid area is thus established, the grid items are laid out into their respective containing blocks. The grid area’s width and height are considered definite for this purpose.


Note: Since formulas calculated using only definite sizes, such as the stretch fit formula, are also definite, the size of a grid item which is stretched is also considered definite.

A grid item has a stretch alignment by default, so its height is definite, meaning using a percentage as a height inside it is perfectly valid.

Let’s say we instead want the second grid in the figure — we simply add an extra column with a width equal to half the width of the other columns:

.main {
  display: grid;
  grid-template-columns: repeat(auto-fit,calc(var(--s) + 2*var(--mh))) calc(var(--s)/2 + var(--mh));
  justify-content :center;
}

Now, in addition to a fully responsive grid that is flexible enough to take custom shapes, everything is perfectly centred!

Fighting the overflow

The use of negative margin-bottom on the last items and the float element pushing our items will create some unwanted overflow that may affect the content placed after our grid.

If you resize the demo, you will notice an overflow equal to the negative offset and sometimes it’s bigger. The fix is to add some padding-bottom to our container. I will make the padding equal to the height of one shape:

I have to admit that there isn’t a perfect solution to fight that overflow and to control the space below our grid. That space depends on a lot of factors and we may have to use a different padding value for each case. The safest solution is to consider a big value that covers most of the cases.

Wait, one more: a pyramidal grid

Let’s take everything we’ve learned and build another amazing grid. This time, we’ll transform the grid we just made into a pyramidal one.

Consider that, unlike the grid we’ve made so far, the number of elements is important especially for the responsive part. It’s required to know the number of elements and more precesily the number of rows.

Different pyramidal grid based on the number of items

It doesn’t mean we need a bunch of hardcoded values; rather we use an extra variable to adjust things based on the number of rows.

The logic is based on the number of rows because different numbers of elements may give us the same number of rows. For example, there are five rows when we have between 11 and 15 elements, even if the last row is not fully occupied. Having between 16 and 21 elements gives us six rows, and so on. The number of rows is our new variable.

Before digging into the geometry and the math here is a working demo:

Notice that most of the code is the same as what we’ve done in the previous examples. So let’s focus on the new properties that we’ve added:

.main {
  --nr: 5;  /* number of rows */
}

.container {
  max-width: calc(var(--nr)*(var(--s) + 2*var(--mh)));
  margin: 0 auto;
}

.container::before ,
.container i {
  content: "";
  width: calc(50% - var(--mh) - var(--s)/2);
  float: left;
  height: calc(var(--f)*(var(--nr) - 1)/2);
  shape-outside: linear-gradient(to bottom right, #000 50%, #0000 0);
}

.container i {
  float:right;
  shape-outside: linear-gradient(to bottom left, #000 50%, #0000 0);
}

NR is our variable for the number of rows. The width of the container needs to be equal to the last row of the pyramid to make sure it hold all the elements. If you check the previous figure, you’ll see that the number of the items contained in the last row is simply equal to the number of rows, which means the formula is: NR* (S + 2*MH).

You may have also noticed that we also added an <i> element in there. We did that because we need two floating elements where we will apply shape-outside.

To understand why we need two floating elements let’s see what is done behind the scenes:

A pyramid grid of octagon shapes. The octagons alternate between green and red. There are 5 rows of octagons.
Pyramidal grid

The blue elements are our floating elements. Each one is having a width equal to half the container size, minus half a shape size, plus margin. The height is equal to four rows in our case, and to NR - 1 in a more generic case. Earlier, we defined the height of two rows, F, so the height of one row is F/2. That’s how we landed at height: calc(var(--f)*(var(--nr) - 1)/2.

Now that we have the size of our elements, we need to apply a gradient to our shape-outside.

The purple coloration in the figure above is the restricted area for our elements (it need to be an opaque color). The remaining area is the free space where the elements can flow (it need to be a transparent color). This can be done using a diagonal gradient:

shape-outside: linear-gradient(to bottom right, #000 50%, #0000 0); 

We simply change right with left for the other floated element. You have probably noticed that this is not responsive. In fact, go ahead and adjust the viewport width of the demo and see just how unresponsive this is.

We have a couple of options to get responsive:

  1. We can fall back to the first grid when the container width is smaller than the viewport width. It’s a bit tricky to code, but it allows us to preserve the same size for our elements.
  2. We can reduce the size of our elements in order to keep the pyramidal grid. This is easier to code using the percentage-based value trick, but that could result in super tiny elements on smaller screen sizes.

Let’s go with the first solution. We like a good challenge, right?

To get the pyramidal grid, we needed two floated element. The initial grid needed just one floated element. Luckily, our structure allows us to have three floated elements without needing to add more elements to the markup, thanks to pseudo-elements. We will use container::before, i::before, i::after:

/* Same as before... */

/* The initial grid */
.container::before {
  content: "";
  width: calc(var(--s)/2 + var(--mh));
  float: left;
  height: 120%; 
  shape-outside: repeating-linear-gradient(#0000 0 calc(var(--f) - 3px),#000 0 var(--f));
}

/* The pyramidal grid */
.container i::before ,
.container i::after {
  content: "";
  width: calc(50% - var(--mh) - var(--s)/2);
  float: left;
  height: calc(var(--f)*(var(--nr) - 1)/2);
  shape-outside: linear-gradient(to bottom right,#000 50%,#0000 0);
}

.container i::after {
  float:right;
  shape-outside: linear-gradient(to bottom left,#000 50%,#0000 0);
}

Now we need a trick that lets us use either the first floated element or the other two, but not all of them at the same time. This condition should be based on the width of our container:

  • If the container width is bigger than the width of the last row, we can have our pyramid and use the floated elements inside of <i>.
  • If the container width is smaller than the width of the last row, we switch to the other grid and use the first floated element.

We can use clamp() for this! It’s sort of like a conditional function that sets a minimum and maximum range and, within that range, we provide it an “ideal” value to use between those points. This way, we can “switch” between grids using our formulas as clamped values, and still avoid using media queries.

Our code will look like this:

.main {
  /* the other variables won't change*/
  --lw: calc(var(--nr)*(var(--s) + 2*var(--mh))); /* width of last row */
}

.container {
  max-width: var(--lw);
}

/* The initial grid */
.container::before {
  width: clamp(0px, (var(--lw) - 100%)*1000, calc(var(--s)/2 + var(--mh)));
}

/* The pyramidal grid */
.container i::before,
.container i::after {
  width: clamp(0px, (100% - var(--lw) + 1px)*1000, calc(50% - var(--mh) - var(--s)/2));
}

On larger screens, the width of the container (LW) is now equal to its max-width, so 100% == LW. That means that the width of .container::before is equal to 0px (and results in this floated element becoming disabled).

For the other floating elements, we clamp the width:

width: clamp(0px, (100% - var(--lw) + 1px)*1000, calc(50% - var(--mh) - var(--s)/2));

…where the middle value ((100% - LW + 1px)*1000) is equal to (0 + 1px)*1000 = 1000px (an intentionally large, but arbitrary value). It gets clamped to calc(50% - var(--mh) - var(--s)/2). In other words, these floated elements are enabled with the correct width (the one we defined previously)

Voilà! we have a pyramidal shape on large screen.

Now, when the container width get smaller, LW is going to be greater than 100%. So, (LW - 100%) will be positive. Multiplied by a big value, it’s clamped to calc(var(--s)/2 + var(--mh)), which enables the first floated element. For the other float elements, (100% - LW + 1px) resolves to a negative value and is clamped to 0px, which disables the float elements.

Resize the below demo and see how we switch between both grids

Let’s try adding more elements:

See that? Things are scaling perfectly. Let’s toss more elements at it just for kicks:

Still great. Notice that the last row isn’t even full. Just shows that this approach covers a bunch of cases. We can also combine this with the CSS grid alignment trick we used earlier:

Do you think “float” is such a bad thing now?

Want invert the pyramid?

Like illustrated with the above figure, two changes to the previous code can invert our pyramid:

  • I change the direction of the gradient from to bottom left|right to to top left|right,
  • I add a margin-top equal to the height of one row.

And, hey, we can swap between both pyramid easily:

Isn’t this beautiful? We have a responsive pyramidal grid with custom shapes that we can easily invert and that fallback to another responsive grid on small screen while everything is perfectly centred. All this without a single media query or JavaScript, but instead using the often overlooked float property.

You will probably notice some missalignment in some particular cases. Yes, it’s again some rounding issue related to the calculation we are doing and the fact that we are trying to make this generic with the interactive demos. To rectify this, we simply adjust few values manually (epsecially the percentage of the gradient) until we get back a perfect alignment.

That’s a float wrap!

There we have it: combining float with shape-outside can help us make complex, flexible and responsive layouts — long live float!

The article ends here but this is only the beginning. I provided you with the layout and now you can easily put any content inside the divs, apply a background, shadows, animations, etc.


The post Hexagons and Beyond: Flexible, Responsive Grid Patterns, Sans Media Queries appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

Advanced CSS Animation Using cubic-bezier()

When dealing with complex CSS animations, there is a tendency to create expansive @keyframes with lots of declarations. There are a couple of tricks though that I want to talk about that might help make things easier, while staying in vanilla CSS:

  1. Multiple animations
  2. Timing functions

The first one is more widely used and familiar but the second one is less common. There could be good reasons for that — chaining animations with commas is relatively easier than grokking the various timing functions that are available to us and what they do. There’s one especially neat timing function that gives us total control to create custom timing functions. That would be cubic-bezier() and in this post I will show you the power of it and how it can be used to create fancy animation without too much complexity.

Let’s start with a basic example showing how we can move a ball around in interesting directions, like an infinity (∞) shape:

As you can see, there is no complex code — only two keyframes and a cubic-bezier() function. And yet, a pretty complex-looking final infinity-shape animation is what we get.

Cool, right? Let’s dig into this!

The cubic-bezier() function

Let’s start with the official definition:

A cubic Bézier easing function is a type of easing function defined by four real numbers that specify the two control points, P1 and P2, of a cubic Bézier curve whose end points P0 and P3 are fixed at (0, 0) and (1, 1) respectively. The x coordinates of P1 and P2 are restricted to the range [0, 1].

The above curve defines how the output (y-axis) will behave based on the time (x-axis). Each axis has a range of [0, 1] (or [0% 100%] ). If we have an animation that lasts two-second (2s), then:

0 (0%) = 0s 
1 (100%) = 2s

If we want to animate left from 5px to 20px, then:

0 (0%) = 5px 
1 (100%) = 20px

X, the time, is always restricted to [0 1]; however, Y, the output, can go beyond [0 1].

My goal is to adjust P1 and P2 in order to create the following curves:

Parabolic curve
Sinusoidal curve

You may think this is impossible to achieve because, as stated in the definition, P0 and P3 are fixed at (0,0) and (1,1) meaning they cannot be on the same axis. That’s true, and we will use some math tricks to “approximate” them.

Parabolic curve

Let’s start with the following definition: cubic-bezier(0,1.5,1,1.5). That gives us the following curve:

cubic-bezier(0,1.5,1,1.5)

Our goal is to move (1,1) and make it at (0,1) which isn’t technically possible. So we will try to fake it.

We previously said that our range is [0 1] (or [0% 100%]) so let’s imagine the case when 0% is very close to 100%. If, for example, we want to animate top from 20px (0%) to 20.1px (100%) then we can say that both the initial and final states are equal.

Hm, but our element will not move at all, right?

Well, it will move a little because the Y value exceeds 20.1px (100%). But that’s not enough to give us perceptible movement:

Let’s update the curve and use cubic-bezier(0,4,1,4) instead. Notice how our curve is way taller than before:

cubic-bezier(0,4,1,4)

But yet, still no movement — even if the top value is crossing 3 (or 300%). Let’s try cubic-bezier(0,20,1,20):

cubic-bezier(0,20,1,20)

Yes! it started to move a little. Did you notice the evolution of the curve each time we increase the value? It’s making our point (1,1) “visually” closer to (0,1) when we zoom out to see the full curve and this is the trick.

By using cubic-bezier(0,V,1,V) where V is some very big value and both the initial and final states are very close together (or almost equal), we can simulate the parabolic curve.

An example is worth a thousand words:

I applied the “magic” cubic-bezier function in there to the top animation, plus a linear one applied to left. This gives us the curve we want.

Digging into the math

For those of you math-minded folks out there, we can break that explanation down further. A cubic bezier can be defined using the following formula:

P = (1−t)³P0 + 3(1−t)²tP1 + 3(1−t)t²P2 + t³P3

Each point is defined as follows: P0 = (0,0), P1 = (0,V), P2 = (1,V), and P3 = (1,1).

This gives us the two functions for x and y coordinates:

  • X(t) = 3(1−t)t² + t³ = 3t² - 2t³
  • Y(t) = 3(1−t)²tV +3(1−t)t²V + t³ = t³ - 3Vt² + 3Vt

V is our big value and t is within the range [0 1]. If we consider our previous example, Y(t) will give us the value of top while X(t) is the time progress. The points (X(t),Y(t)) will then define our curve.

Let’s find the maximum value of Y(t). For this, we need to find the value of t that will give us Y'(t) = 0 (when the derivative is equal to 0):

Y'(t) = 3t² - 6Vt + 3V

Y'(t) = 0 is a quadratic equation. I will skip the boring part and will give you the result, which is t = V - sqrt(V² - V).

When V is a large value, t will be equal to 0.5. So, Y(0.5) = Max and X(0.5) will be equal to 0.5. That means we reach the maximum value at the halfway point in the animation, which conforms to the parabolic curve we want.

Also, Y(0.5) will give us (1 + 6V)/8 and this will allow us to find the max value based on V. And since we will always use a big value for V, we can simplify to 6V/8 = 0.75V.

We used V = 500 in the last example, so the max value there would come out to 375 (or 37500%) and we get the following:

  • Initial state (0): top: 200px
  • Final state (1): top: 199.5px

There’s a difference of -0.5px between 0 and 1. Let’s call it the increment. For 375 (or 37500%) we have an equation of 375*-0.5px = -187.5px. Our animated element is reaching top: 12.5px (200px - 187.5px) and gives us the following animation:

top: 200px (at 0% of the time ) → top: 12.5px (at 50% of the time) → top: 199.5px (at 100% of the time) 

Or, expressed another way:

top: 200px (at 0%) → top: 12.5px (at 50%) → top: 200px (at 100%)

Let’s do the opposite logic. What value of V should we use to make our element reach top: 0px? The animation will be 200px → 0px → 199.5px, so we need -200px to reach 0px. Our increment is always equal to -0.5px. The max value will be equal to 200/0.5 = 400, so 0.75V = 400 which means V = 533.33.

Our element is touching the top!

Here is a figure that sums up that math we just did:

Parabolic Curve using cubic-bezier(0,V,1,V)

Sinusoidal curve

We will use almost the exact same trick to create a sinusoidal curve but with a different formula. This time we will use cubic-bezier(0.5,V,0.5,-V)

Like we did before, let’s see how the curve will evolve when we increase the value:

Three graphs from left to right, showing how the sinusoidal curve gets narrower as the V value increases.

I think you probably get the idea by now. Using a big value for V gets us close to a sinusoidal curve.

Here’s another one with a continuous animation — a real sinusoidal animation!

The math

Let’s get in the math for this one! Folllowing the same formula as before, we will get the following functions:

  • X(t) = 3/2(1−t)²t + 3/2(1−t)t² + t³ = (3/2)t - (3/2)t² + t³
  • Y(t) = 3(1−t)²tV - 3(1−t)t²V + t³ = (6V + 1)t³ - 9Vt² + 3Vt

This time we need to find the minimum and maximum values for Y(t). Y'(t) = 0 will give us two solutions. After solving for this:

Y'(t) = 3(6V + 1)t² - 18Vt + 3V = 0

…we get:

  • t' = (3V + sqrt(3V² - V))/(6V + 1)
  • t''= (3V - sqrt(3V² - V))/(6V + 1)

For a big value of V, we have t'=0.211 and t"=0.789. That means that Y(0.211) = Max and Y(0.789) = Min. That also means that X(0.211)= 0.26 and X(0.789) = 0.74. In other words, we reach the Max at 26% of the time and Min at 74% of the time.

Y(0.211) is equal to 0.289V and Y(0.789) to -0.289V. We got those values with some rounding considering that V is very big.

Our sinusoidal curve should also cross the x-axis (or Y(t) = 0) at half the time (or X(t) = 0.5). In order to prove this, we use the second derivate of Y(t) — which should be equal to 0 — so Y''(t) = 0.

Y''(t) = 6(6V + 1)t - 18V = 0

The solution is 3V/(6V + 1), and for a big V value, the solution is 0.5. That give us Y(0.5) = 0 and X(0.5) = 0.5 which confirms that our curve crosses the (0.5,0) point.

Now let’s consider the previous example and try to find the value of V that gets us back to top: 0%. We have:

  • Initial state (0): top: 50%
  • Final state (1): top: 49.9%
  • Increment: -0.1%

We need -50% to reach top: 0%, so 0.289V*-0.1% = -50% which gives us V = 1730.10.

As you can see, our element is touching the top and disappearing at the bottom because we have the following animation:

top: 50% → top: 0% → top: 50% → top: 100% → top: 50% → and so on ... 

A figure to sum up the calculation:

Sinusoidal Curve using cubic-bezier(0.5,V,0.5,-V)

And an example to illustrate all curves together:

Yes, you see four curves! If you look closely, you will notice that I am using two different animations, one going to 49.9% (an increment of -0.01%) and another going to 50.1% (an increment of +0.01%). By changing the sign of the increment, we control the direction of the curve. We can also control the other parameters of the cubic bezier (not the V one that should remain a big value) to create more variations from the same curves.

And below, an interactive demo:

Getting back to our example

Let’s get back to our initial example of a ball moving around in the shape of an infinity symbol. I simply combined two sinusoidal animations to make it work.

If we combine what we did previously with the concept of multiple animations, we can get astonishing results. Here again is the initial example, this time as an interactive demo. Change the values and see the magic:

Let’s go further and add a little CSS Houdini to the mix. We can animate a complex transform declaration thanks to @property (but CSS Houdini is limited to Chrome and Edge support at the moment).

What kind of drawings can you make with that? Here is a few that I was able to make:

And here is a spirograph animation:

And a version without CSS Houdini:

There’s a few things to take away from these examples:

  • Each keyframe is defined using only one declaration that contain the increment.
  • The position of the element and the animation are independent. We can easily place the element anywhere without the need to adjust the animation.
  • We made no calculations. There isn’t a ton of angles or pixel values. We only need a tiny value within the keyframe and a big value within the cubic-bezier() function.
  • The whole animation can be controlled just by adjusting the duration value.

What about transition?

The same technique can also be used with the CSS transition property since it follows the same logic when it comes to timing functions. This is great because we’re able to avoid keyframes when creating some complex hover effect.

Here’s what I made without keyframes:

Mario is jumping thanks to the parabolic curve. We needed no keyframes at all to create that shake animation on hover. The sinusoidal curve is perfectly capable of doing all the work.

Here is another version of Mario, this time using CSS Houdini. And, yep, he’s still jumping thanks to the parabolic curve:

For good measure, here are more fancy hover effects without keyframes (again, Chrome and Edge only):

That’s it!

Now you have some magic cubic-bezier() curves and the math behind them. The benefit, of course, is that custom timing functions like this let us do fancy animations without the complex keyframes we generally reach for.

I understand that not everyone is math-minded and that’s okay. There are tools to help, like Matthew Lein’s Ceaser, which lets you drag the curve points around to get what you need. And, if you don’t already have it bookmarked, cubic-bezier.com is another one. If you want to play with cubic-bezier outside the CSS world, I recommend desmos where you can see some math formulas.

Regardless of how you get your cubic-bezier() values, hopefully now you have a sense of their powers and how they can help make for nicer code in the process.


The post Advanced CSS Animation Using cubic-bezier() appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

Float an Element to the Bottom Corner

Need to lay out an element to the right or the left, such that text wraps around it? That’s an easy task for the float property. But what about if you also want to push that element (let’s call it an image) to one of the bottom corners while we’re at it? Sounds a bit tricky, right? We probably need JavaScript?

Nope, few lines of (tricky) CSS can do it! Here’s the CSS-only solution that will make your image to stick to the bottom corner, regardless of the size and content.

Resize the wrapper element and see the magic at work:

Let’s dissect the code.

Markup and layout

We’ll need a wrapper element to contain everything, and we’ll be using flexbox on it. Flexbox allows us to rely on the default stretch alignment to be able to later use height: 100%.

<div class="wrapper">
  <div class="box">
    <div class="float"><img></div>
    Lorem ipsum dolor ...
  </div>
</div>
.wrapper {
  display: flex;
}

.float {
  float: right;
  height: 100%;
  display: flex;
  align-items: flex-end;
  shape-outside: inset(calc(100% - 100px) 0 0);
}

The .box within the .wrapper is our flex item. We don’t need any particular CSS applied to the box. It defines the height of the wrapper and, at the same time, is stretched to the same height. This behavior will give us a “reference height” that can be used by the child elements.

From the specification:

If the flex item has align-self: stretch, redo layout for its contents, treating this used size as its definite cross size so that percentage-sized children can be resolved.

The keyword is the definite which allows us to safely use a percentage (%) height inside the box element.

Now for the floated element

Our .float element will take the entire height next to the text content, thanks to the height calculation we detailed above. Inside this element we push the image to the bottom using flexbox alignment.

A paragraph of lorem i-sum text with a small square image to the right of the text. The image is located at the bottom right of the text and the space above the image is empty.
The image is floated to the right but the free space above it prevents the content from wrapping around it.

Now for the real trickery, using the shape-outside property. Here’s how MDN defines it:

The shape-outside CSS property defines a shape—which may be non-rectangular—around which adjacent inline content should wrap. By default, inline content wraps around its margin box; shape-outside provides a way to customize this wrapping, making it possible to wrap text around complex objects rather than simple boxes.

In other words, shape-outside sets the way content flows around an element’s bounding box.

It takes a number of values. One of those is the inset() function which, again, according to MDN:

Defines an inset rectangle. When all of the first four arguments are supplied they represent the top, right, bottom and left offsets from the reference box inward that define the positions of the edges of the inset rectangle.

So, with shape-outside: inset(calc(100% - X) 0 0) we can create an inset rectangle that starts exactly at the top of the image. And the top is equal to 100% - X, where X is the image height and 100% is the height of the .float element. This allows the text to wrap within the free space on the top of the image. This is responsive, plus we can easily switch between left and right (by adjusting the float property)

That’s it! The only major caveat is that you need to know the image height.

Want more?

We can extend this concept a little further to account for fancier situations. For example, we can float the image to the right, but pin it to the middle of the box with justify-content: center: and also adjust our inset rectangle to the middle by changing the shape-outside from inset(calc(100% - X) 0 0) to inset(calc(50% - X/2) 0 0)

We can also float two images at both bottom corners:

Nothing complex here. I am simply using the same floating element twice, once on the right, and again on the left. And why stop at two corners when we can place images at all four corners:

The same basic idea is at play here, but we’re are also relying on the common float feature for the top images. However, you’ll notice that this is where the concept starts to break down a bit, and we get some unwanted overflow depending on the size of the containing box. We can make the height of the .float element greater than 100% and apply somewhat “magic numbers” that smooth things out by adjusting the padding and margin of the images.

Did you know that shape-outside accepts radial-gradient() as a value? We can use that to place rounded images like below:

The transparent part of the gradient is the free space where the text can go. You may have noticed that we applied a border-radius to the image as well. The shape-outside property will simply affect the .float element and we need to manually adjust the shape of the image to follow the shape defined by shape-outside.

While we’re at it, let’s combine this with our earlier example that pins the image to the vertical center of the box using justify-content: center:

Another radial-gradient() and also another border-radius configuration.

We could have used a linear-gradient() instead to make a triangular shape for the wrapping area:

This is the same idea that we used for the radial-gradient(). The big difference is that we’re using clip-path instead of border-radius to cut our image.

And, since we did it for the others, let’s use the justify-content: center idea to pin the image to the vertical center of the box’s right edge:

We used a conic-gradient() in the above demo with shape-outside to define the triangular shape and clip-path to get a similar shape on the image

All of these examples can still be optimized using less of code in the case that the image is decorative (when it’s not needed inside the HTML for SEO purposes). Let’s replace the .float element with a pseudo-element and apply the image as background instead:

We’re using mask to show just the portion of the image that we need and, guess what, it uses the same value as shape-outside! So, all we had to do is define one value for the shape.

That’s it!

There are a lot of possibilities here to place not just rectangles in corners, but any kind of shape at any position, using largely the same code structure. We only need to:

  • Adjust the shape-outside property to define the shape
  • Apply some styling to the image to follow the previously defined shape or apply the same value to mask in case we are using the pseudo element version

Then everything holds it place, even in responsive designs.


The post Float an Element to the Bottom Corner appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.