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.
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.
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);
}
}
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;
}
}
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);
}
}
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.
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.
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?
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);
}
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);
}
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%);
}
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);
}
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
!
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 */
}
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;
}
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) }
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);
}
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
.
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)
.
Alright, but weโre still sticking to just a translation for now:
#blur {
/* same as before */
transform: translate(-.25em, -.25em); /* replaced margin */
}
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.
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.
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));
}
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!
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.
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;
}
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:
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:
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!
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!
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.