Frosted Glass from Games to the Web

During my time as a UI developer for Forza Horizon 3 and Forza Motorsport 7, I had the opportunity to work with stunning frosted acrylic design elements. Here's an example from Horizon 3:

Inspired by this design, I've always wanted to create a similar effect using HTML. On this page, I share my attempt at achieving a beautiful glass effect, along with sample code and assets for anyone who wants to explore this technique themselves. Here's a look at the final product:

Toggle Fill Space
Drag Me

Final Recipe Lookahead

Before diving into the core of this tutorial, some readers may prefer to skip straight to the final recipe so they can paste it right into their own page. Those readers can find it conveniently tucked away in this toggle.

Toggle final recipe

Light Reflection Asset

JavaScript Version

This works on all platforms.

HTML

1
2
3
4
<div class="glass">  <div class="light" data-js-background-attachment-fixed/>  <div class="drag-me">Drag Me</div></div>

CSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
.glass {  /* Blur effect */  backdrop-filter: blur(10px);  -webkit-backdrop-filter: blur(10px);
  box-shadow:    /* Bottom and right depth effect */    inset -0.75px -0.5px rgba(255, 255, 255, 0.1),    /* Top and left depth effect */    inset +0.75px +0.5px rgba(255, 255, 255, 0.025),    /* Shadow effect */    3px 2px 10px rgba(0, 0, 0, 0.25),    /* Short subsurface effect */    inset 0px 0px 10px 5px rgba(255, 255, 255, 0.025),    /* Long subsurface effect */    inset 0px 0px 40px 5px rgba(255, 255, 255, 0.025);
  /* Allow children to fill the parent */  position: relative;
  /* Round the corners */  border-radius: 5px;
  /* Hide the corners of the header */  overflow: hidden;}
.light {   /* Apply the background image */   background-image: url(path/to/light.png);   background-repeat: repeat;   background-size: 750px;
   /* Adjust the intensity */   opacity: 0.075;
   /* Fill the background space */   position: absolute;   bottom: 0;   left: 0;   right: 0;   top: 0;
   /* Render behind other children */   z-index: -1;}
.drag-me {  /* Center the content */  display: flex;  align-items: center;  justify-content: center;
  /* Size the content */  height: 30px;
  /* Add a transparent background */  background-color: rgba(12, 13, 14, 0.75);}

JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/** * Iterates through all `HTMLElement`s with * `data-js-background-attachment-fixed` and updates the `background-position` * to simulate `background-attachment: fixed`. */const updateDataJSBackgroundAttachmentFixedElements = () => {  // Find all elements with the `data-js-background-attachment-fixed` attribute  const elements = document.querySelectorAll(    "[data-js-background-attachment-fixed]",  );
  for (const element of elements) {    // Only consider `HTMLElement`s    if (!(element instanceof HTMLElement)) continue;
    // Find the position of the element    const clientRect = element.getBoundingClientRect();
    // Move the background position opposite the position in the viewport    const backgroundPositionX = `${(-clientRect.x).toString()}px`;    const backgroundPositionY = `${(-clientRect.y).toString()}px`;
    element.style.backgroundPositionX = backgroundPositionX;    element.style.backgroundPositionY = backgroundPositionY;  }};
/** * Begins a loop which simulates `background-attachment: fixed` for * `HTMLElement`s with `data-js-background-attachment-fixed`. * * This loop executes each animation frame. */const initDataJSBackgroundAttachmentFixed = () => {  requestAnimationFrame(() => {    updateDataJSBackgroundAttachmentFixedElements();    initDataJSBackgroundAttachmentFixed();  });};
initDataJSBackgroundAttachmentFixed();

Non-JavaScript Version

This won't work on every platform.

HTML

1
2
3
<div class="glass">  <div class="drag-me">Drag Me</div></div>

CSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
.glass {  /* Blur effect */  backdrop-filter: blur(10px);  -webkit-backdrop-filter: blur(10px);
  box-shadow:    /* Bottom and right depth effect */    inset -0.75px -0.5px rgba(255, 255, 255, 0.1),    /* Top and left depth effect */    inset +0.75px +0.5px rgba(255, 255, 255, 0.025),    /* Shadow effect */    3px 2px 10px rgba(0, 0, 0, 0.25),    /* Short subsurface effect */    inset 0px 0px 10px 5px rgba(255, 255, 255, 0.025),    /* Long subsurface effect */    inset 0px 0px 40px 5px rgba(255, 255, 255, 0.025);
  /* Allow children to fill the parent */  position: relative;
  /* Round the corners */  border-radius: 5px;
  /* Hide the corners of the header */  overflow: hidden;}
