Tag: Effect

Recreating MDN’s Truncated Text Effect

It’s no secret that MDN rolled out a new design back in March. It’s gorgeous! And there are some sweet CSS-y gems in it that are fun to look at. One of those gems is how card components handle truncated text.

Pretty cool, yeah? I wanna tear that apart in just a bit, but a couple of things really draw me into this approach:

  • It’s an example of intentionally cutting off content. We’ve referred to that as CSS data loss in other places. And while data loss is generally a bad thing, I like how it’s being used here since excerpts are meant to be a teaser for the full content.
  • This is different than truncating text with text-overflow: ellipsis, a topic that came up rather recently when Eric Eggert shared his concerns with it. The main argument against it is that there is no way to recover the text that gets cut off in the truncation — assistive tech will announce it, but sighted users have no way to recover it. MDNs approach provides a bit more control in that department since the truncation is merely visual.

So, how did MDN do it? Nothing too fancy here as far the HTML goes, just a container with a paragraph.

<div class="card">   <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Inventore consectetur temporibus quae aliquam nobis nam accusantium, minima quam iste magnam autem neque laborum nulla esse cupiditate modi impedit sapiente vero?</p> </div>

We can drop in a few baseline styles to shore things up.

Again, nothing too fancy. Our goal is cut the content off after, say, the third line. We can set a max-height on the paragraph and hide the overflow for that:

.card p {   max-height: calc(4rem * var(--base)); /* Set a cut-off point for the content */   overflow: hidden; /* Cut off the content */ }

Whoa whoa, what’s up with that calc() stuff? Notice that I set up a --base variable up front that can be used as a common multiplier. I’m using it to compute the font-size, line-height, padding for the card, and now the max-height of the paragraph. I find it easier to work with a constant values especially when the sizing I need is really based on scale like this. I noticed MDN uses a similar --base-line-height variable, probably for the same purpose.

Getting that third line of text to fade out? It’s a classic linear-gradient() on the pargraph’s :after pseudo-element, which is pinned to the bottom-right corner of the card. So, we can set that up:

.card p:after {   content: ""; /* Needed to render the pseudo */   background-image: linear-gradient(to right, transparent, var(--background) 80%);   position: absolute;   inset-inline-end: 0; /* Logical property equivalent to `right: 0` */ }

Notice I’m calling a --background variable that’s set to the same background color value that’s used on the .card itself. That way, the text appears to fade into the background. And I found that I needed to tweak the second color stop in the gradient because the text isn’t completely hidden when the gradient blends all the way to 100%. I found 80% to be a sweet spot for my eyes.

And, yes, :after needs a height and width. The height is where that --base variables comes back into play because we want that scaled to the paragraph’s line-height in order to cover the text with the height of :after.

.card p:after {   /* same as before */   height: calc(1rem * var(--base) + 1px);   width: 100%; /* relative to the .card container */ }

Adding one extra pixel of height seemed to do the trick, but MDN was able to pull it off without it when I peeked at DevTools. Then again, I’m not using top (or inset-block-start) to offset the gradient in that direction either. 🤷‍♂️

Now that p:after is absolutely positioned, we need to explicitly declare relative positioning on the paragraph to keep :after in its flow. Otherwise, :after would be completely yanked from the document flow and wind up outside of the card. This becomes the full CSS for the .card paragraph:

.card p {   max-height: calc(4rem * var(--base)); /* Set a cut-off point for the content */   overflow: hidden; /* Cut off the content */   position: relative; /* needed for :after */ }

We’re done, right? Nope! The dang gradient just doesn’t seem to be in the right position.

I’ll admit I brain-farted on this one and fired up DevTools on MDN to see what the heck I was missing. Oh yeah, :after needs to be displayed as a block element. It’s clear as day when adding a red border to it.🤦‍♂️

.card p:after {   content: "";   background: linear-gradient(to right, transparent, var(--background) 80%);   display: block;   height: calc(1rem * var(--base) + 1px);   inset-block-end: 0;   position: absolute;   width: 100%; }

All together now!

And, yep, looks sounds like VoiceOver respects the full text. I haven’t tested any other screen readers though.

I also noticed that MDN’s implementation removes pointer-events from p:after. Probably a good defensive tactic to prevent odd behaviors when selecting text. I added it in and selecting text does feel a little smoother, at least in Safari, Firefox, and Chrome.


Recreating MDN’s Truncated Text Effect originally published on CSS-Tricks. You should get the newsletter.

CSS-Tricks

, , , ,

How to Make a “Raise the Curtains” Effect in CSS

“Raise the curtains” is what I call an effect where the background goes from dark to light on scroll, and the content on top also goes from light to dark while in a sticky position.

Here’s an example where I used the effect on a real-life project:

Want to know how it’s done? I will take you behind the curtain and show you how to raise it, with nothing but HTML and CSS.

Let’s start with the HTML

What we’re making is sort of a simplified “raise the curtain” effect like this:

Showing the raise the curtains effect from dark blue to wheat.
The background and text both change color while scrolling over the element.

I’m keeping things simple for the sake of clarity, but we can stub this out with three elements:

<div class="curtain">   <div class="invert">     <h2>Section title</h2>   </div> </div>

First, we need a container for the curtain, which we’ll give a .curtain class. Then, inside the .curtain, we have the an .invert child element that will serve as our “sticky” box. And, finally, we have the content inside this box — a good old-fashioned <h2> element for this specific example.

Let’s set up some CSS variables

There are three values we know we’ll need upfront. Let’s make CSS variables out of them so it’s easy to write them into our styles and easily change them later if we need to.

  • --minh – The height of the container
  • --color1 – The light color
  • --color2 – The dark color
:root {   --minh: 98vh;   --color1: wheat;   --color2: midnightblue; }

Time to draw the curtain

Next, we can define our .curtain element using the following techniques:

  • A linear-gradient for the “split” background
  • min-height for the extra space at the bottom of the container

We use the ::after pseudo-element to add the extra space to the bottom. This way, our “sticky” content will actually stick to the container while scrolling past the ::after element. It’s an illusion.

.curtain {   /** create the "split" background **/   background-image: linear-gradient(to bottom, var(--color2) 50%, var(--color1) 50%); }  /** add extra space to the bottom (need this for the "sticky" effect) **/ .curtain::after {   content: "";   display: block;   min-height: var(--minh); }

Making sticky content

Next up, we need to make our content “sticky” in the sense that it sits perfectly inside the container as the background and text swap color values. In fact, we already gave the .curtain‘s child element an .invert class that we can use as the sticky container.

Stay with me for a moment — here’s how this is going to play out:

  • position: sticky and top define the stickiness and where it sticks.
  • mix-blend-mode: difference blends the color of the content inside the <h2> element into the .curtain‘s background gradient.
  • display: flex centers the content for presentation.
  • min-height defines the height of the container and allows for the extra space at the bottom.
  • color sets the color of the h2 heading.

Now to put that into CSS code!

.invert {   /** make the content sticky **/   position: sticky;   top: 20px;    /** blend the content with the contrast effect **/   mix-blend-mode: difference;    /** center the content **/   display: flex;   align-items: center;   justify-content: center;      /** set the minimum height of the section **/   min-height: var(--minh); }  h2 {   /** set the color of the text **/   color: var(--color1); }

There are many things going on here, so let’s explain each one of them.

First, we have a sticky position that is self-explanatory and flexbox to help center the content. Nothing new or particularly tricky about this.

The content’s height is set using CSS variable and the value is the same height value as the .curtain::after pseudo-element.

The mix-blend-mode: difference declaration blends our content with the background. The difference value is complicated, but you might visualize it like inverted text color against the background. Here’s a nice demo from the CSS-Tricks Almanac showing off the different mix-blend-mode values:

To make the blending work, we need to set the color of our heading. In this case, we’re assigning a light color value (wheat) to the --color1 variable.

“Raise the Curtains” Demo

Gotchas

I experienced a few problems while working out the details of the “raise the curtain” effect. If you want to add images to the “sticky” content, for example, avoid using images that don’t look good when their colors are inverted. Here’s a quick demo where I made a simple SVG and transparent PNG image, and it looks good.

Another gotcha: there’s no way to set mix-blend-mode: difference on specific child elements, like headings, while avoiding the effect on images. I discovered there are several reasons why it doesn’t work, the first of which is that position: sticky cancels the blending.

The same goes when using something like transform: skewY on the container to add a little “tilt” to things. I suspect other properties don’t play well with the blending, but I didn’t go that far to find out which ones.

Here’s the demo without scrolling that removes the troubling properties:

Curtain call!

I enjoyed building this component, and I always love it when I can accomplish something using only HTML and CSS, especially when they work smoothly on every browser.

What will make with it? Is there a different way you would approach a “raise the curtain” effect like this? Let me know in the comments!


How to Make a “Raise the Curtains” Effect in CSS originally published on CSS-Tricks. You should get the newsletter.

CSS-Tricks

, ,
[Top]

A Serene CSS Dappled Light Effect

There’s a serene warmth to the early evening sunlight peaking through rustling leaves. Artists use dappled light to create a soft, hypnotic effect.

An oil painting of a tall rectangular orange building with six windows, two by two, and a faint full-width fence in front of it. There is a similar building off in the distance. A tall birch tree is in the foreground with light green and yellow leaves, casting the dappled light effect that is being covered in this article. The shadows cover the green grass between the tree and building, and they extend to the building.
Bedford Dwellings by Ron Donoughe (2013)

We can create the same sort of dappled light effect in web design, using it on photos and illustrations to add that magic touch to what might otherwise be drab walls of content to bring them back to life.

I’ll give you one easy, quick way to add this effect… with just CSS.

Before we get into the code, it’s important to know the composition of dappled light. It’s made up of large spots — circular or elliptical — of light that are intercepted by the shadows cast by the foliage. Basically the light that slips past leaves, branches and so forth. Sometimes the shadows create crisp edges, but are more often blurred since we’re talking about light that passes though many, less defined spaces that diffuse and distort the light as it casts shadows from a further distance than, say, your own stark shadow on a nearby wall from direct sunlight.

