CSS Responsive Multi-Line Ribbon Shapes (Part 2)

In my previous article, we tackled ribbons in CSS. The idea was to create a classic ribbon pattern using a single element and values that allow it to adapt to however much content it contains. We established a shape with repeating CSS gradients and tailor-cut the ribbon’s ends with clip-path() to complete the pattern, then used it and wound up with two ribbon variations: one that stacks vertically with straight strands of ribbons and another that tweaks the shape by introducing pseudo-elements.

If you are wondering why I am using 80%, then there is no particular logic to my approach. It’s because I found that covering more space with the color and leaving less space between lines produces a better result for my eye. I could have assigned variables to control the space without touching the core code, but there’s already more than enough complexity going on. So, that’s the reasoning behind the hard-coded value.

Styling The First Ribbon

We’ll start with the red ribbon from the demo. This is what we’re attempting to create:

It may look complex, but we will break it down into a combination of basic shapes.

Stacking Gradients

Let’s start with the gradient configuration, and below is the result we are aiming for. I am adding a bit of transparency to better see both gradients.

h1 {
  --c: #d81a14;

  padding-inline: .8lh;
  background:
    /* Gradient 1 */
    linear-gradient(var(--c) 80%, #0000 0) 
      0 .1lh / 100% 1lh,
    /* Gradient 2 */
    linear-gradient(90deg, color-mix(in srgb, var(--c), #000 35%) 1.6lh, #0000 0) 
      -.8lh 50% / 100% calc(100% - .3lh) repeat-x;
}

We already know all about the first gradient because we set it up in the last section. The second gradient, however, is placed behind the first one to simulate the folded part. It uses the same color variable as the first gradient, but it’s blended with black (#000) in the color-mix() function to darken it a smidge and create depth in the folds.

The thing with the second gradient is that we do not want it to reach the top and bottom of the element, which is why its height is equal to calc(100% - .3lh).

Note the use of padding in the inline direction, which is required to avoid text running into the ribbon’s folds.

Masking The Folded Parts

Now, it’s time to introduce a CSS mask. If you look closely at the design of the ribbon, you will notice that we are cutting triangular shapes from the sides.

We have applied a triangular shape on the left and right sides of the ribbon. Unlike the backgrounds, they repeat every two lines, giving us the complex repetition we want.

Imagine for a moment that those parts are transparent.

That will give us the final shape! We can do it with masks, but this time, let’s try using conic-gradient(), which is nice because it allows us to create triangular shapes. And since there’s one shape on each side, we’ll use two conical gradients — one for the left and one for the right — and repeat them in the vertical direction.


mask:
  conic-gradient(from 225deg at .9lh, #0000 25%, #000 0) 
    0 1lh / 50% 2lh repeat-y,
  conic-gradient(from 45deg at calc(100% - .9lh), #0000 25%, #000 0) 
    100% 0 / 50% 2lh repeat-y;

Each gradient covers half the width (50%) and takes up two lines of text (2lh). Also, note the 1lh offset of the first gradient, which is what allows us to alternate between the two as the ribbon adapts in size. It’s pretty much a zig-zag pattern and, guess what, I have an article that covers how to create zig-zag shapes with CSS masks. I highly recommend reading that for more context and practice applying masks with conical gradients.

Masking The Ribbon’s Ends

We are almost done! All we are missing are the ribbon’s cut edges. This is what we have so far:

We can fill that in by adding a third gradient to the mask:

mask:
  /* New gradient */
  linear-gradient(45deg, #000 50%, #0000 0) 100% .1lh / .8lh .8lh no-repeat,

  conic-gradient(from 225deg at .9lh, #0000 25%, #000 0) 
   0 1lh / 50% 2lh repeat-y,
  conic-gradient(from 45deg  at calc(100% - .9lh), #0000 25%, #000 0) 
   100% 0 / 50% 2lh repeat-y;

That linear gradient will give us the missing part at the top, but we still need to do the same at the bottom, and here, it’s a bit tricky because, unlike the top part, the bottom is not static. The cutout can be either on the left or the right based on the number of lines of text we’re working with:

We will fill in those missing parts with two more gradients. Below is a demo where I use different colors for the newly added gradients to see exactly what’s happening. Use the resize handle to see how the ribbon adjusts when the number of lines changes.

Styling The Second Ribbon

The second ribbon from the demo — the green one — is a variation of the first ribbon.

I am going a little bit faster this time around. We’re working with many of the same ideas and concepts, but you will see how relatively easy it is to create variations with this approach.

The first thing to do is to add some space on the top and bottom for the cutout part. I’m applying a transparent border for this. The thickness needs to be equal to half the height of one line (.5lh).

h1 {
  --c: #d81a14;

  border-block: .5lh solid #0000;
  padding-inline: 1lh;
  background: linear-gradient(var(--c) 80%, #0000 0) 0 .1lh / 100% 1lh padding-box;
}

Note how the background gradient is set to cover only the padding area using padding-box.

Now, unlike the first ribbon, we are going to add two more gradients for the vertical pieces that create the folded darker areas.

h1 {
  --c: #d81a14;

  border-block: .5lh solid #0000;
  padding-inline: 1lh;
  background:
    /* Gradient 1 */
    linear-gradient(var(--c) 80%, #0000 0) 0 .1lh / 100% 1lh padding-box,
    /* Gradient 2 */
    linear-gradient(#0000 50%, color-mix(in srgb, var(--c), #000 35%) 0) 
     0 0 / .8lh 2lh repeat-y border-box,
    /* Gradient 3 */
    linear-gradient(color-mix(in srgb, var(--c), #000 35%) 50%, #0000 0) 
     100% 0 / .8lh 2lh repeat-y border-box;
}

Notice how the last two gradients are set to cover the entire area with a border-box. The height of each gradient needs to equal two lines of text (2lh), while the width should be consistent with the height of each horizontal gradient. With this, we establish the folded parts of the ribbon and also prepare the code for creating the triangular cuts at the start and end of the ribbon.

Here is an interactive demo where you can resize the container to see how the gradient responds to the number of lines of text.

Applying only the conic gradients will also hide the cutout part, so I have to introduce a third gradient to make sure they remain visible:

mask:
  /* New Gradient */
  linear-gradient(#000 1lh, #0000 0) 0 -.5lh,
  /* Left Side */
  conic-gradient(from 225deg at .9lh, #0000 25%, #000 0) 
   0 1lh / 51% 2lh repeat-y padding-box,
  /* Right Side */
  conic-gradient(from 45deg at calc(100% - .9lh), #0000 25%, #000 0) 
   100% 0 / 51% 2lh repeat-y padding-box;

And the final touch is to use clip-path for the cutouts at the ends of the ribbon.

Notice how the clip-path is cutting two triangular portions from the bottom to make sure the cutout is always visible whether we have an odd or even number of lines.

This is how the final code looks when we put everything together:

h1 {
  --c: #d81a14;

  padding-inline: 1lh;
  border-block: .5lh solid #0000;
  background: 
    linear-gradient(var(--c) 80%, #0000 0)
      0 .1lh / 100% 1lh padding-box,
    linear-gradient(#0000 50%, color-mix(in srgb,var(--c), #000 35%) 0)
      0 0 / .8lh 2lh repeat-y border-box,
    linear-gradient(color-mix(in srgb, var(--c), #000 35%) 50%, #0000 0)
      100% 0 / .8lh 2lh repeat-y border-box;
  mask:
    linear-gradient(#000 1lh, #0000 0) 0 -.5lh,
    conic-gradient(from 225deg at .9lh,#0000 25%,#000 0)
     0 1lh/51% 2lh repeat-y padding-box,
    conic-gradient(from 45deg at calc(100% - .9lh), #0000 25%, #000 0)
     100% 0 / 51% 2lh repeat-y padding-box;
  clip-path: polygon(0 0, calc(100% - .8lh) 0,
    calc(100% - .4lh) .3lh,
    100% 0, 100% 100%,
    calc(100% - .4lh) calc(100% - .3lh),
    calc(100% - .8lh) 100%, .8lh 100%, .4lh calc(100% - .3lh), 0 100%);
}

I challenged you to find a way to reverse the direction of the first ribbon by adjusting the gradient values. Try to do the same thing here!

It may sound difficult. If you need a lifeline, you can get the code from my online collection, but it’s the perfect exercise to understand what we are doing. Explaining things is good, but nothing beats practicing.

The Final Demo

Here is the demo once again to see how everything comes together.

See the Pen Responsive multi-line ribbon shapes by Temani Afif.

Wrapping Up

There we go, two more ribbons that build off of the ones we created together in the first article of this brief two-part series. If there’s only one thing you take away from these articles, I hope it’s that modern CSS provides us with powerful tools that offer different, more robust approaches to things we used to do a long time ago. Ribbons are an excellent example of a long-living design pattern that’s been around long enough to demonstrate how creating them has evolved over time as new CSS features are released.

I can tell you that the two ribbons we created in this article are perhaps the most difficult shapes in my collection of ribbon shapes. But if you can wrap your head around the use of gradients — not only for backgrounds but masks and clipping paths as well — you’ll find that you can create every other ribbon in the collection without looking at my code. It’s getting over that initial hurdle that makes this sort of thing challenging.

You now have the tools to make your own ribbon patterns, too, so why not give it a try? If you do, please share them in the comments so I can see your work!

Further Reading On SmashingMag

CSS Responsive Multi-Line Ribbon Shapes (Part 1)

Back in the early 2010s, it was nearly impossible to avoid ribbon shapes in web designs. It was actually back in 2010 that Chris Coyier shared a CSS snippet that I am sure has been used thousands of times over.

And for good reason: ribbons are fun and interesting to look at. They’re often used for headings, but that’s not all, of course. You’ll find corner ribbons on product cards (“Sale!”), badges with trimmed ribbon ends (“First Place!”), or even ribbons as icons for bookmarks. Ribbons are playful, wrapping around elements, adding depth and visual anchors to catch the eye’s attention.

I have created a collection of more than 100 ribbon shapes, and we are going to study a few of them in this little two-part series. The challenge is to rely on a single element to create different kinds of ribbon shapes. What we really want is to create a shape that accommodates as many lines of text as you throw at them. In other words, there is no fixed dimension or magic numbers — the shape should adapt to its content.

Here is a demo of what we are building in this first part:

Sure, this is not the exact ribbon shape we want, but all we are missing is the cutouts on the ends. The idea is to first start with this generic design and add the extra decoration as we go.

Both ribbons in the demo we looked at are built using pretty much the same exact CSS; the only differences are nuances that help differentiate them, like color and decoration. That’s my secret sauce! Most of the ribbons from my generator share a common code structure, and I merely adjust a few values to get different variations.

Let’s Start With The Gradients

Any time I hear that a component’s design needs to be repeated, I instantly think of background gradients. They are perfect for creating repeatable patterns, and they are capable of drawing lines with hard stops between colors.

We’re essentially talking about applying a background behind a text element. Each line of text gets the background and repeats for as many lines of text as there happens to be. So, the gradient needs to be as tall as one line of text. If you didn’t know it, we recently got the new line height (lh) unit in CSS that allows us to get the computed value of the element’s line-height. In our case, 1lh will always be equal to the height of one line of text, which is perfect for what we need.

Note: It appears that Safari uses the computed line height of a parent element rather than basing the lh unit on the element itself. I’ve accounted for that in the code by explicitly setting a line-height on the body element, which is the parent in our specific case. But hopefully, that will be unnecessary at some point in the future.

Let’s tackle our first gradient. It’s a rectangular shape behind the text that covers part of the line and leaves breathing space between the lines.

The gradient’s red color is set to 70% of the height, which leaves 30% of transparent color to account for the space between lines.

h1 {
  --c: #d81a14;

  background-image: linear-gradient(var(--c) 70%, #0000 0);
  background-position: 0 .15lh;
  background-size: 100% 1lh;
}

Nothing too complex, right? We've established a background gradient on an h1 element. The color is controlled with a CSS variable (--c), and we’ve sized it with the lh unit to align it with the text content.

Note that the offset (.15lh) is equal to half the space between lines. We could have used a gradient with three color values (e.g., transparent, #d81a14, and transparent), but it’s more efficient and readable to keep things to two colors and then apply an offset.

Next, we need a second gradient for the wrapped or slanted part of the ribbon. This gradient is positioned behind the first one. The following figure demonstrates this with a little opacity added to the front ribbon’s color to see the relationship better.

Here’s how I approached it:

linear-gradient(to bottom right, #0000 50%, red 0 X, #0000 0);

This time, we’re using keywords to set the gradient’s direction (to bottom right). Meanwhile, the color starts at the diagonal (50%) instead of its default 0% and should stop at a value that we’re indicating as X for a placeholder. This value is a bit tricky, so let’s get a visual that illustrates what we’re doing.

The green arrow illustrates the gradient direction, and we can see the different color stops: 50%, X, and 100%. We can apply some geometry rules to solve for X:

(X - 50%) / (100% - 50%) = 70%/100%
X = 85%

This gives us the exact point for the end of the gradient’s hard color stop. We can apply the 85% value to our gradient configuration in CSS:

h1 {
  --c: #d81a14;

  background-image: 
    linear-gradient(var(--c) 70%, #0000 0), 
    linear-gradient(to bottom left, #0000 50%, color-mix(in srgb, var(--c), #000 40%) 0 85%, #0000 0);
  background-position: 0 .15lh;
  background-size: 100% 1lh;
}

You’re probably noticing that I added the new color-mix() function to the second gradient. Why introduce it now? Because we can use it to mix the main color (#d81a14) with white or black. This allows us to get darker or lighter values of the color without having to introduce more color values and variables to the mix. It helps keep things efficient!

We have all of the coordinates we need to make our cuts using the polygon() function on the clip-path property. Coordinates are not always intuitive, but I have expanded the code and added a few comments below to help you identify some of the points from the figure.

h1 {
  --r: 10px; /* control the cutout */

  clip-path: polygon(
   0 .15lh, /* top-left corner */
   100% .15lh, /* top right corner */
   calc(100% - var(--r)) .5lh, /* top-right cutout */
   100% .85lh,
   100% calc(100% - .15lh), /* bottom-right corner  */
   0 calc(100% - .15lh), /* bottom-left corner */
   var(--r) calc(100% - .5lh), /* bottom-left cutout */
   0 calc(100% - .85lh)
  );
}

This completes the first ribbon! Now, we can wrap things up (pun intended) with the second ribbon.

The Second Ribbon

We will use both pseudo-elements to complete the shape. The idea can be broken down like this:

  1. We create two rectangles that are placed at the start and end of the ribbon.
  2. We rotate the two rectangles with an angle that we define using a new variable, --a.
  3. We apply a clip-path to create the triangle cutout and trim where the green gradient overflows the top and bottom of the shape.

First, the variables:

h1 {
  --r: 10px;  /* controls the cutout */
  --a: 20deg; /* controls the rotation */
  --s: 6em;   /* controls the size */
}

Next, we’ll apply styles to the :before and :after pseudo-elements that they share in common:

h1:before,
h1:after {
  content: "";
  position: absolute;
  height: .7lh;
  width: var(--s);
  background: color-mix(in srgb, var(--c), #000 40%);
  rotate: var(--a);
}

Then, we position each pseudo-element and make our clips:

h1:before {
  top: .15lh;
  right: 0;
  transform-origin: top right;
  clip-path: polygon(0 0, 100% 0, calc(100% - .7lh / tan(var(--a))) 100%, 0 100%, var(--r) 50%);
}

h1:after {
  bottom: .15lh;
  left: 0;
  transform-origin: bottom left;
  clip-path: polygon(calc(.7lh / tan(var(--a))) 0, 100% 0, calc(100% - var(--r)) 50%, 100% 100%, 0 100%);
}

We are almost done! We still have some unwanted overflow where the repeating gradient bleeds out of the top and bottom of the shape. Plus, we need small cutouts to match the pseudo-element’s shape.

It’s clip-path again to the rescue, this time on the main element:

clip-path: polygon(
    0 .15lh,
    calc(100% - .7lh/sin(var(--a))) .15lh,
    calc(100% - .7lh/sin(var(--a)) - 999px) calc(.15lh - 999px*tan(var(--a))),
    100% -999px,
    100% .15lh,
    calc(100% - .7lh*tan(var(--a)/2)) .85lh,
    100% 1lh,
    100% calc(100% - .15lh),
    calc(.7lh/sin(var(--a))) calc(100% - .15lh),
    calc(.7lh/sin(var(--a)) + 999px) calc(100% - .15lh + 999px*tan(var(--a))),
    0 999px,
    0 calc(100% - .15lh),
    calc(.7lh*tan(var(--a)/2)) calc(100% - .85lh),
    0 calc(100% - 1lh)
);

Ugh, looks scary! I’m taking advantage of a new set of trigonometric functions that help a bunch with the calculations but probably look foreign and confusing if you’re seeing them for the first time. There is a mathematical explanation behind each value in the snippet that I’d love to explain, but it’s long-winded. That said, I’m more than happy to explain them in greater detail if you drop me a line in the comments.

Our second ribbon is completed! Here is the full demo again with both variations.

You can still find the code within my ribbons collection, but it’s a good exercise to try writing code without. Maybe you will find a different implementation than mine and want to share it with me in the comments! In the next article of this two-part series, we will increase the complexity and produce two more interesting ribbon shapes.

Further Reading On SmashingMag

Shines, Perspective, And Rotations: Fancy CSS 3D Effects For Images

We all agree that 3D effects are cool, right? I think so, especially when they are combined with subtle animations. In this article, we will explore a few CSS tricks to create stunning 3D effects!

“Why do we need another article about CSS 3D effects… aren’t there already a million of those?” Yes, but this one is a bit special because we are going to work with the smallest amount of HTML possible. In fact, this is the only markup we will use to craft some pretty amazing CSS effects for images:

<img src="" alt="">

That’s it! All we need is an <img> tag. Everything else will be done in CSS.

Here’s how it’s going to work. We are going to explore three different effects that are not linked to each other but might borrow a little from one another. You don’t need to read the entire article in one sitting. Actually, I suggest reading one section at a time, taking time to understand the concepts and what the underlying code is doing before moving on to another effect.

Table Of Contents

CSS 3D Shine

For the first effect, we are going to add a shine animation to the image, as well as a slight rotation when hovered.

The green box illustrates the gradient where the blue lines define the color stops we used. Initially, it’s placed at 100% 100%, and on hover, we slide it to 0 0. The slide effect will move the diagonal part of the gradient (the opaque part) along the image to create the shine effect.

Here is the full demo again. I’m even including a second variation for you to tear apart and investigate how it works.

The clip-path defines the clipped area, and we need that area to remain fixed. That’s why we added a translation on hover to move the image in the opposite direction of the clip-path.

Here’s how that works. First, we add some padding to the top and the bottom of the image and apply an outline that is semi-transparent black.

Second, we apply a negative outline-offset so that the outline covers the image on the left and right sides but leaves the top and bottom alone:

img {
  --d: 18px;  /* depth */

  padding-block: var(--d);
  outline: var(--d) solid #0008;
  outline-offset: calc(-1 * var(--d));
}

Notice that I have created a variable, --d, that controls the thickness of the outline. This is what gives the image depth.

The last step is to add the clip-path. We need a polygon with eight points for that.

The red points are fixed, and the green points are ones that we will animate to reveal the depth. I know it’s far from a 3D box, but this next visual, where we add the rotation, gives a better illustration.

Initially, the image is rotated with some perspective. The green points on the right are aligned with the red ones. Thus, we hide the outline on that side to keep it visible only on the left side. We have our 3D box with the depth on the left.

On hover, we move the green points on the left while rotating the image. Halfway through the animation, all the green points are aligned with the red ones, and the rotation angle is equal to 0deg, hiding the outline and giving the image a flat appearance.

Then, we continue the rotation, and the green points on the right move while the left ones remain in place. We get the same 3D effect but with the depth on the right side.

Bear with me because the next block of code is going to look really confusing at first. That’s due to a few new variables and the eight-point polygon we’re drawing on the clip-path property.

@property --_l {
  syntax: "<flength>";
  initial-value: 0px;
  inherits: true;
}
@property --_r {
  syntax: "<length>";
  initial-value: 0px;
  inherits: true;
}

img {
  --d: 18px;  /* depth */
  --a: 20deg; /* angle */
  --x: 10px;

  --_d: calc(100% - var(--d));
  --_l: 0px;
  --_r: 0px;

  clip-path: polygon(
    /* The two green points on the left */
    var(--_l) calc(var(--_d) - var(--x)),
    var(--_l) calc(var(--d)  + var(--x)),

    /* The two red points on the top */
    var(--d) var(--d),var(--_d) var(--d),

    /* The two green points on the right */
    calc(var(--_d) + var(--_r)) calc(var(--d)  + var(--x)),
    calc(var(--_d) + var(--_r)) calc(var(--_d) - var(--x)),

    /* The two red points on the bottom */
    var(--_d) var(--_d),var(--d) var(--_d)

    );
  transition: transform .3s, --_r .15s, --_l .15s .15s;
}

/* Update the points of the polygon on hover */
img:hover{
  --_l: var(--d);
  --_r: var(--d);
  --_i: -1;
  transition-delay: 0s, .15s, 0s;
}

I’ve used comments to help explain what the code is doing. Notice I am using the variables --_l and --_r to define the position of the green points. I animate those variables from 0 to the depth (--d) value. The @property declarations at the top allow us to animate the variables by specifying the type of values they are (<length>).

Note: Not all browsers currently support @property. So, I’ve added a fallback in the demo with a slightly different animation.

After the polygon is drawn on the clip-path property, the next thing the code does is apply a transition that handles the rotation. The full rotation lasts .3s, so the green points need to transition at half that duration (.15s). On hover, the polygon points on the left move immediately (0s) while the right points move at half the duration (courtesy of a .15s delay). When we leave the hovered state, we use different delays because we need the right points to move immediately (0s) while the left points move at half the duration.

What’s up with that --x variable, right? If you check the first image that I provided to illustrate the clip-path points, you will notice that the green points are slightly shifted from the top and bottom edges, which is logical to simulate the 3D effect. The --x variable controls how much shifting takes place, but the math behind it is a bit complex and not easy to express in CSS. So, we update it manually based on each case until we get a value that feels right.

That gives us our final result!

See the Pen 3D images with hover effect by Temani Afif.

Wrapping Up

I hope you enjoyed — and perhaps were even challenged by — this exploration of CSS 3D image effects. We worked with a whole bunch of advanced CSS features, including masks, clipping, gradients, transitions, and calculations, to make some pretty incredible hover effects for images that you certainly don’t see every day.

And we did it in a way that only needed one line of HTML. No divs. No classes or IDs. No pseudo-elements. Just a single <img> tag is all we need. Yes, it’s true that more markup may have made the CSS less complex, but the fact that it relies on a plain HTML element means the CSS can be used more broadly. CSS is powerful enough to do all of this on a single element!

I’ve written extensively about advanced CSS styles for images. If you’re looking for more ideas and inspiration, I encourage you to check out the following articles I’ve published:

I also run a site called CSS Tip that explores even more fancy effects — subscribe to the RSS feed to keep up with the experiments I do over there!

Further Reading On SmashingMag

Single Element Loaders: Going 3D!

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

Article series

The split cube loader

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Now let’s make ‘em move!

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

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

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

Here’s the final demo once again:

The progress cube loader

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

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

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

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

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

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

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

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

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

That’s a little better:

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

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

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

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

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

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

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

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

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

More 3D

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Here’s the full thing:

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

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

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

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

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

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

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

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

Variations galore!

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

That’s a wrap

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

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

Article series


Single Element Loaders: Going 3D! originally published on CSS-Tricks. 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.

Collective #705






Collective 705 item image

Awwwards Conference

Three exciting days with some of the most influential speakers of the industry, who inspire, teach, and guide us as we face the many challenges and opportunities which lie ahead in the future of the web.

Check it out






Collective 705 item image

Canvas + Text

Paul Henschel made a demo that shows how to form self-contained components with their own state and user interaction.

Check it out



Collective 705 item image

Rhea

Rhea is a geometry-shader based grass for Unity’s Universal Render Pipeline (URP).

Check it out


Collective 705 item image

Color Morph

Randomly generate beautiful mesh gradients, export them as an SVG or copy the generated CSS code into your project.

Check it out


Collective 705 item image

Bionic Reading

An amazing project: Bionic Reading revises texts so that the most concise parts of words are highlighted which enables faster, more efficient reading.

Check it out



Collective 705 item image

PWA Resources

A curated collection of resources to learn about Progressive Web Apps. Also, a super cool site design.

Check it out


Collective 705 item image

Building a Vaporwave scene with Three.js

A step-by-step tutorial documenting Maxime Heckel’s attempt at reverse-engineering the vaporwave WebGL scene from the Linear 2021 release page using only fundamental concepts of Three.js like textures, lights, animations, and post-processing effects.

Read it


Collective 705 item image

Tao of Node

Alex Kondov summarizes the set of principles that he has established for building Node applications.

Read it





Collective 705 item image

Coolify

Version 2 of the amazing open-source and self-hostable Heroku/Netlify alternative.

Check it out



The post Collective #705 appeared first on Codrops.

Collective #700



Collective 700 Item image

Our Sponsor

Build Using the DoorDash API

DoorDash Developer allows you to easily integrate delivery services into your website or app. We’ve created the API, you set the rules, offer delivery on your terms and only pay for what you use. And integration is incredibly easy.

Get Started









Collective 700 Item image

MicroWaver 59™

A fun game: Fifty-nine seconds of delicious, electromagnetic horsepower. Are you hungry for the future? Insert a coin and wave away inside this hyper-interactive food machine. By Pink Yellow.

Check it out






Collective 700 Item image

Shaders and Gradients

The second issue of Offscreen Canvas is about learning shaders and explores why gradients are so important, and more.

Read it




Collective 700 Item image

Google Search Is Dying

A very interesting article on how Reddit is currently the most popular search engine and how Google Search is not working anymore the way it should.

Check it out



Collective 700 Item image

In case you didn’t know about it: jless is a command-line JSON viewer designed for reading, exploring, and searching through JSON data.

Check it out




The post Collective #700 appeared first on Codrops.

The Search For a Fixed Background Effect With Inline Images

I was working on a client project a few days ago and wanted to create a certain effect on an <img>. See, background images can do the effect I was looking for somewhat easily with background-attachment: fixed;. With that in place, a background image stays in place—even when the page scrolls. It isn’t used all that often, so the effect can look unusual and striking, especially when used sparingly.

It took me some time to figure out how to achieve the same effect only with an inline image, rather than a CSS background image. This is a video of the effect in action:

The exact code for the above demo is available in this Git repo. Just note that it’s a Next.js project. We’ll get to a CodePen example with raw HTML in a bit.

Why use <img> instead of background-image?

The are a number of reasons I wanted this for my project:

  • It’s easier to lazy load (e.g. <img loading="lazy"… >.
  • It provides better SEO (not to mention accessibility), thanks to alt text.
  • It’s possible to use srcset/sizes to improve the loading performance.
  • It’s possible to use the <picture> tag to pick the best image size and format for the user’s browser.
  • It allows users to download save the image (without resorting to DevTools).

Overall, it’s better to use the image tag where you can, particularly if the image could be considered content and not decoration. So, I wound up landing on a technique that uses CSS clip-path. We’ll get to that in a moment, right after we first look at the background-image method for a nice side-by-side comparison of both approaches.

1. Using CSS background-image

This is the “original” way to pull off a fixed scrolling effect. Here’s the CSS:

.hero-section {
  background-image: url("nice_bg_image.jpg");
  background-repeat: no-repeat;
  background-size: cover;
  background-position: center; 
  background-attachment: fixed;
}

But as we just saw, this approach isn’t ideal for some situations because it relies on the CSS background-image property to call and load the image. That means the image is technically not considered content—and thus unrecognized by screen readers. If we’re working with an image that is part of the content, then we really ought to make it accessible so it is consumed like content rather than decoration.

Otherwise, this technique works well, but only if the image spans the whole width of the viewport and/or is centered. If you have an image on the right or left side of the page like the example, you’ll run into a whole number of positioning issues because background-position is relative to the center of the viewport.

Fixing it requires a few media queries to make sure it is positioned properly on all devices.

2. Using the clip-path trick on an inline image

Someone on StackOverflow shared this clip-path trick and it gets the job done well. You also get to keep using the<img> tag, which, as we covered above, might be advantageous in some circumstances, especially where an image is part of the content rather than pure decoration.

Here’s the trick:

.image-container {
  position: relative;
  height: 200px;
  clip-path: inset(0);
}

.image {
  object-fit: cover;
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
}

Check it out in action:

Now, before we rush out and plaster this snippet everywhere, it has its own set of downsides. For example, the code feels a bit lengthy to me for such a simple effect. But, even more important is the fact that working with clip-path comes with some implications as well. For one, I can’t just slap a border-radius: 10px; in there like I did in the earlier example to round the image’s corners. That won’t work—it requires making rounded corners from the clipping path itself.

Another example: I don’t know how to position the image within the clip-path. Again, this might be a matter of knowing clip-path really well and drawing it where you need to, or cropping the image itself ahead of time as needed.

Is there something better?

Personally, I gave up on using the fixed scrolling effect on inline images and am back to using a CSS background image—which I know is kind of limiting.

Have you ever tried pulling this off, particularly with an inline image, and managed it well? I’d love to hear!


The Search For a Fixed Background Effect With Inline Images originally published on CSS-Tricks. You should get the newsletter and become a supporter.

Reducing The Need For Pseudo-Elements

Per the W3C spec, “a pseudo-element represents an element not directly present in the document tree". They have been around since version 1 of the CSS specification, when ::first-letter and ::first-line were introduced. The popular ::before and ::after pseudo-elements were added in version 2 — these represent content that does not exist in the source document at all. They can be thought of as two extra elements you can “tack onto” their originating element. When front-end developers hear “pseudo-elements”, we think of ::before and ::after more often than not, as we use them in various ways to add decorations to our elements.

There are additional pseudo-elements beyond these. They are listed in the spec across three categories: typographic, highlight, and tree-abiding.

Interestingly, after years of web development, I never found myself using ::first-line, but it’s pretty neat and responds well to window resizing! Check it out.

Pseudo-Element Version

Many of you reading this will be accustomed to a pseudo-element version:

  • We use a relatively positioned wrapper element with large right padding to accommodate our angle — this is our <button>;
  • Many of us, students of the sliding doors technique, are accustomed to nesting an element to take on the button’s background-color;
  • Finally, we absolutely position a pseudo-element with its border rules into our <button>’s right padding empty space — we use ::before for this.

Aside from those steps, our hover styles must account for both our nested element and pseudo-element. This might seem manageable for you, but the more complicated our button designs get, the more overhead we have with hover styles. Also, with this version, buttons with word wrapping just plain fail.

Visit the final showcase to see these other button styles made easier without pseudo-elements. In particular, the blue bevel button’s pseudo-element version is pretty brutal. The amount of overall work is greatly reduced thanks to clip-path.

Button Wipes

A wiping effect is a popular button style. I’ve included left-to-right and top-to-bottom wipes.

Pseudo-Element Version

This can be achieved by transitioning a pseudo-element’s transform.

  • We absolutely position a ::before pseudo-element and give it a transform: scaleX(0) so it’s not visible.
  • We also must explicitly set its transform-origin: 0 0 to ensure the wipe comes in from the left rather than center (transform-origin defaults to center).
  • We set up transitions on the transform for some smooth jazz animation on/off hover.
  • Because our pseudo-element is absolutely positioned, we require a nested element to hold the button’s text, position: relative on this nested element creates a new stacking context so our text stays on top of our wiping pseudo-element.
  • On hover, we can target our pseudo-element and transition its scaleX to now be 1 (transform: scaleX(1)).

See the Pen Button wipe with pseudo-element by Marcel.

No Pseudo-Element Version

Why worry about nested elements, pseudo-element positioning, stacking contexts, and sprawling hover rules if we don’t have to?

We can reach for linear-gradient() and background-size to nail this down.

  • We give our <button> a background-color for its default state, while also setting up a linear-gradient via background-image — but the background-size will be 0, so we won’t see anything by default.
  • On hover, we transition the background-size to 100% 100% which gives us our wipe effect!

Remember, linear-gradient() uses the background-image property and background-image supersedes background-color, so this is what takes precedence on hover.

That’s it. No nested element required. Want a vertical wipe? Just change the linear-gradient direction and the background-size values. I’ve changed those via CSS custom properties.

See the Pen Button wipe with NO pseudo-element by Marcel.

Tiles With Screen Color Overlays

This is a common pattern where a semi-transparent color overlays a tile/card. Our example’s tile also has a background-image. It’s often important in this pattern to retain a set aspect-ratio so that tiles look uniform if more than one appears in a set.

Pseudo Version

Some of the same things come into play with our pseudo-element version:

  • We use the aspect-ratio “padding-trick”, setting a 60% padding-top value (5:3 ratio) for our tile.
  • We must position our screen color overlay pseudo-element, giving it a 100% width and height to fill the tile — we target this pseudo-element on hover to change its background-color.
  • Due to the pseudo-element’s absolute positioning, we must use a nested element for our text content, also giving it position: absolute in order for it to appear above our screen color overlay in the stacking order and to ensure it appears where it should within the tile.

See the Pen Tile screen color overlay with pseudo-element by Marcel.

No Pseudo-Element Version

It can be much simpler thanks to the aspect-ratio and background-blend-mode properties.

Note: aspect-ratio does not work in Safari 14.x, but will in version 15.

That said, as of this writing, caniuse lists it with 70%+ global support.

  • The “padding-trick” is replaced by aspect-ratio: 400/240 (we could use any 5:3-based value here).
  • We use both background-image and background-color properties in conjunction with background-blend-mode — simply change the background-color of our tile element on hover.
Background-blend-mode

background-blend-mode blends a background-color with an element’s background-image. Any Photoshop users reading this will find background-blend-mode reminiscent of Photoshop’s blending modes. Unlike mix-blend-mode, background-blend-mode does not create a new stacking context! So no z-index hell!

See the Pen Tile screen color overlay with NO pseudo-element by Marcel.

Conclusion

Front-end development is exciting and fast-moving. With newer CSS properties, we can brush the dust off our old techniques and give them another look. Doing this helps foster reduced and simpler code. Pseudo-elements are helpful, but we don’t need to reach for them as much.

Perfect Tooltips With CSS Clipping and Masking

Clipping and masking have been around for a while now in CSS and even have pretty decent browser support. I recently worked on a project that needed to use a clipping technique for tooltips showing above links in text.

Those tooltips have two designs based on their content:

One design is a tooltip that contains plain text against a solid background.
The other design allows an image to cover the entire space.

You might not think the text tooltip requires any clipping at all. A pseudo-element can be positioned at the bottom to add the little notch, right? You are indeed absolutely right! Because the background of the tooltip is a a plain color, there’s really no need for CSS trickery and whatnot.

But clipping the image in the second design is where things get interesting…

Here’s the thought process my mind followed when I started the task.

Idea 1: clip-path & polygon

The CSS clip-path property allows us to define a custom polygon with percentage values to make the path we want.
This solution is often enough if the shape of your path is simple enough. In the demo below, I’m using calc() values to make sure the clip is fully responsive, while the little triangle stays the same size no matter how stretched the parent is.

.tooltip {
  clip-path: polygon(
    0% 0%, // Top left point
    100% 0%, // Top right point
    100% calc(100% - 10px), // Bottom right point
    calc(50% + 10px) calc(100% - 10px), // Center right of the triangle
    50% 100%, // Tip of the triangle
    calc(50% - 10px) calc(100% - 10px), // Center left of the triangle
    0% calc(100% - 10px) // Bottom left point
  );
}

This solution is very clean but, in my case, not good enough as I don’t have a straight triangle notch, but rather a custom shape.

Idea 2: clip-path and SVG

Using an SVG path seemed like a good solution. First, you export your SVG clipping path, then use it in your CSS with the url(#clipPathId) value.

Check the demo below. Do you see any issue with the path?

The arrow is stretched based on the image ratio. Since the little notch is part of the whole path shape, it is as stretched as the rectangle part of the path stretches in size.

Idea 3: mask-image

Now here is the thing I discovered with the CSS mask-image property in CSS: You can combine mask layers! Think about it like a background-image in CSS. You can apply multiple gradients or images on a single element. Now, what if you combine all those layers to generate the final mask you need?

This is exactly what we are going to do here with two layers:

  1. A large rectangle that cover the whole block except for a stripe at the bottom (shown in green)
  2. An image of the arrow (shown in pink)

With that solution, the rectangle can stretch according to our tooltip’s dimensions, and the arrow will always keep its fixed size.

All the code and demos below are prefix free and the demos are using Autoprefixer. At the time I’m writing this article, Edge, Chrome & Safari require prefixes.

Just as we would with background properties, we are going to use three different mask properties to define our two layers:

  • mask-image: This property lets us draw the rectangle with a linear background and the arrow with an inline SVG.
  • mask-position: The rectangle doesn’t need a position (as it starts from the top-left), but the arrow needs to be positioned at the center-bottom.
  • mask-repeat: We need to avoid repeating both layers; otherwise, the linear gradient would cover the whole element when it repeats.
.tooltip {
  mask-image:
    linear-gradient(#fff, #fff), /* Rectangle */
    url('data:image/svg+xml;utf8,'); /* Bottom arrow mask-position: */
    0 0, /* Rectangle */
    50% 100%; /* Bottom arrow */
  mask-size:
    100% calc(100% - 18px), /* Rectangle */
    38px 18px; /* Bottom arrow */
  mask-repeat: no-repeat;
}

Tada! Change the tooltip dimensions or replace the image and the bottom arrow will keep its original ratio.

More complex shapes

Let’s get a little fancy and go deeper with this technique. I was inspired by the iMessage app on iOS and tried to reproduce the same tooltips with this masking technique.

I had to draw more layers for my mask to render every rounded corner:

  • four circles, one for each corner (shown in red)
  • one horizontal rectangle (shown in blue)
  • one vertical rectangle (shown in green)
  • one SVG for the arrow (shown in yellow)

The full code is going to be a bit longer as we have more layers to draw, but the logic stays the same. The corners are drawn using four radial gradients. To fill the rectangle, we need two rectangles (one vertical, one horizontal) as shown above. And finally, our little arrow that is using an inline SVG.

.tooltip {
  --radius: 25px;
  mask-image:
    radial-gradient(#fff (var(--radius) - 1), #fff0 var(--radius)), /* Top left corner */
    radial-gradient(#fff (var(--radius) - 1), #fff0 var(--radius)), /* Top right corner */
    radial-gradient(#fff (var(--radius) - 1), #fff0 var(--radius)), /* Bottom left corner */
    radial-gradient(#fff (var(--radius) - 1), #fff0 var(--radius)), /* Bottom right corner */
    linear-gradient(#fff, #fff), /* Horizontal gradient */
    linear-gradient(#fff, #fff), /* Vertical gradient */
    url('data:image/svg+xml;utf8,'); /* Bottom right icon */
  mask-position: 
    0 0, /* Top left corner */
    100% 0, /* Top right corner */
    0 100%, /* Bottom left corner */
    100% 100%, /* Bottom right corner */
    0 var(--radius), /* Horizontal gradient */
    var(--radius) 0, /* Vertical gradient */
    100% 100%; /* Bottom right icon */
  mask-size:
    (var(--radius) * 2) (var(--radius) * 2),  /* Top left corner */
    (var(--radius) * 2) (var(--radius) * 2),  /* Top right corner */
    (var(--radius) * 2) (var(--radius) * 2),  /* Bottom left corner */
    (var(--radius) * 2) (var(--radius) * 2),  /* Bottom right corner */
    100% calc(100% - #{var(--radius) * 2}), /* Horizontal gradient */
    calc(100% - #{var(--radius) * 2}) 100%, /* Vertical gradient */
    (39px / 2) (25px / 2); /* Bottom right icon */
  mask-repeat: no-repeat;
}

As you see, we can create a version with the arrow on the left or right by using a flipped version of the arrow and positioning it in a different corner. The trick is working fine on tooltips without images too. But like I said at the beginning of this article, you probably don’t need that much CSS if you only have a plain background to style.


If you want to learn more about clipping and masking in CSS, there are lots of other great articles right here on CSS-Tricks worth checking out.


The post Perfect Tooltips With CSS Clipping and Masking appeared first on CSS-Tricks.

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