The soul of a “liquid glass” design is a good illusion of light refraction. That is, to see parts of the screen through a metaphorical lens. This article shows you how to get that for the backgrounds of elements.
This is the type of design I’m talking about, taken directly from the demo we’ll build:

Why are fixed backgrounds limiting?
Typically, to achieve the refracted effect, we set the same fixed background to the refraction backdrop element and the elements displaying the refracted look inside. This way, the overlapping elements show the same part of the image. A slight shift in the background of one of these elements causes that refracted appearance.
<div id=card-container>
<div id=card></div>
<div>Code language: HTML, XML (xml)
#card-container {
background: center/cover url("image.jpg");
background-attachment: fixed;
}
#card {
border: 2px red solid;
background: inherit;
background-position: -20px -20px;
margin-left: auto;
}Code language: CSS (css)
The downside of this method is that the background image sits relative to the screen (viewport), not the backdrop element. To make sure the image fits nicely into the backdrop, we’ll need to adjust its position and size. This means we need to know the backdrop element’s dimensions and, most importantly, its position on the screen in CSS. Figuring this out for a random element in a responsive layout can be tricky.
How does CSS view() resolve this?
There is an easy way to track the positions of even random elements in CSS, under the right circumstances. CSS view() tracks the position, visibility to be precise, of an element inside a scroll container.
So if we use a scroll container as a refraction backdrop, the backgrounds of the elements inside it can easily align with the corresponding parts of the backdrop by repositioning themselves based on their positions within the container.
No need for fixed backgrounds and complex calculations. Interestingly, actual scrolling is not required for this technique either.
Let’s see how it’s done.
The Setup
We got a container with a background image for the refraction backdrop and a child element that’ll be showing the refraction effect.
<div id="card-container">
<div id="card"></div>
<div>Code language: HTML, XML (xml)
The Container (Refraction Backdrop)
In CSS, we first explicitly set the container’s background image size to ensure consistent dimensions.
#card-container {
--w: 380px;
--h: 420px;
width: var(--w);
height: var(--h);
background: url("image.jpg") 0 0 / var(--w) var(--h) no-repeat;
}Code language: CSS (css)
Then we convert the container to a scroll container with overflow:hidden. I’ll tell you why in a moment.
#card-container {
/* ... */
overflow: hidden;
}Code language: CSS (css)
The Card (Refraction Foreground)
Moving onto the card, the element that’ll be showing the refracted look, we set the card’s background image to inherit from the container so they are the same.
#card {
background: inherit;
}Code language: CSS (css)
Let’s then add two keyframe animations on the card that meet the following criteria:
- Both animations have linear timing function (constant animation progression). This avoids any discrepenacy when we try to align the refracted elements’ backgrounds to the backdrop.
- contain animation range is set (animates within the viewport’s bounds). So that the backrop’s edges strictly remain the reference point for the inner elements’ refracted effects.
- The animation timelines are view timelines, one for the x-axis and the other for the y-axis. CSS view() handles only one direction at a time, so we set one for the horizontal direction and one for the vertical.
#card {
/* ... */
animation: pos-x linear, pos-y linear;
animation-timeline: view(x), view(y);
animation-range: contain;
}Code language: CSS (css)
Since we’d turned the container into a scroll container earlier, we can now apply view timeline to the card to animate based on its visibility within the container.
What will we be animating? The card’s background position.
@keyframes pos-x {
from { background-position-x: 100%; }
}
@keyframes pos-y {
from { background-position-y: 100%; }
}Code language: CSS (css)
The keyframe pos-x positions the background image horizontally, depending on the card’s horizontal position within the container (view(x)). Similarly, pos-y positions it vertically, based on the card’s vertical position within the container (view(y))
We’ve now set the exact portion of the container’s background that the card is placed on as the card’s background. (The card has a red border)
The Refraction
Let’s introduce a little refraction by resizing the card’s background.
#card {
background: inherit;
background-size: calc(var(--w) - 100px) calc(var(--h) - 100px);
/.../
}Code language: CSS (css)
Alternatively, we could move the background position for a different refraction look.
@keyframes pos-x {
from { background-position-x: 85%; }
}
@keyframes pos-y {
from { background-position-y: 85%; }
}Code language: CSS (css)
Basically, we offset the background image slightly in the card.
With the refraction covered, it should be easy to layer effects like blur, shadow, skew and such for a more advanced glassmorphism. Since we’re using scroll based view() for this effect, if the card can be scrolled around, the background image will update automatically.
Here’s the demo for the example from the beginning of the article:
Here’s another example where the elements can be scrolled:

When I first saw this, I was like… we can just make the main background and all the sub-element backgrounds have the same background and use
background-attachment: fixed;so they appear at the same point to start, then mess with them.https://codepen.io/editor/chriscoyier/pen/01a00c77-6c5a-70b2-887a-8e71372ce58e
But the “attachment point” of the background is like the upper left of the page and way too limiting. This
view()based technique is way more freeing.