.glass::before {  /* Make the element render */  content: "";
  /* Apply the background image */  background-image: url(path/to/light.png);  background-repeat: repeat;  background-size: 750px;
  /* Adjust the intensity */  opacity: 0.075;
  /* Fill the background space */  position: absolute;  bottom: 0;  left: 0;  right: 0;  top: 0;
  /* Render behind other children */  z-index: -1;
  /* Fix the reflection to the screen */  background-attachment: fixed;}
.drag-me {  /* Center the content */  display: flex;  align-items: center;  justify-content: center;
  /* Size the content */  height: 30px;
  /* Add a transparent background */  background-color: rgba(12, 13, 14, 0.75);}

Now, on with the show!

backdrop-filter Does Heavy Lifting

The key to a good frosted glass effect is using a Gaussian blur to obscure the background, mimicking the look of a translucent screen. Many browsers and game engines achieve Gaussian blurs with efficient approximations like box blurs.

In CSS, Gaussian blurs can be applied by using backdrop-filter with the blur function. On iOS -webkit-backdrop-filter is required unless you go deep into the settings where no man has ventured before (Settings -> Safari -> Advanced -> Feature Flags -> CSS Unprefixed Backdrop Filter). backdrop-filter is only recently supported by browsers. So Internet Explorer users can't experience these demos.

Putting this all together, our glass is a simple div:

1
<div class="glass" />

We style the div with a glass class:

1
2
3
4
5
.glass {  /* Blur effect */  backdrop-filter: blur(10px);  -webkit-backdrop-filter: blur(10px);}

This is the result. Drag it to see how the blur interacts with the background.

Toggle Fill Space

Adding Depth

We've crafted a respectable piece of glass using backdrop-filter: blur(10px). Many guides stop here, but there's room for improvement! Right now, our glass looks flat and boring. Real glass has an interesting visual character around its edges that we're missing. Let's bring that to life.

Edges

First we'll add edges to our glass. Others, like css.glass, do this using border: 1px solid. Borders affect the element's size and how it interacts with children. I prefer using box-shadow: inset. By using box-shadow: inset the children seamlessly fill the content area of the glass without negative margins or other magic. This allows them to behave like decals applied to the glass surface.

box-shadow requires at least two size values and supports up to four. The first two define the shadow's x and y offsets. To mimic the edges of glass, we use two translucent white box-shadows.

The first shadow represents the edge of the glass as seen from outside. In our example this appears along the bottom and left edges. The second shadow represents the edge as seen through the glass. In our example this appears along the top and right edges.

Since light diminishes when viewing the edge through the glass, the top and left edges are softer and more translucent compared to the bottom and right edges. This subtle difference creates the illusion that the glass is slightly elevated at the bottom-right corner.

1
2
3
4
5
6
7
8
9
10
11
.glass {  /* Blur effect */  backdrop-filter: blur(10px);  -webkit-backdrop-filter: blur(10px);
  box-shadow:    /* Bottom and right depth effect */    inset -0.75px -0.5px rgba(255, 255, 255, 0.1),    /* Top and left depth effect */    inset +0.75px +0.5px rgba(255, 255, 255, 0.025);}
Toggle Fill Space

Real Shadow

Our glass now has some nice depth, but something still feels off—it has that uncanny appearance of a 3D object trapped in 2D space. To fix this, let's make it look physically raised from the background. Thankfully, there's a simple CSS trick for this: a traditional, dark box-shadow.

Let's add a box-shadow proportional to the size of our edges to create the desired lift effect:

1
2
3
4
5
6
7
8
9
10
11
12
13
.glass {  /* Blur effect */  backdrop-filter: blur(10px);  -webkit-backdrop-filter: blur(10px);
  box-shadow:    /* Bottom and right depth effect */    inset -0.75px -0.5px rgba(255, 255, 255, 0.1),    /* Top and left depth effect */    inset +0.75px +0.5px rgba(255, 255, 255, 0.025),    /* Shadow effect */    3px 2px 10px rgba(0, 0, 0, 0.25);}
Toggle Fill Space

Blending in Light

Our glass has a nice depth and we're at a great stopping point. In comparison to other glass designs we're already ahead of the curve—but there's more to do! Lets shift our focus to the interaction between glass and light.

Simple Subsurface Scattering

We'll start playing with light by introducing a subsurface scattering approximation. Subsurface scattering refers to the way light disperses within a translucent surface. For glass, this is most noticable around the edges.

We will use box-shadow: inset to simulate this effect. This will add a subtle layer of light that penetrates slightly into the glass from the edges. To fully appreciate the effect, try dragging the glass over a darker area of the image.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.glass {  /* Blur effect */  backdrop-filter: blur(10px);  -webkit-backdrop-filter: blur(10px);
  box-shadow:    /* Bottom and right depth effect */    inset -0.75px -0.5px rgba(255, 255, 255, 0.1),    /* Top and left depth effect */    inset +0.75px +0.5px rgba(255, 255, 255, 0.025),    /* Shadow effect */    3px 2px 10px rgba(0, 0, 0, 0.25),    /* Short subsurface effect */    inset 0px 0px 10px 5px rgba(255, 255, 255, 0.025);}