Here’s the difference in the appearance of a white wall with and without lit by dappled light:

A side-by-side comparison of the same white brick surface, the left showing the CSS dappled light effect compared to no shadows.
The effect creates splashes of light and shadow.

I’m going to recreate the dappled light effect with both plain text and fun emojis, applying CSS shadows and blends to mimic nature. I’ll cover alternative methods too.

Setting the scene

We’ll use text — letters from the alphabet, special characters, emojis, etc. — to create the shapes of light. And by light, I mean pale, translucent colors. Again, we’re for a dappled light effect rather than something that’s sharp, crisp, or stark.

It’s best to choose characters that are elliptical or oblong in some way — the spots produced by dappled light comes in a variety of shapes. You’ll have to go with your best judgement here to get exactly what you’re going for. Me? I’m using 🍃, 🍂, because they are elliptical, oblong, and slanted — a bit of chaos and unpredictability for an otherwise serene effect.

I’m wrapping those in paragraphs that are contained in a .backdrop parent element:

<div class="backdrop">   <p class="shapes">🍃</p>   <p class="shapes">🍂</p>   <p class="shapes"></p> </div>

I’m using the parent element as the surface where the dappled light and shadows are cast, applying a background image for its texture. And not only am I giving the surface an explicit width and height, but also setting hidden overflow on it so I’m able to cast shadows that go beyond the surface without revealing them. The objects that cast the dappled light effect are aligned in the middle of the backdrop’s surface, thanks to CSS grid:

.backdrop {   background: center / cover no-repeat url('image.jpeg');   width: 400px; height: 240px;   overflow: hidden;   display: grid; } .backdrop > * {   grid-area: 1/1; }

I find that it’s OK if the shapes aren’t aligned exactly on top of one another as long as they overlap in a way that gets the dappled light effect you want. So no pressure to do exactly what I’m doing here to position things in CSS. In fact, I encourage you to try playing with the values to get different patterns of dappled light!

Styling the dappled light in CSS

These are the key properties the emojis should have — transparent color, black semi-transparent background (using the alpha channel in rgba()), blurry white text-shadow with a nice large font-size, and finally, a mix-blend-mode to smooth things out.

.shapes {   color:  transparent;   background-color: rgba(0, 0, 0, 0.3); // Use alpha transparency   text-shadow: 0 0 40px #fff; // Blurry white shadow   font: bolder 320pt/320pt monospace;   mix-blend-mode: multiply; }

mix-blend-mode sets how an element’s colors blend with that of its container element’s content. The multiply value causes the backdrop of an element to show through the element’s light colors and keeps dark colors the same, making for a nicer and more natural dappled light effect.

Refining colors and contrast

I wanted the background-image on the backdrop to be a bit brighter, so I also added filter: brightness(1.6). Another way to do this is with background-blend-mode instead, where all the different backgrounds of an element are blended and, instead of adding the emojis as separate elements, we add them as background images.

Notice that I used a different emoji in that last example as well as floralwhite for some color that’s less intense than pure white for the light. Here’s one of the emoji background images unwrapped:

<svg xmlns='http://www.w3.org/2000/svg'>    <foreignObject width='400px' height='240px'>      <div xmlns='http://www.w3.org/1999/xhtml' style=       'font: bolder 720pt/220pt monospace;        color: transparent;        text-shadow: 0 0 40px floralwhite;        background: rgba(0, 0, 0, 0.3);'     >       🌾     </div>    </foreignObject>  </svg>

If you want to use your own images for the shapes, ensure the borders are blurred to create a soft light. The CSS blur() filter can be handy for the same sort of thing. I also used CSS @supports to adjust the shadow blur value for certain browsers as a fallback.

Now let’s circle back to the first example and add a few things:

<div class="backdrop">   <p class="shapes">🍃</p>   <p class="shapes">🍂</p>   <p class="shapes"></p> </div>  <p class="content">   <img width="70px" style="float: left; margin-right: 10px;" src="image.jpeg" alt="">   Top ten tourists spots for the summer vacation <br><br><i style="font-weight: normal;">Here are the most popular places...</i> </p>

.backdrop and .shapes are basically the same styles as before. As for the .content, which also sits on top of the .backdrop, I added isolation: isolate to form a new stacking context, excluding the element from the blending as a refining touch.

Animating the light source

I also decided to add a simple CSS animation with @keyframes that get applied to the .backdrop on :hover:

.backdrop:hover > .shapes:nth-of-type(1){   animation: 2s ease-in-out infinite alternate move; } .backdrop:hover > .shapes:nth-of-type(2):hover{   animation: 4s ease-in-out infinite alternate move-1; }  @keyframes move {   from {     text-indent: -20px;   }   to {     text-indent: 20px;   } } @keyframes move-1 {   from {     text-indent: -60px;   }   to {     text-indent: 40px;   } }

Animating the text-indent property on the emojis products a super subtle bit of movement — the kind you might expect from clouds moving overhead that change the direction of the light. Just a touch of class, you know.

Wrapping up

There we have it! We drew some inspiration from nature and art to mimic one of those partly cloudy days where the sun shines through trees and bushes, projecting dappled light and shadow spots against a surface. And we did all of it with a small handful of CSS and a few emoji.

The key was how we applied color on the emoji. Using an extra blurry text-shadow in a light color sets the light, and a semi-transparent background-color defines the shadow spots. From there, all we had to do was ensure the backdrop for the light and shadows used a realistic texture with enough contrast to see the dappled light effect in action.


A Serene CSS Dappled Light Effect originally published on CSS-Tricks. You should get the newsletter and become a supporter.

CSS-Tricks

, , ,
[Top]

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.

Table of Contents

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.

CSS-Tricks

, , , , ,
[Top]

Adam Argyle’s Sick Mouse-Out CSS Hover Effect

I was killing some time browsing my CodePen feed for some eye candy and didn’t need to go past the first page before spotting a neat CSS hover effect by Adam Argyle.

I must’ve spent 10 minutes just staring at the demo in awe. There’s something about this that feels so app-like. I think it might be how contextually accurate it is in that the background color slides in from the left, then exits out through the right. It’s exactly the sort of behavior I’d expect from a mouse-in, mouse-out sort of interaction.

Whatever the case, I fired up a fresh pen and went to work recreating it. And it’s not super complex or anything, but rather a clever use of transitions and transforms paired with proper offsets. Quite elegant! I’m actually a little embarrassed how long it took me to realize how the mouse-out part works.

Here’s how I tackled it, warts and all.

“I bet that’s using a transition on a background.”

That was my first thought. Define the background-color, set the background-size and background-position, then transition the background-position. That’s how I’ve seen that “growing” background color thing done in the past. I’ve done that myself on some projects, like this:

If I could do the same thing, only from left-to-right, then all that’s left is the mouse-out, right? Nope. The problem is there’s nothing that can really make the background-position transition from left-to-right to left-to-right. I could make it do one or the other, but not both.

“Maybe it’s a transform instead.”

My next attempt was jump into transforms. The transform property provides a bunch of functions that can transition together for slightly more complex movement. For example, the background can “grow” or “shrink” by changing the element’s scale(). Or, in this case, just along the x-axis with scaleX().

But like I mentioned, there isn’t a way to isolate the element’s background to do that. Going from scaleX(0) to scaleX(1) scales the entire element, so that basically squishes the link — content and all — down to nothing, then stretches it back out to its natural size which is a totally different effect. Plus, it means starting with scaleX(0) which hides the whole dang thing by default making it unusable.

But a pseudo-element could work! It doesn’t matter if that gets squished or hidden because it isn’t part of the actual content. Gotta put the background on that instead and position it directly under the link.

a {   /* Keeps the pseudo-element contained to the element */   position: relative; }  a::before {   background: #ff9800;   content: "";   inset: 0; /* Logical equivalent to physical offsets */   position: absolute;   transform: scaleX(0); /* Hide by default */   z-index: -1; /* Ensures the link is stacked on top */ }

“Now I need ::before to change on hover.”

I knew I could make ::before scale from 0 to 1 by chaining it to the link element’s :hover state.

a:hover::before {   transform: scaleX(1) }

Nice! I was onto something.

Sprinkle a little transition fairy dust on it and things start to come to life.

a::before {   background: #ff9800;   content: "";   inset: 0;   position: absolute;   transform: scaleX(0);   transition: transform .5s ease-in-out;   z-index: -1; }

“Hmm, the transition moves in both directions.”

Again, this is where I sorta got stuck. Something in my head just wasn’t clicking for some reason. As per usual, I ran over to the CSS-Tricks Almanac to see what property might’ve slipped my mind.

Ah, yes. That would be transform-origin. That allows me to set where the transform starts, which is not totally dissimilar from setting the background-position like I tried earlier. The transform could start from the left instead of its default 50% 50% position.

a::before {   background: #ff9800;   content: "";   inset: 0;   position: absolute;   transform: scaleX(0);   transform-origin: left;   transition: transform .5s ease-in-out;   z-index: -1; }

Yeah, like this:

I was already transitioning ::before to scaleX(1) on link hover. If I reversed the transform-origin from left to right at the same time, then mayyyybe the highlight goes out the opposite of how it came in when the mouse exits?

a:hover::before {   transform: scaleX(1);   transform-origin: right; }

🤞

Whoops, backwards! Let’s swap the left and right values. 🙃

Gorgeous. Thank you, Adam, for the inspiration!


Adam Argyle’s Sick Mouse-Out CSS Hover Effect originally published on CSS-Tricks. You should get the newsletter and become a supporter.

CSS-Tricks

, , , , ,
[Top]

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.

CSS-Tricks

, , , , ,
[Top]

Icon Glassmorphism Effect in CSS

I recently came across a cool effect known as glassmorphism in a Dribble shot. My first thought was I could quickly recreate it in a few minutes if I just use some emojis for the icons without wasting time on SVG-ing them.

Animated gif. Shows a nav bar with four grey icons. On :hover/ :focus, a tinted icon slides and rotates, partly coming out from behind the grey one. In the area where they overlap, we have a glassmorphism effect, with the icon in the back seen as blurred through the semitransparent grey one in front.
The effect we’re after.

I couldn’t have been more wrong about those “few minutes” — they ended up being days of furiously and frustratingly scratching this itch!

It turns out that, while there are resources on how to CSS such an effect, they all assume the very simple case where the overlay is rectangular or at most a rectangle with border-radius. However, getting a glassmorphism effect for irregular shapes like icons, whether these icons are emojis or proper SVGs, is a lot more complicated than I expected, so I thought it would be worth sharing the process, the traps I fell into and the things I learned along the way. And also the things I still don’t understand.

Why emojis?

Short answer: because SVG takes too much time. Long answer: because I lack the artistic sense of just drawing them in an image editor, but I’m familiar with the syntax enough such that I can often compact ready-made SVGs I find online to less than 10% of their original size. So, I cannot just use them as I find them on the internet — I have to redo the code to make it super clean and compact. And this takes time. A lot of time because it’s detail work.

And if all I want is to quickly code a menu concept with icons, I resort to using emojis, applying a filter on them in order to make them match the theme and that’s it! It’s what I did for this liquid tab bar interaction demo — those icons are all emojis! The smooth valley effect makes use of the mask compositing technique.

Animated gif. Shows a white liquid navigation bar with five items, one of which is selected. The selected one has a smooth valley at the top, with a dot levitating above it. It's also black, while the non-selected ones are grey in the normal state and beige in the :hover/ :focus state. On clicking another icon, the selection smoothly changes as the valley an the levitating dot slide to always be above the currently selected item.
Liquid navigation.

Alright, so this is going to be our starting point: using emojis for the icons.

The initial idea

My first thought was to stack the two pseudos (with emoji content) of the navigation links, slightly offset and rotate the bottom one with a transform so that they only partly overlap. Then, I’d make the top one semitransparent with an opacity value smaller than 1, set backdrop-filter: blur() on it, and that should be just about enough.

Now, having read the intro, you’ve probably figured out that didn’t go as planned, but let’s see what it looks like in code and what issues there are with it.

We generate the nav bar with the following Pug:

- let data = { -   home: { ico: '🏠', hue: 200 },  -   notes: { ico: '🗒️', hue: 260 },  -   activity: { ico: '🔔', hue: 320 },  -   discovery: { ico: '🧭', hue: 30 } - }; - let e = Object.entries(data); - let n = e.length;  nav   - for(let i = 0; i > n; i++)     a(href='#' data-ico=e[i][1].ico style=`--hue: $ {e[i][1].hue}deg`) #{e[i][0]}

Which compiles to the HTML below:

<nav>   <a href='#' data-ico='🏠' style='--hue: 200deg'>home</a>   <a href='#' data-ico='🗒️' style='--hue: 260deg'>notes</a>   <a href='#' data-ico='🔔' style='--hue: 320deg'>activity</a>   <a href='#' data-ico='🧭' style='--hue: 30deg'>iscovery</a> </nav>

We start with layout, making our elements grid items. We place the nav in the middle, give links explicit widths, put both pseudos for each link in the top cell (which pushes the link text content to the bottom cell) and middle-align the link text and pseudos.

body, nav, a { display: grid; }  body {   margin: 0;   height: 100vh; }  nav {   grid-auto-flow: column;   place-self: center;   padding: .75em 0 .375em; }  a {   width: 5em;   text-align: center;      &::before, &::after {     grid-area: 1/ 1;     content: attr(data-ico);   } }
Screenshot. Shows the four menu items lined up in a row in the middle of the page, each item occupying a column, all columns having the same width; with emojis above the link text, both middle-aligned horizontally.
Firefox screenshot of the result after we got layout basics sorted.

Note that the look of the emojis is going to be different depending on the browser you’re using view the demos.

We pick a legible font, bump up its size, make the icons even bigger, set backgrounds, and a nicer color for each of the links (based on the --hue custom property in the style attribute of each):

body {   /* same as before */   background: #333; }  nav {   /* same as before */   background: #fff;   font: clamp(.625em, 5vw, 1.25em)/ 1.25 ubuntu, sans-serif; }  a {   /* same as before */   color: hsl(var(--hue), 100%, 50%);   text-decoration: none;      &::before, &::after {     /* same as before */     font-size: 2.5em;   } }
Screenshot. Shows the same layout as before, only with a prettier and bigger font and even bigger icons, backgrounds and each menu item having a different color value based on its --hue.
Chrome screenshot of the result (live demo) after prettifying things a bit.

Here’s where things start to get interesting because we start differentiating between the two emoji layers created with the link pseudos. We slightly move and rotate the ::before pseudo, make it monochrome with a sepia(1) filter, get it to the right hue, and bump up its contrast() — an oldie but goldie technique from Lea Verou. We also apply a filter: grayscale(1) on the ::after pseudo and make it semitransparent because, otherwise, we wouldn’t be able to see the other pseudo through it.

a {   /* same as before */      &::before {     transform:        translate(.375em, -.25em)        rotate(22.5deg);     filter:        sepia(1)        hue-rotate(calc(var(--hue) - 50deg))        saturate(3);   } 	   &::after {     opacity: .5;     filter: grayscale(1);   } }
Screenshot. Same nav bar as before, only now the top icon layer is grey and semitransparent, while the bottom one is slightly offset and rotated, mono in the specified --hue.
Chrome screenshot of the result (live demo) after differentiating between the two icon layers.

Hitting a wall

So far, so good… so what? The next step, which I foolishly thought would be the last when I got the idea to code this, involves setting a backdrop-filter: blur(5px) on the top (::after) layer.

Note that Firefox still needs the gfx.webrender.all and layout.css.backdrop-filter.enabled flags set to true in about:config in order for the backdrop-filter property to work.

Animated gif. Shows how to find the flags mentioned above (gfx.webrender.all and layout.css.backdrop-filter.enabled) in order to ensure they are set to true. Go to about:config, start typing their name in the search box and double click their value to change it if it's not set to true already.
The flags that are still required in Firefox for backdrop-filter to work.

Sadly, the result looks nothing like what I expected. We get a sort of overlay the size of the entire top icon bounding box, but the bottom icon isn’t really blurred.

Screenshot collage. Shows the not really blurred, but awkward result with an overlay the size of the top emoji box after applying the backdrop-filter property. This happens both in Chrome (top) and in Firefox (bottom).
Chrome (top) and Firefox (bottom) screenshots of the result (live demo) after applying backdrop-filter.

However, I’m pretty sure I’ve played with backdrop-filter: blur() before and it worked, so what the hairy heck is going on here?

Screenshot. Shows a working glassmorphism effect, created via a control panel where we draw some sliders to get the value for each filter function.
Working glassmorphism effect (live demo) in an older demo I coded.

Getting to the root of the problem

Well, when you have no idea whatsoever why something doesn’t work, all you can do is take another working example, start adapting it to try to get the result you want… and see where it breaks!

So let’s see a simplified version of my older working demo. The HTML is just an article in a section. In the CSS, we first set some dimensions, then we set an image background on the section, and a semitransparent one on the article. Finally, we set the backdrop-filter property on the article.

section { background: url(cake.jpg) 50%/ cover; }  article {   margin: 25vmin;   height: 40vh;   background: hsla(0, 0%, 97%, .25);   backdrop-filter: blur(5px); }
Screenshot. Shows a working glassmorphism effect, where we have a semitransparent box on top of its parent one, having an image background.
Working glassmorphism effect (live demo) in a simplified test.

This works, but we don’t want our two layers nested in one another; we want them to be siblings. So, let’s make both layers article siblings, make them partly overlap and see if our glassmorphism effect still works.

<article class='base'></article> <article class='grey'></article>
article { width: 66%; height: 40vh; }  .base { background: url(cake.jpg) 50%/ cover; }  .grey {   margin: -50% 0 0 33%;   background: hsla(0, 0%, 97%, .25);   backdrop-filter: blur(5px); }
Screenshot collage. Shows the case where we have a semitransparent box on top of its sibling having an image background. The top panel screenshot was taken in Chrome, where the glassmorphism effect works as expected. The bottom panel screenshot was taken in Firefox, where things are mostly fine, but the blur handling around the edges is really weird.
Chrome (top) and Firefox (bottom) screenshots of the result (live demo) when the two layers are siblings.

Everything still seems fine in Chrome and, for the most part, Firefox too. It’s just that the way blur() is handled around the edges in Firefox looks awkward and not what we want. And, based on the few images in the spec, I believe the Firefox result is also incorrect?

I suppose one fix for the Firefox problem in the case where our two layers sit on a solid background (white in this particular case) is to give the bottom layer (.base) a box-shadow with no offsets, no blur, and a spread radius that’s twice the blur radius we use for the backdrop-filter applied on the top layer (.grey). Sure enough, this fix seems to work in our particular case.

Things get a lot hairier if our two layers sit on an element with an image background that’s not fixed (in which case, we could use a layered backgrounds approach to solve the Firefox issue), but that’s not the case here, so we won’t get into it.

Still, let’s move on to the next step. We don’t want our two layers to be two square boxes, we want then to be emojis, which means we cannot ensure semitransparency for the top one using a hsla() background — we need to use opacity.

.grey {   /* same as before */   opacity: .25;   background: hsl(0, 0%, 97%); }
Screenshot. Shows the case where we have a subunitary opacity on the top layer in order to make it semitransparent, instead of a subunitary alpha value for the semitransparent background.
The result (live demo) when the top layer is made semitransparent using opacity instead of a hsla() background.

It looks like we found the problem! For some reason, making the top layer semitransparent using opacity breaks the backdrop-filter effect in both Chrome and Firefox. Is that a bug? Is that what’s supposed to happen?

Bug or not?

MDN says the following in the very first paragraph on the backdrop-filter page:

Because it applies to everything behind the element, to see the effect you must make the element or its background at least partially transparent.

Unless I don’t understand the above sentence, this appears to suggest that opacity shouldn’t break the effect, even though it does in both Chrome and Firefox.

What about the spec? Well, the spec is a huge wall of text without many illustrations or interactive demos, written in a language that makes reading it about as appealing as sniffing a skunk’s scent glands. It contains this part, which I have a feeling might be relevant, but I’m unsure that I understand what it’s trying to say — that the opacity set on the top element that we also have the backdrop-filter on also gets applied on the sibling underneath it? If that’s the intended result, it surely isn’t happening in practice.

The effect of the backdrop-filter will not be visible unless some portion of element B is semi-transparent. Also note that any opacity applied to element B will be applied to the filtered backdrop image as well.

Trying random things

Whatever the spec may be saying, the fact remains: making the top layer semitransparent with the opacity property breaks the glassmorphism effect in both Chrome and Firefox. Is there any other way to make an emoji semitransparent? Well, we could try filter: opacity()!

At this point, I should probably be reporting whether this alternative works or not, but the reality is… I have no idea! I spent a couple of days around this step and got to check the test countless times in the meanwhile — sometimes it works, sometimes it doesn’t in the exact same browsers, wit different results depending on the time of day. I also asked on Twitter and got mixed answers. Just one of those moments when you can’t help but wonder whether some Halloween ghost isn’t haunting, scaring and scarring your code. For eternity!

It looks like all hope is gone, but let’s try just one more thing: replacing the rectangles with text, the top one being semitransparent with color: hsla(). We may be unable to get the cool emoji glassmorphism effect we were after, but maybe we can get such a result for plain text.

So we add text content to our article elements, drop their explicit sizing, bump up their font-size, adjust the margin that gives us partial overlap and, most importantly, replace the background declarations in the last working version with color ones. For accessibility reasons, we also set aria-hidden='true' on the bottom one.

<article class='base' aria-hidden='true'>Lion 🧡</article> <article class='grey'>Lion 🖤</article>
article { font: 900 21vw/ 1 cursive; }  .base { color: #ff7a18; }  .grey {   margin: -.75em 0 0 .5em;   color: hsla(0, 0%, 50%, .25);   backdrop-filter: blur(5px); }
Screenshot collage. Shows the case where we have a semitransparent text layer on top of its identical solid orange text sibling. The top panel screenshot was taken in Chrome, where we get proper blurring, but it's underneath the entire bounding box of the semitransparent top text, not limited to just the actual text. The bottom panel screenshot was taken in Firefox, where things are even worse, with the blur handling around the edges being really weird.
Chrome (top) and Firefox (bottom) screenshots of the result (live demo) when we have two text layers.

There are couple of things to note here.

First, setting the color property to a value with a subunitary alpha also makes emojis semitransparent, not just plain text, both in Chrome and in Firefox! This is something I never knew before and I find absolutely mindblowing, given the other channels don’t influence emojis in any way.

Second, both Chrome and Firefox are blurring the entire area of the orange text and emoji that’s found underneath the bounding box of the top semitransparent grey layer, instead of just blurring what’s underneath the actual text. In Firefox, things look even worse due to that awkward sharp edge effect.

Even though the box blur is not what we want, I can’t help but think it does make sense since the spec does say the following:

[…] to create a “transparent” element that allows the full filtered backdrop image to be seen, you can use “background-color: transparent;”.

So let’s make a test to check what happens when the top layer is another non-rectangular shape that’s not text, but instead obtained with a background gradient, a clip-path or a mask!

Screenshot collage. Shows the case where we have semitransparent non-rectangular shaped layers (obtained with three various methods: gradient background, clip-path and mask) on top of a rectangular siblings. The top panel screenshot was taken in Chrome, where things seem to work fine in the clip-path and mask case, but not in the gradient background case. In this case, everything that's underneath the bounding box of the top element gets blurred, not just what's underneath the visible part. The bottom panel screenshot was taken in Firefox, where, regardless of the way we got the shape, everything underneath its bounding box gets blurred, not just what's underneath the actual shape. Furthermore, in all three cases we have the old awkward sharp edge issue we've had in Firefox before
Chrome (top) and Firefox (bottom) screenshots of the result (live demo) when the top layer is a non-rectangular shape.

In both Chrome and Firefox, the area underneath the entire box of the top layer gets blurred when the shape is obtained with background: gradient() which, as mentioned in the text case before, makes sense per the spec. However, Chrome respects the clip-path and mask shapes, while Firefox doesn’t. And, in this case, I really don’t know which is correct, though the Chrome result does make more sense to me.

Moving towards a Chrome solution

This result and a Twitter suggestion I got when I asked how to make the blur respect the text edges and not those of its bounding box led me to the next step for Chrome: applying a mask clipped to the text on the top layer (.grey). This solution doesn’t work in Firefox for two reasons: one, text is sadly a non-standard mask-clip value that only works in WebKit browsers and, two, as shown by the test above, masking doesn’t restrict the blur area to the shape created by the mask in Firefox anyway.

/* same as before */  .grey {   /* same as before */   -webkit-mask: linear-gradient(red, red) text; /* only works in WebKit browsers */ }
Chrome screenshot. Shows two text and emoji layers partly overlapping. The top one is semitransparent, so through it, we can see the layer underneath blurred (by applying a backdrop-filter on the top one).
Chrome screenshot of the result (live demo) when the top layer has a mask restricted to the text area.

Alright, this actually looks like what we want, so we can say we’re heading in the right direction! However, here we’ve used an orange heart emoji for the bottom layer and a black heart emoji for the top semitransparent layer. Other generic emojis don’t have black and white versions, so my next idea was to initially make the two layers identical, then make the top one semitransparent and use filter: grayscale(1) on it.

article {    color: hsla(25, 100%, 55%, var(--a, 1));   font: 900 21vw/ 1.25 cursive; }  .grey {   --a: .25;   margin: -1em 0 0 .5em;   filter: grayscale(1);   backdrop-filter: blur(5px);   -webkit-mask: linear-gradient(red, red) text; }
Chrome screenshot. Shows two text and emoji layers partly overlapping. The top one is semitransparent, so through it, we can see the layer underneath blurred (by applying a backdrop-filter on the top one). The problem is that applying the grayscale filter on the top semitransparent layer not only affects this layer, but also the blurred area of the layer underneath.
Chrome screenshot of the result (live demo) when the top layer gets a grayscale(1) filter.

Well, that certainly had the effect we wanted on the top layer. Unfortunately, for some weird reason, it seems to have also affected the blurred area of the layer underneath. This moment is where to briefly consider throwing the laptop out the window… before getting the idea of adding yet another layer.

It would go like this: we have the base layer, just like we have so far, slightly offset from the other two above it. The middle layer is a “ghost” (transparent) one that has the backdrop-filter applied. And finally, the top one is semitransparent and gets the grayscale(1) filter.

body { display: grid; }  article {   grid-area: 1/ 1;   place-self: center;   padding: .25em;   color: hsla(25, 100%, 55%, var(--a, 1));   font: 900 21vw/ 1.25 pacifico, z003, segoe script, comic sans ms, cursive; }  .base { margin: -.5em 0 0 -.5em; }  .midl {   --a: 0;   backdrop-filter: blur(5px);   -webkit-mask: linear-gradient(red, red) text; }  .grey { filter: grayscale(1) opacity(.25) }
Chrome screenshot. Shows two text and emoji layers partly overlapping. The top one is semitransparent grey, so through it, we can see the layer underneath blurred (by applying a backdrop-filter on a middle, completely transparent one).
Chrome screenshot of the result (live demo) with three layers.

Now we’re getting somewhere! There’s just one more thing left to do: make the base layer monochrome!

/* same as before */  .base {   margin: -.5em 0 0 -.5em;   filter: sepia(1) hue-rotate(165deg) contrast(1.5); }
Chrome screenshot. Shows two text and emoji layers partly overlapping. The bottom one is mono (bluish in this case) and blurred at the intersection with the semitransparent grey one on top.
Chrome screenshot of the result (live demo) we were after.

Alright, this is the effect we want!

Getting to a Firefox solution

While coding the Chrome solution, I couldn’t help but think we may be able to pull off the same result in Firefox since Firefox is the only browser that supports the element() function. This function allows us to take an element and use it as a background for another element.

The idea is that the .base and .grey layers will have the same styles as in the Chrome version, while the middle layer will have a background that’s (via the element() function) a blurred version of our layers.

To make things easier, we start with just this blurred version and the middle layer.

<article id='blur' aria-hidden='true'>Lion 🦁</article> <article class='midl'>Lion 🦁</article>

We absolutely position the blurred version (still keeping it in sight for now), make it monochrome and blur it and then use it as a background for .midl.

#blur {   position: absolute;   top: 2em; right: 0;   margin: -.5em 0 0 -.5em;   filter: sepia(1) hue-rotate(165deg) contrast(1.5) blur(5px); }  .midl {   --a: .5;   background: -moz-element(#blur); }

We’ve also made the text on the .midl element semitransparent so we can see the background through it. We’ll make it fully transparent eventually, but for now, we still want to see its position relative to the background.

Firefox screenshot. Shows a blurred mono (bluish in this case) text and emoji element below everything else. 'Everything else' in this case is another text and emoji element that uses a semitransparent color so we can partly see through to the background which is set to the blurred element via the element() function.
Firefox screenshot of the result (live demo) when using the blurred element #blur as a background.

We can notice a one issue right away: while margin works to offset the actual #blur element, it does nothing for shifting its position as a background. In order to get such an effect, we need to use the transform property. This can also help us if we want a rotation or any other transform — as it can be seen below where we’ve replaced the margin with transform: rotate(-9deg).

Firefox screenshot. Shows a slightly rotated blurred mono (bluish in this case) text and emoji element below everything else. 'Everything else' in this case is another text and emoji element that uses a semitransparent color so we can partly see through to the background which is set to the slightly rotated blurred element via the element() function.
Firefox screenshot of the result (live demo) when using transform: rotate() instead of margin on the #blur element.

Alright, but we’re still sticking to just a translation for now:

#blur {   /* same as before */   transform: translate(-.25em, -.25em); /* replaced margin */ }
Firefox screenshot. Shows a slightly offset blurred mono (bluish in this case) text and emoji element below everything else. 'Everything else' in this case is another text and emoji element that uses a semitransparent color so we can partly see through to the background which is set to the slightly offset blurred element via the element() function. This slight offset means the actual text doesn't perfectly overlap with the background one anymore.
Firefox screenshot of the result (live demo) when using transform: translate() instead of margin on the #blur element.

One thing to note here is that a bit of the blurred background gets cut off as it goes outside the limits of the middle layer’s padding-box. That doesn’t matter at this step anyway since our next move is to clip the background to the text area, but it’s good to just have that space since the .base layer is going to get translated just as far.

Firefox screenshot. Shows a slightly offset blurred mono (bluish in this case) text and emoji element below everything else. 'Everything else' in this case is another text and emoji element that uses a semitransparent color so we can partly see through to the background which is set to the slightly offset blurred element via the element() function. This slight offset means the actual text doesn't perfectly overlap with the background one anymore. It also means that the translated background text may not fully be within the limits of the padding-box anymore, as highlighted in this screenshot, which also shows the element boxes overlays.
Firefox screenshot highlighting how the translated #blur background exceeds the limits of the padding-box on the .midl element.

So, we’re going to bump up the padding by a little bit, even if, at this point, it makes absolutely no difference visually as we’re also setting background-clip: text on our .midl element.

article {   /* same as before */   padding: .5em; }  #blur {   position: absolute;   bottom: 100vh;   transform: translate(-.25em, -.25em);   filter: sepia(1) hue-rotate(165deg) contrast(1.5) blur(5px); }  .midl {   --a: .1;   background: -moz-element(#blur);   background-clip: text; }

We’ve also moved the #blur element out of sight and further reduced the alpha of the .midl element’s color, as we want a better view at the background through the text. We’re not making it fully transparent, but still keeping it visible for now just so we know what area it covers.

Firefox screenshot. Shows a text and emoji element that uses a semitransparent color so we can partly see through to the background which is set to a blurred element (now positioned out of sight) via the element() function. This slight offset means the actual text doesn't perfectly overlap with the background one anymore. We have also clipped the background of this element to the text, so that none of the background outside it is visible. Even so, there's enough padding room so that the blurred background is contained within the padding-box.
Firefox screenshot of the result (live demo) after clipping the .midl element’s background to text.

The next step is to add the .base element with pretty much the same styles as it had in the Chrome case, only replacing the margin with a transform.

<article id='blur' aria-hidden='true'>Lion 🦁</article> <article class='base' aria-hidden='true'>Lion 🦁</article> <article class='midl'>Lion 🦁</article>
#blur {   position: absolute;   bottom: 100vh;   transform: translate(-.25em, -.25em);   filter: sepia(1) hue-rotate(165deg) contrast(1.5) blur(5px); }  .base {   transform: translate(-.25em, -.25em);   filter: sepia(1) hue-rotate(165deg) contrast(1.5); }

Since a part of these styles are common, we can also add the .base class on our blurred element #blur in order to avoid duplication and reduce the amount of code we write.

<article id='blur' class='base' aria-hidden='true'>Lion 🦁</article> <article class='base' aria-hidden='true'>Lion 🦁</article> <article class='midl'>Lion 🦁</article>
#blur {   --r: 5px;   position: absolute;   bottom: 100vh; }  .base {   transform: translate(-.25em, -.25em);   filter: sepia(1) hue-rotate(165deg) contrast(1.5) blur(var(--r, 0)); }
Firefox screenshot. Shows two text and emoji layers slightly offset from one another. The .base one, first in the DOM order, is made mono with a filter and slightly offset to the top left with a transform. The .midl one, following it in DOM order, has semitransparent text so that we can see through to the text clipped background, which uses as a background image the blurred version of the mono, slightly offset .base layer. In spite of DOM order, the .base layer still shows up on top.
Firefox screenshot of the result (live demo) after adding the .base layer.

We have a different problem here. Since the .base layer has a transform, it’s now on top of the .midl layer in spite of DOM order. The simplest fix? Add z-index: 2 on the .midl element!

Firefox screenshot. Shows two text and emoji layers slightly offset from one another. The .base one, first in the DOM order, is made mono with a filter and slightly offset to the top left with a transform. The .midl one, following it in DOM order, has semitransparent text so that we can see through to the text clipped background, which uses as a background image the blurred version of the mono, slightly offset .base layer. Having explicitly set a z-index on the .midl layer, it now shows up on top of the .base one.
Firefox screenshot of the result (live demo) after fixing the layer order such that .base is underneath .midl.

We still have another, slightly more subtle problem: the .base element is still visible underneath the semitransparent parts of the blurred background we’ve set on the .midl element. We don’t want to see the sharp edges of the .base layer text underneath, but we are because blurring causes pixels close to the edge to become semitransparent.

Screenshot. Shows two lines of blue text with a red outline to highlight the boundaries of the actual text. The text on the second line is blurred and it can be seen how this causes us to have semitransparent blue pixels on both sides of the red outline - both outside and inside.
The blur effect around the edges.

Depending on what kind of background we have on the parent of our text layers, this is a problem that can be solved with a little or a lot of effort.

If we only have a solid background, the problem gets solved by setting the background-color on our .midl element to that same value. Fortunately, this happens to be our case, so we won’t go into discussing the other scenario. Maybe in another article.

.midl {   /* same as before */   background: -moz-element(#blur) #fff;   background-clip: text; }
Firefox screenshot. Shows two text and emoji layers slightly offset from one another. The .base one, first in the DOM order, is made mono with a filter and slightly offset to the top left with a transform. The .midl one, following it in DOM order, has semitransparent text so that we can see through to the text clipped background, which uses as a background image the blurred version of the mono, slightly offset .base layer. Having explicitly set a z-index on the .midl layer and having set a fully opaque background-color on it, the .base layer now lies underneath it and it isn't visible through any semitransparent parts in the text area because there aren't any more such parts.
Firefox screenshot of the result (live demo) after ensuring the .base layer isn’t visible through the background of the .midl one.

We’re getting close to a nice result in Firefox! All that’s left to do is add the top .grey layer with the exact same styles as in the Chrome version!

.grey { filter: grayscale(1) opacity(.25); }

Sadly, doing this doesn’t produce the result we want, which is something that’s really obvious if we also make the middle layer text fully transparent (by zeroing its alpha --a: 0) so that we only see its background (which uses the blurred element #blur on top of solid white) clipped to the text area:

Firefox screenshot. Shows two text and emoji layers slightly offset from one another. The .base one, first in the DOM order, is made mono with a filter and slightly offset to the top left with a transform. The .midl one, following it in DOM order, has transparent text so that we can see through to the text clipped background, which uses as a background image the blurred version of the mono, slightly offset .base layer. Since the background-color of this layer coincides to that of their parent, it is hard to see. We also have a third .grey layer, the last in DOM order. This should be right on top of the .midl one, but, due to having set a z-index on the .midl layer, the .grey layer is underneath it and not visible, in spite of the DOM order.
Firefox screenshot of the result (live demo) after adding the top .grey layer.

The problem is we cannot see the .grey layer! Due to setting z-index: 2 on it, the middle layer .midl is now above what should be the top layer (the .grey one), in spite of the DOM order. The fix? Set z-index: 3 on the .grey layer!

.grey {   z-index: 3;   filter: grayscale(1) opacity(.25); }

I’m not really fond of giving out z-index layer after layer, but hey, it’s low effort and it works! We now have a nice Firefox solution:

Firefox screenshot. Shows two text and emoji layers partly overlapping. The bottom one is mono (bluish in this case) and blurred at the intersection with the semitransparent grey one on top.
Firefox screenshot of the result (live demo) we were after.

Combining our solutions into a cross-browser one

We start with the Firefox code because there’s just more of it:

<article id='blur' class='base' aria-hidden='true'>Lion 🦁</article> <article class='base' aria-hidden='true'>Lion 🦁</article> <article class='midl' aria-hidden='true'>Lion 🦁</article> <article class='grey'>Lion 🦁</article>
body { display: grid; }  article {   grid-area: 1/ 1;   place-self: center;   padding: .5em;   color: hsla(25, 100%, 55%, var(--a, 1));   font: 900 21vw/ 1.25 cursive; }  #blur {   --r: 5px;   position: absolute;   bottom: 100vh; }  .base {   transform: translate(-.25em, -.25em);   filter: sepia(1) hue-rotate(165deg) contrast(1.5) blur(var(--r, 0)); }  .midl {   --a: 0;   z-index: 2;   background: -moz-element(#blur) #fff;   background-clip: text; }  .grey {   z-index: 3;   filter: grayscale(1) opacity(.25); }

The extra z-index declarations don’t impact the result in Chrome and neither does the out-of-sight #blur element. The only things that this is missing in order for this to work in Chrome are the backdrop-filter and the mask declarations on the .midl element:

backdrop-filter: blur(5px); -webkit-mask: linear-gradient(red, red) text;

Since we don’t want the backdrop-filter to get applied in Firefox, nor do we want the background to get applied in Chrome, we use @supports:

$ r: 5px;  /* same as before */  #blur {   /* same as before */   --r: #{$ r}; }  .midl {   --a: 0;   z-index: 2;   /* need to reset inside @supports so it doesn't get applied in Firefox */   backdrop-filter: blur($ r);   /* invalid value in Firefox, not applied anyway, no need to reset */   -webkit-mask: linear-gradient(red, red) text;      @supports (background: -moz-element(#blur)) { /* for Firefox */     background: -moz-element(#blur) #fff;     background-clip: text;     backdrop-filter: none;   } }

This gives us a cross-browser solution!

Chrome (top) and Firefox (bottom) screenshot collage of the text and emoji glassmorphism effect for comparison. The blurred backdrop seems thicker in Chrome and the emojis are obviously different, but the result is otherwise pretty similar.
Chrome (top) and Firefox (bottom) screenshots of the result (live demo) we were after.

While the result isn’t the same in the two browsers, it’s still pretty similar and good enough for me.

What about one-elementing our solution?

Sadly, that’s impossible.

First off, the Firefox solution requires us to have at least two elements since we use one (referenced by its id) as a background for another.

Second, while the first thought with the remaining three layers (which are the only ones we need for the Chrome solution anyway) is that one of them could be the actual element and the other two its pseudos, it’s not so simple in this particular case.

For the Chrome solution, each of the layers has at least one property that also irreversibly impacts any children and any pseudos it may have. For the .base and .grey layers, that’s the filter property. For the middle layer, that’s the mask property.

So while it’s not pretty to have all those elements, it looks like we don’t have a better solution if we want the glassmorphism effect to work on emojis too.

If we only want the glassmorphism effect on plain text — no emojis in the picture — this can be achieved with just two elements, out of which only one is needed for the Chrome solution. The other one is the #blur element, which we only need in Firefox.

<article id='blur'>Blood</article> <article class='text' aria-hidden='true' data-text='Blood'></article>

We use the two pseudos of the .text element to create the base layer (with the ::before) and a combination of the other two layers (with the ::after). What helps us here is that, with emojis out of the picture, we don’t need filter: grayscale(1), but instead we can control the saturation component of the color value.

These two pseudos are stacked one on top of the other, with the bottom one (::before) offset by the same amount and having the same color as the #blur element. This color value depends on a flag, --f, that helps us control both the saturation and the alpha. For both the #blur element and the ::before pseudo (--f: 1), the saturation is 100% and the alpha is 1. For the ::after pseudo (--f: 0), the saturation is 0% and the alpha is .25.

$ r: 5px;  %text { // used by #blur and both .text pseudos   --f: 1;   grid-area: 1/ 1; // stack pseudos, ignored for absolutely positioned #base   padding: .5em;   color: hsla(345, calc(var(--f)*100%), 55%, calc(.25 + .75*var(--f)));   content: attr(data-text); }  article { font: 900 21vw/ 1.25 cursive }  #blur {   position: absolute;   bottom: 100vh;   filter: blur($ r); }  #blur, .text::before {   transform: translate(-.125em, -.125em);   @extend %text; }  .text {   display: grid; 	   &::after {     --f: 0;     @extend %text;     z-index: 2;     backdrop-filter: blur($ r);     -webkit-mask: linear-gradient(red, red) text;      @supports (background: -moz-element(#blur)) {       background: -moz-element(#blur) #fff;       background-clip: text;       backdrop-filter: none;     }   } }

Applying the cross-browser solution to our use case

The good news here is our particular use case where we only have the glassmorphism effect on the link icon (not on the entire link including the text) actually simplifies things a tiny little bit.

We use the following Pug to generate the structure:

- let data = { -   home: { ico: '🏠', hue: 200 },  -   notes: { ico: '🗒️', hue: 260 },  -   activity: { ico: '🔔', hue: 320 },  -   discovery: { ico: '🧭', hue: 30 } - }; - let e = Object.entries(data); - let n = e.length;  nav   - for(let i = 0; i < n; i++)     - let ico = e[i][1].ico;     a.item(href='#' style=`--hue: $ {e[i][1].hue}deg`)       span.icon.tint(id=`blur$ {i}` aria-hidden='true') #{ico}       span.icon.tint(aria-hidden='true') #{ico}       span.icon.midl(aria-hidden='true' style=`background-image: -moz-element(#blur$ {i})`) #{ico}       span.icon.grey(aria-hidden='true') #{ico}       | #{e[i][0]}

Which produces an HTML structure like the one below:

<nav>   <a class='item' href='#' style='--hue: 200deg'>     <span class='icon tint' id='blur0' aria-hidden='true'>🏠</span>     <span class='icon tint' aria-hidden='true'>🏠</span>     <span class='icon midl' aria-hidden='true' style='background-image: -moz-element(#blur0)'>🏠</span>     <span class='icon grey' aria-hidden='true'>🏠</span>     home   </a>   <!-- the other nav items --> </nav>

We could probably replace a part of those spans with pseudos, but I feel it’s more consistent and easier like this, so a span sandwich it is!

One very important thing to notice is that we have a different blurred icon layer for each of the items (because each and every item has its own icon), so we set the background of the .midl element to it in the style attribute. Doing things this way allows us to avoid making any changes to the CSS file if we add or remove entries from the data object (thus changing the number of menu items).

We have almost the same layout and prettified styles we had when we first CSS-ed the nav bar. The only difference is that now we don’t have pseudos in the top cell of an item’s grid; we have the spans:

span {   grid-area: 1/ 1; /* stack all emojis on top of one another */   font-size: 4em; /* bump up emoji size */ }

For the emoji icon layers themselves, we also don’t need to make many changes from the cross-browser version we got a bit earlier, though there are a few lttle ones.

First off, we use the transform and filter chains we picked initially when we were using the link pseudos instead of spans. We also don’t need the color: hsla() declaration on the span layers any more since, given that we only have emojis here, it’s only the alpha channel that matters. The default, which is preserved for the .base and .grey layers, is 1. So, instead of setting a color value where only the alpha, --a, channel matters and we change that to 0 on the .midl layer, we directly set color: transparent there. We also only need to set the background-color on the .midl element in the Firefox case as we’ve already set the background-image in the style attribute. This leads to the following adaptation of the solution:

.base { /* mono emoji version */   transform: translate(.375em, -.25em) rotate(22.5deg);   filter: sepia(1) hue-rotate(var(--hue)) saturate(3) blur(var(--r, 0)); }  .midl { /* middle, transparent emoji version */   color: transparent; /* so it's not visible */   backdrop-filter: blur(5px);   -webkit-mask: linear-gradient(red 0 0) text;      @supports (background: -moz-element(#b)) {     background-color: #fff;     background-clip: text;     backdrop-filter: none;   } }

And that’s it — we have a nice icon glassmorphism effect for this nav bar!

Chrome (top) and Firefox (bottom) screenshot collage of the emoji glassmorphism effect for comparison. The emojis are obviously different, but the result is otherwise pretty similar.
Chrome (top) and Firefox (bottom) screenshots of the desired emoji glassmorphism effect (live demo).

There’s just one more thing to take care of — we don’t want this effect at all times; only on :hover or :focus states. So, we’re going to use a flag, --hl, which is 0 in the normal state, and 1 in the :hover or :focus state in order to control the opacity and transform values of the .base spans. This is a technique I’ve detailed in an earlier article.

$ t: .3s;  a {   /* same as before */   --hl: 0;   color: hsl(var(--hue), calc(var(--hl)*100%), 65%);   transition: color $ t;      &:hover, &:focus { --hl: 1; } }  .base {   transform:      translate(calc(var(--hl)*.375em), calc(var(--hl)*-.25em))      rotate(calc(var(--hl)*22.5deg));   opacity: var(--hl);   transition: transform $ t, opacity $ t; }

The result can be seen in the interactive demo below when the icons are hovered or focused.

What about using SVG icons?

I naturally asked myself this question after all it took to get the CSS emoji version working. Wouldn’t the plain SVG way make more sense than a span sandwich, and wouldn’t it be simpler? Well, while it does make more sense, especially since we don’t have emojis for everything, it’s sadly not less code and it’s not any simpler either.

But we’ll get into details about that in another article!


The post Icon Glassmorphism Effect in CSS appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, ,
[Top]

Proxying Third-Party JavaScript as First-Party JavaScript (and the Potential Effect on Analytics)

First, check out how incredibly easy it is to write a Cloudflare Worker to proxy another URL:

addEventListener("fetch", (event) => {   event.respondWith(      fetch("https://css-tricks.com")   ); });

It doesn’t have any error handling or anything, but hey, it works:

Now imagine how some websites give you a URL to JavaScript in order to do stuff. CodePen does this for our Embedded Pens feature.

That URL is:

https://cpwebassets.codepen.io/assets/embed/ei.js

I can proxy that URL just as easily:

Doing nothing special, it even serves up the right content-type header and everything:

Cloudflare Workers gives you a URL for them, which is decently nice, but you can also very easily “Add a Route” to a worker on your own website. So, here I’ll make a URL on CSS-Tricks to serve up that Worker. Lookie lookie, it does just what it says it’s going to do:

CSS-Tricks.com serving up a JavaScript file that is actually just proxied from CodePen. I’m probably not going to leave this URL live, it’s just a demo.

So now, I could do….

<script src="/super-real-url/codepen-embeds.js"></script>

Right from css-tricks.com and it’ll load that JavaScript. It will look to the browser like first-party JavaScript, but it will really be proxied third-party JavaScript.

Why? Well nobody is going to block your first-party JavaScript. If you were a bit slimy, you could run all your scripts for ads this way to avoid ad blockers. I have mixed feelings there. I feel like if you wanna block ads you should be able to block ads without having to track down specific scripts on specific sites to do that. On the other hand, proxying some third-party resources sometimes seems kinda fine? Like if it’s your own site and you’re just trying to get around some CORS issue… that would be fine.

More in the middle is something like analytics. I recently blogged “Comparing Google Analytics and Plausible Numbers” where I discussed Plausible, a third-party analytics service that “is built for privacy-conscious site owners.” So, ya know, theoretically trustable and not third-party JavaScript that is terribly worrisome. But still, it doesn’t do anything to really help site visitors and is in the broad category of analytics, so I could see it making its way onto blocklists, thus giving you less accurate information over time as more and more people block it.

The default usage for Plausible is third-party JavaScript

But as we talked about, very few people are going to block first-party JavaScript, so proxying would theoretically deliver more accurate information. In fact, they have docs for proxying. It’s slightly more involved, and it’s over my head as to exactly why, but hey, it works.

I’ve done this proxying as a test. So now I have data from just using the third-party JavaScript directly (from the last article):

Metric Plausible (No Proxy) Google Analytics
Unique Visitors 973k 841k
Pageviews 1.4m 1.5m
Bounce Rate 82% 82%
Visit Duration 1m 31s 1m 24s
Data from one week of non-proxied third-party JavaScript integration

And can compare it to an identical-in-length time period using the proxy:

Metric Plausible (Proxy) Google Analytics
Unique Visitors 1.32m 895k
Pageviews 2.03m 1.7m
Bounce Rate 81% 82%
Visit Duration 1m 35s 1m 24s
Data from one week of proxied third-party JavaScript integration

So the proxy really does highly suggest that doing it that way is far less “blocked” than even out-of-the-box Plausible is. The week tested was 6%¹ busier according to the unchanged Google Analytics. I would have expected to see 15.7% more Unique Visitors that week based on what happened with the non-proxied setup (meaning 1.16m), but instead I saw 1.32m, so the proxy demonstrates a solid 13.8% increase in seeing unique visitors versus a non-proxy setup. And comparing the proxied Plausible setup to Google Analytics directly shows a pretty staggering 32% more unique visitors.

With the non-proxied setup, I actually saw a decrease in pageviews (-6.6%) on Plausible compared to Google Analytics, but with the proxied setup I’m seeing 19.4% more pageviews. So the numbers are pretty wishy-washy but, for this website, suggest something in the ballpark of 20-30% of users blocking Google Analytics.

  1. I always find it so confusing to figure out the percentage increase between two numbers. The trick that ultimately works for my brain is (final - initial) / final * 100.

The post Proxying Third-Party JavaScript as First-Party JavaScript (and the Potential Effect on Analytics) appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , ,
[Top]

Exploring the CSS Paint API: Image Fragmentation Effect

In my previous article, I created a fragmentation effect using CSS mask and custom properties. It was a neat effect but it has one drawback: it uses a lot of CSS code (generated using Sass). This time I am going to redo the same effect but rely on the new Paint API. This drastically reduces the amount of CSS and completely removes the need for Sass.

Here is what we are making. Like in the previous article, only Chrome and Edge support this for now.

See that? No more than five CSS declarations and yet we get a pretty cool hover animation.

What is the Paint API?

The Paint API is part of the Houdini project. Yes, “Houdini” the strange term that everyone is talking about. A lot of articles already cover the theoretical aspect of it, so I won’t bother you with more. If I have to sum it up in a few words, I would simply say : it’s the future of CSS. The Paint API (and the other APIs that fall under the Houdini umbrella) allow us to extend CSS with our own functionalities. We no longer need to wait for the release of new features because we can do it ourselves!

From the specification:

An API for allowing web developers to define a custom CSS <image> with javascript [sic], which will respond to style and size changes.

And from the explainer:

The CSS Paint API is being developed to improve the extensibility of CSS. Specifically this allows developers to write a paint function which allows us to draw directly into an elements [sic] background, border, or content.

I think the idea is pretty clear. We can draw what we want. Let’s start with a very basic demo of background coloration:

  1. We add the paint worklet using CSS.paintWorklet.addModule('your_js_file').
  2. We register a new paint method called draw.
  3. Inside that, we create a paint() function where we do all the work. And guess what? Everything is like working with <canvas>. That ctx is the 2D context, and I simply used some well-known functions to draw a red rectangle covering the whole area.

This may look unintuitive at first glance, but notice that the main structure is always the same: the three steps above are the “copy/paste” part that you repeat for each project. The real work is the code we write inside the paint() function.

Let’s add a variable:

As you can see, the logic is pretty simple. We define the getter inputProperties with our variables as an array. We add properties as a third parameter to paint() and later we get our variable using properties.get().

That’s it! Now we have everything we need to build our complex fragmentation effect.

Building the mask

You may wonder why the paint API to create a fragmentation effect. We said it’s a tool to draw images so how it will allow us to fragment an image?

In the previous article, I did the effect using different mask layer where each one is a square defined with a gradient (remember that a gradient is an image) so we got a kind of matrix and the trick was to adjust the alpha channel of each one individually.

This time, instead of using many gradients we will define only one custom image for our mask and that custom image will be handled by our paint API.

An example please!

In the above, I have created an image having an opaque color covering the left part and a semi-transparent one covering the right part. Applying this image as a mask gives us the logical result of a half-transparent image.

Now all we need to do is to split our image to more parts. Let’s define two variables and update our code:

The relevant part of the code is the following:

const n = properties.get('--f-n'); const m = properties.get('--f-m');  const w = size.width/n; const h = size.height/m;  for(var i=0;i<n;i++) {   for(var j=0;j<m;j++) {     ctx.fillStyle = 'rgba(0,0,0,'+(Math.random())+')';         ctx.fillRect(i*w, j*h, w, h); } }

N and M define the dimension of our matrix of rectangles. W and H are the size of each rectangle. Then we have a basic FOR loop to fill each rectangle with a random transparent color.

With a little JavaScript, we get a custom mask that we can easily control by adjusting the CSS variables:

Now, we need to control the alpha channel in order to create the fading effect of each rectangle and build the fragmentation effect.

Let’s introduce a third variable that we use for the alpha channel that we also change on hover.

We defined a CSS custom property as a <number> that we transition from 1 to 0, and that same property is used to define the alpha channel of our rectangles. Nothing fancy will happen on hover because all the rectangles will fade the same way.

We need a trick to prevent fading of all the rectangles at the same time, instead creating a delay between them. Here is an illustration to explain the idea I am going to use:

The above is showing the alpha animation for two rectangles. First we define a variable L that should be bigger or equal to 1 then for each rectangle of our matrix (i.e. for each alpha channel) we perform a transition between X and Y where X - Y = L so we have the same overall duration for all the alpha channel. X should be bigger or equal to 1 and Y smaller or equal to 0.

Wait, the alpha value shouldn’t be in the range [1 0], right ?

Yes, it should! And all the tricks that we’re working on rely on that. Above, the alpha is animating from 8 to -2, meaning we have an opaque color in the [8 1] range, a transparent one in the [0 -2] range and an animation within [1 0]. In other words, any value bigger than 1 will have the same effect as 1, and any value smaller than 0 will have the same effect as 0.

Animation within [1 0] will not happen at the same time for both our rectangles. Rectangle 2 will reach [1 0] before Rectangle 1 will. We apply this to all the alpha channels to get our delayed animations.

In our code we will update this:

rgba(0,0,0,'+(o)+') 

…to this:

rgba(0,0,0,'+((Math.random()*(l-1) + 1) - (1-o)*l)+') 

L is the variable illustrated previously, and O is the value of our CSS variable that transitions from 1 to 0

When O=1, we have (Math.random()*(l-1) + 1). Considering the fact that the random() function gives us a value within the [0 1] range, the final value will be in the [L 1]range.

When O=0, we have (Math.random()*(l-1) + 1 - l) and a value with the [0 1-L] range.

L is our variable to control the delay.

Let’s see this in action:

We are getting closer. We have a cool fragmentation effect but not the one we saw in the beginning of the article. This one isn’t as smooth.

The issue is related the random() function. We said that each alpha channel need to animate between X and Y, so logically those value need to remain the same. But the paint() function is called a bunch during the transition, so each time, the random() function give us different X and Y values for each alpha channel; hence the “random” effect we are getting.

To fix this we need to find a way to store the generated value so they are always the same for each call of the paint() function. Let’s consider a pseudo-random function, a function that always generates the same sequence of values. In other words, we want to control the seed.

Unfortunately, we cannot do this with the JavaScript’s built-in random() function, so like any good developer, let’s pick one up from Stack Overflow:

const mask = 0xffffffff; const seed = 30; /* update this to change the generated sequence */ let m_w  = (123456789 + seed) & mask; let m_z  = (987654321 - seed) & mask;  let random =  function() {   m_z = (36969 * (m_z & 65535) + (m_z >>> 16)) & mask;   m_w = (18000 * (m_w & 65535) + (m_w >>> 16)) & mask;   var result = ((m_z << 16) + (m_w & 65535)) >>> 0;   result /= 4294967296;   return result; }

And the result becomes:

We have our fragmentation effect without complex code:

  • a basic nested loop to create NxM rectangles
  • a clever formula for the channel alpha to create the transition delay
  • a ready random() function taken from the Net

That’s it! All you have to do is to apply the mask property to any element and adjust the CSS variables.

Fighting the gaps!

If you play with the above demos you will notice, in some particular case, strange gaps between the rectangles

To avoid this, we can extend the area of each rectangle with a small offset.

We update this:

ctx.fillRect(i*w, j*h, w, h); 

…with this:

ctx.fillRect(i*w-.5, j*h-.5, w+.5, h+.5); 

It creates a small overlap between the rectangles that compensates for the gaps between them. There is no particular logic with the value 0.5 I used. You can go bigger or smaller based on your use case.

Want more shapes?

Can the above be extended to consider more than rectangular shape? Sure it can! Let’s not forget that we can use Canvas to draw any kind of shape — unlike pure CSS shapes where we sometimes need some hacky code. Let’s try to build that triangular fragmentation effect.

After searching the web, I found something called Delaunay triangulation. I won’t go into the deep theory behind it, but it’s an algorithm for a set of points to draw connected triangles with specific properties. There are lots of ready-to-use implementations of it, but we’ll go with Delaunator because it’s supposed to be the fastest of the bunch.

We first define a set of points (we will use random() here) then run Delauntor to generate the triangles for us. In this case, we only need one variable that defines the number of points.

const n = properties.get('--f-n'); const o = properties.get('--f-o'); const w = size.width; const h = size.height; const l = 7;   var dots = [[0,0],[0,w],[h,0],[w,h]]; /* we always include the corners */ /* we generate N random points within the area of the element */ for (var i = 0; i < n; i++) {   dots.push([random() * w, random() * h]); } /**/ /* We call Delaunator to generate the triangles*/ var delaunay = Delaunator.from(dots); var triangles = delaunay.triangles; /**/ for (var i = 0; i < triangles.length; i += 3) { /* we loop the triangles points */   /* we draw the path of the triangles */   ctx.beginPath();   ctx.moveTo(dots[triangles[i]][0]    , dots[triangles[i]][1]);   ctx.lineTo(dots[triangles[i + 1]][0], dots[triangles[i + 1]][1]);   ctx.lineTo(dots[triangles[i + 2]][0], dots[triangles[i + 2]][1]);     ctx.closePath();   /**/   var alpha = (random()*(l-1) + 1) - (1-o)*l; /* the alpha value */   /* we fill the area of triangle with the semi-transparent color */   ctx.fillStyle = 'rgba(0,0,0,'+alpha+')';   /* we consider stroke to fight the gaps */   ctx.strokeStyle = 'rgba(0,0,0,'+alpha+')';   ctx.stroke();   ctx.fill(); } 

I have nothing more to add to the comments in the above code. I simply used some basic JavaScript and Canvas stuff and yet we have a pretty cool effect.

We can make even more shapes! All we have to do is to find an algorithm for it.

I cannot move on without doing the hexagon one!

I took the code from this article written by Izan Pérez Cosano. Our variable is now R that will define the dimension of one hexagon.

What’s next?

Now that we have built our fragmentation effect, let’s focus on the CSS. Notice that the effect is as simple as changing the opacity value (or the value of whichever property you are working with) of an element on it hover state.

Opacity animation

img {   opacity:1;   transition:opacity 1s; }  img:hover {   opacity:0; }

Fragmentation effect

img {   -webkit-mask: paint(fragmentation);   --f-o:1;   transition:--f-o 1s; }  img:hover {   --f-o:0; }

This means we can easily integrate this kind of effect to create more complex animations. Here are a bunch of ideas!

Responsive image slider

Another version of the same slider:

Noise effect

Loading screen

Card hover effect

That’s a wrap

And all of this is just the tip of the iceberg of what can be achieved using the Paint API. I’ll end with two important points:

  • The Paint API is 90% <canvas>, so the more you know about <canvas>, the more fancy things you can do. Canvas is widely used, which means there’s a bunch of documentation and writing about it to get you up to speed. Hey, here’s one right here on CSS-Tricks!
  • The Paint API removes all the complexity from the CSS side of things. There’s no dealing with complex and hacky code to draw cool stuff. This makes CSS code so much easier to maintain, not to mention less prone to error.

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

CSS-Tricks

, , , ,
[Top]

Let’s Create an Image Pop-Out Effect With SVG Clip Path

Few weeks ago, I stumbled upon this cool pop-out effect by Mikael Ainalem. It showcases the clip-path: path() in CSS, which just got proper support in most modern browsers. I wanted to dig into it myself to get a better feel for how it works. But in the process, I found some issues with clip-path: path(); and wound up finding an alternative approach that I wanted to walk through with you in this article.

If you haven’t used clip-path or you are unfamiliar with it, it basically allows us to specify a display region for an element based on a clipping path and hide portions of the element that fall outside the clip path.

A rectangle with a pastel pattern, plus an unfilled star shape with a black border, equals a star shape with the pastel background pattern.
You can kind of think of it as though the star is a cookie cutter, the element is the cookie dough, and the result is a star-shaped cookie.

Possible values for clip-path include circle , ellipse and polygon which limit the use-case to just those specific shapes. This is where the new path value comes in — it allows us to use a more flexible SVG path to create various clipping paths that go beyond basic shapes.

Let’s take what we know about clip-path and start working on the hover effect. The basic idea of the is to make the foreground image of a person appear to pop-out from the colorful background and scale up in size when the element is hovered. An important detail is how the foreground image animation (scale up and move up) appears to be independent from the background image animation (scale up only).

This effect looks cool, but there are some issues with the path value. For starters, while we mentioned that support is generally good, it’s not great and hovers around 82% coverage at the time of writing. So, keep in mind that mobile support is currently limited to Chrome and Safari.

Besides support, the bigger and more bizarre issue with path is that it currently only works with pixel values, meaning that it is not responsive. For example, let’s say we zoom into the page. Right off the bat, the path shape starts to cut things off.

This severely limits the number of use cases for clip-path: path(), as it can only be used on fixed-sized elements. Responsive web design has been a widely-accepted standard for many years now, so it’s weird to see a new CSS property that doesn’t follow the principle and exclusively uses pixel units.

What we’re going to do is re-create this effect using standard, widely-supported CSS techniques so that it not only works, but is truly responsive as well.

The tricky part

We want anything that overflows the clip-path to be visible only on the top part of the image. We cannot use a standard CSS overflow property since it affects both the top and bottom.

Photo of a young woman against a pastel floral pattern cropped to the shape of a circle.
Using overflow-y: hidden, the bottom part looks good, but the image is cut-off at the top where the overflow should be visible.

So, what are our options besides overflow and clip-path? Well, let’s just use <clipPath> in the SVG itself. <clipPath> is an SVG property, which is different than the newly-released and non-responsive clip-path: path.

SVG <clipPath> element

SVG <clipPath> and <path> elements adapt to the coordinate system of the SVG element, so they are responsive out of the box. As the SVG element is being scaled, its coordinate system is also being scaled, and it maintains its proportions based on the various properties that cover a wide range of possible use cases. As an added benefit, using clip-path in CSS on SVG has 95% browser support, which is a 13% increase compared to clip-path: path.

Let’s start by setting up our SVG element. I’ve used Inkscape to create the basic SVG markup and clipping paths, just to make it easy for myself. Once I did that, I updated the markup by adding my own class attributes.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -10 100 120" class="image">   <defs>     <clipPath id="maskImage" clipPathUnits="userSpaceOnUse">       <path d="..." />     </clipPath>     <clipPath id="maskBackground" clipPathUnits="userSpaceOnUse">       <path d="..." />     </clipPath>   </defs>   <g clip-path="url(#maskImage)" transform="translate(0 -7)">     <!-- Background image -->     <image clip-path="url(#maskBackground)" width="120" height="120" x="70" y="38" href="..." transform="translate(-90 -31)" />     <!-- Foreground image -->     <image width="120" height="144" x="-15" y="0" fill="none" class="image__foreground" href="..." />   </g> </svg>
A bright green circle with a bright red shape coming out from the top of it, as if another shape is behind the green circle.
SVG <clipPath> elements created in Inkscape. The green element represents a clipping path that will be applied to the background image. The red is a clipping path that will be applied to both the background and foreground image.

This markup can be easily reused for other background and foreground images. We just need to replace the URL in the href attribute inside image elements.

Now we can work on the hover animation in CSS. We can get by with transforms and transitions, making sure the foreground is nicely centered, then scaling and moving things when the hover takes place.

.image {   transform: scale(0.9, 0.9);   transition: transform 0.2s ease-in; }  .image__foreground {   transform-origin: 50% 50%;   transform: translateY(4px) scale(1, 1);   transition: transform 0.2s ease-in; }  .image:hover {   transform: scale(1, 1); }  .image:hover .image__foreground {   transform: translateY(-7px) scale(1.05, 1.05); }

Here is the result of the above HTML and CSS code. Try resizing the screen and changing the dimensions of the SVG element to see how the effect scales with the screen size.

This looks great! However, we’re not done. We still need to address some issues that we get now that we’ve changed the markup from an HTML image element to an SVG element.

SEO and accessibility

Inline SVG elements won’t get indexed by search crawlers. If the SVG elements are an important part of the content, your page SEO might take a hit because those images probably won’t get picked up.

We’ll need additional markup that uses a regular <img> element that’s hidden with CSS. Images declared this way are automatically picked up by crawlers and we can provide links to those images in an image sitemap to make sure that the crawlers manage to find them. We’re using loading="lazy" which allows the browser to decide if loading the image should be deferred.

We’ll wrap both elements in a <figure> element so that we markup reflects the relationship between those two images and groups them together:

<figure>   <!-- SVG element -->   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -10 100 120" class="image">      <!-- ... -->   </svg>   <!-- Fallback image -->   <img src="..." alt="..." loading="lazy" class="fallback-image" /> </figure>

We also need to address some accessibility concerns for this effect. More specifically, we need to make improvements for users who prefer browsing the web without animations and users who browse the web using screen readers.

Making SVG elements accessible takes a lot of additional markup. Additionally, if we want to remove transitions, we would have to override quite a few CSS properties which can cause issues if our selector specificities aren’t consistent. Luckily, our newly added regular image has great accessibility features baked right in and can easily serve as a replacement for users who browse the web without animations.

<figure>   <!-- Animated SVG element -->   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -10 100 120" class="image" aria-hidden="true">     <!-- ... -->   </svg>    <!-- Fallback SEO & a11y image -->   <img src="..." alt="..." loading="lazy" class="fallback-image" /> </figure>

We need to hide the SVG element from assistive devices, by adding aria-hidden="true", and we need to update our CSS to include the prefers-reduced-motion media query. We are inclusively hiding the fallback image for users without the reduced motion preference meanwhile keeping it available for assistive devices like screen readers.

@media (prefers-reduced-motion: no-preference) { .fallback-image {   clip: rect(0 0 0 0);    clip-path: inset(50%);   height: 1px;   overflow: hidden;   position: absolute;   white-space: nowrap;    width: 1px;   }  }  @media (prefers-reduced-motion) {   .image {     display: none;   } }

Here is the result after the improvements:

Please note that these improvements won’t change how the effect looks and behaves for users who don’t have the prefers-reduced-motion preference set or who aren’t using screen readers.

That’s a wrap

Developers were excited about path option for clip-path CSS attribute and new styling possibilities, but many were displeased to find out that these values only support pixel values. Not only does that mean the feature is not responsive, but it severely limits the number of use cases where we’d want to use it.

We converted an interesting image pop-out hover effect that uses clip-path: path into an SVG element that utilizes the responsiveness of the <clipPath> SVG element to achieve the same thing. But in doing so, we introduced some SEO and accessibility issues, that we managed to work around with a bit of extra markup and a fallback image.

Thank you for taking the time to read this article! Let me know if this approach gave you an idea on how to implement your own effects and if you have any suggestions on how to approach this effect in a different way.


The post Let’s Create an Image Pop-Out Effect With SVG Clip Path appeared first on CSS-Tricks.

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

CSS-Tricks

, , , , , ,
[Top]