Toggle Fill Space

More Subsurface Scattering

As you can see, the subsurface scattering is subtle. Lets add another, deeper layer to enhance it further.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.glass {  /* Blur effect */  backdrop-filter: blur(10px);  -webkit-backdrop-filter: blur(10px);
  box-shadow:    /* Bottom and right depth effect */    inset -0.75px -0.5px rgba(255, 255, 255, 0.1),    /* Top and left depth effect */    inset +0.75px +0.5px rgba(255, 255, 255, 0.025),    /* Shadow effect */    3px 2px 10px rgba(0, 0, 0, 0.25),    /* Short subsurface effect */    inset 0px 0px 10px 5px rgba(255, 255, 255, 0.025),    /* Long subsurface effect */    inset 0px 0px 40px 5px rgba(255, 255, 255, 0.025);}
Toggle Fill Space

More Interesting Light

We've done a good job refracting light from the background through our glass element. But another key characteristic of glass is its ability to reflect light off its surface. This reflective quality is the secret sauce to give our glass a distinctive look from other HTML glass examples.

Rays of Light

We'll blend in a image of light rays to create this effect. If you're interested in creating a similar image yourself, there are plenty of tutorials online. Here's one from YouTube, and here's a detailed textual walkthrough if you prefer that approach. Alternatively, you can use download and use these light rays I created:

We'll explore a few ways to blend this image into the background. The first is to set it as the background-image of our glass element. Doing this directly on the element causes the box-filter to blur the background-image—we don't want that. Instead, we can apply the background-image to a pseudo child element using ::before.

To ensure the pseudo-element fills the parent completely, we need to position the parent. "Positioning" an element just means, "setting its position property to something other than static (the default)." That's most often position: relative or position: absolute. We'll use relative positioning for our glass. Once a parent is positioned, a child can be stretched to fill it by setting its position to absolute and its edge offsets (bottom, left, right, top) to 0.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
.glass {  /* Blur effect */  backdrop-filter: blur(10px);  -webkit-backdrop-filter: blur(10px);
  box-shadow:    /* Bottom and right depth effect */    inset -0.75px -0.5px rgba(255, 255, 255, 0.1),    /* Top and left depth effect */    inset +0.75px +0.5px rgba(255, 255, 255, 0.025),    /* Shadow effect */    3px 2px 10px rgba(0, 0, 0, 0.25),    /* Short subsurface effect */    inset 0px 0px 10px 5px rgba(255, 255, 255, 0.025),    /* Long subsurface effect */    inset 0px 0px 40px 5px rgba(255, 255, 255, 0.025);
  /* Allow children to fill the parent */  position: relative;}
.glass::before {  /* Make the element render */  content: "";
  /* Apply the background image */  background-image: url(path/to/light.png);  background-repeat: repeat;  background-size: 750px;
  /* Adjust the intensity */  opacity: 0.075;
  /* Fill the background space */  position: absolute;  bottom: 0;  left: 0;  right: 0;  top: 0;
  /* Render behind other children */  z-index: -1;}

Use the "Toggle Fill Space" option to most easily observe this effect.

Toggle Fill Space

Dynamic Light

Our light rays are blending nicely, but they appear static on the glass. In reality, moving glass through light creates dynamic reflections as the light shifts across the glass surface. Unfortunately, replicating this effect across all browsers is challenging. To address this, we'll explore two solutions: a pure CSS approach that works on most platforms except mobile, and a CSS + JavaScript solution which works everywhere.

The CSS solution is satisfyingly simple. We just add background-attachement: fixed to our ::before element.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
.glass::before {  /* Make the element render */  content: "";
  /* Apply the background image */  background-image: url(path/to/light.png);  background-repeat: repeat;  background-size: 750px;
  /* Adjust the intensity */  opacity: 0.075;
  /* Fill the background space */  position: absolute;  bottom: 0;  left: 0;  right: 0;  top: 0;
  /* Render behind other children */  z-index: -1;
  /* Fix the reflection to the screen */  background-attachment: fixed;}

Use the "Toggle Fill Space" option and scroll or move the glass to observe the effect. Keep in mind that mobile support may be limited; we'll address this in the next example.

Toggle Fill Space

Cross-Platform Dynamic Light

We'll use JavaScript to simulate background-attachment: fixed on all platforms :(. To achieve the effect, we'll dynamically adjust the background-position as the image moves within the viewport. Since accessing ::before elements directly from JavaScript is inefficient, we'll use a div instead.

Our JavaScript will target elements with a specific data-* attribute. We'll use the attribute data-js-background-attachment-fixed.

Putting this all together, we now have two divs:

1
2
3
<div class="glass">  <div class="light" data-js-background-attachment-fixed/></div>

We style the new child div with a light class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.light {   /* Apply the background image */   background-image: url(path/to/light.png);   background-repeat: repeat;   background-size: 750px;
   /* Adjust the intensity */   opacity: 0.075;
   /* Fill the background space */   position: absolute;   bottom: 0;   left: 0;   right: 0;   top: 0;
   /* Render behind other children */   z-index: -1;}

This JavaScript updates our background:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/** * Iterates through all `HTMLElement`s with * `data-js-background-attachment-fixed` and updates the `background-position` * to simulate `background-attachment: fixed`. */const updateDataJSBackgroundAttachmentFixedElements = () => {  // Find all elements with the `data-js-background-attachment-fixed` attribute  const elements = document.querySelectorAll(    "[data-js-background-attachment-fixed]",  );
  for (const element of elements) {    // Only consider `HTMLElement`s    if (!(element instanceof HTMLElement)) continue;
    // Find the position of the element    const clientRect = element.getBoundingClientRect();
    // Move the background position opposite the position in the viewport    const backgroundPositionX = `${(-clientRect.x).toString()}px`;    const backgroundPositionY = `${(-clientRect.y).toString()}px`;
    element.style.backgroundPositionX = backgroundPositionX;    element.style.backgroundPositionY = backgroundPositionY;  }};
/** * Begins a loop which simulates `background-attachment: fixed` for * `HTMLElement`s with `data-js-background-attachment-fixed`. * * This loop executes each animation frame. */const initDataJSBackgroundAttachmentFixed = () => {  requestAnimationFrame(() => {    updateDataJSBackgroundAttachmentFixedElements();    initDataJSBackgroundAttachmentFixed();  });};
initDataJSBackgroundAttachmentFixed();

Use the "Toggle Fill Space" option and scroll or move the glass to observe the effect.

Toggle Fill Space

Bits n' Bobs

Now that our glass effect is complete, let's finalize it by adding the remaining CSS properties and HTML to fully replicate our initial example. That means beautiful rounded corners and extra elements to enhance the glass with color and text.

Rounded Corners

Rounding the corners of our glass is straightforward—simply use the border-radius property.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.glass {  /* Blur effect */  backdrop-filter: blur(10px);  -webkit-backdrop-filter: blur(10px);
  box-shadow:    /* Bottom and right depth effect */    inset -0.75px -0.5px rgba(255, 255, 255, 0.1),    /* Top and left depth effect */    inset +0.75px +0.5px rgba(255, 255, 255, 0.025),    /* Shadow effect */    3px 2px 10px rgba(0, 0, 0, 0.25),    /* Short subsurface effect */    inset 0px 0px 10px 5px rgba(255, 255, 255, 0.025),    /* Long subsurface effect */    inset 0px 0px 40px 5px rgba(255, 255, 255, 0.025);
  /* Allow children to fill the parent */  position: relative;
  /* Round the corners */  border-radius: 5px;}
Toggle Fill Space

Colored Glass

To color our glass, we'll add one final element on top, using a background-color with an alpha value for translucency. Since our glass has rounded corners, the child element will extend beyond the glass surface. To prevent this, we'll use overflow: hidden to clip it.

Here is our final HTML:

1
2
3
4
<div class="glass">  <div class="light" data-js-background-attachment-fixed/>  <div class="drag-me">Drag Me</div></div>

And our final CSS for the glass and drag-me classes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
.glass {  /* Blur effect */  backdrop-filter: blur(10px);  -webkit-backdrop-filter: blur(10px);
  box-shadow:    /* Bottom and right depth effect */    inset -0.75px -0.5px rgba(255, 255, 255, 0.1),    /* Top and left depth effect */    inset +0.75px +0.5px rgba(255, 255, 255, 0.025),    /* Shadow effect */    3px 2px 10px rgba(0, 0, 0, 0.25),    /* Short subsurface effect */    inset 0px 0px 10px 5px rgba(255, 255, 255, 0.025),    /* Long subsurface effect */    inset 0px 0px 40px 5px rgba(255, 255, 255, 0.025);
  /* Allow children to fill the parent */  position: relative;
  /* Round the corners */  border-radius: 5px;
  /* Hide the corners of the header */  overflow: hidden;}
.drag-me {  /* Center the content */  display: flex;  align-items: center;  justify-content: center;
  /* Size the content */  height: 30px;
  /* Add a transparent background */  background-color: rgba(12, 13, 14, 0.75);}

Rendering it out we see our old friend:

Toggle Fill Space
Drag Me

Thanks for Staying!

Thanks for following along as we created an amazing glass effect! I hope you enjoyed the journey. For a quick way to copy all the assets and code, don't forget to check out the Final Recipe Lookahead section near the beginning.

Discuss on
Hacker News