Background Image Refraction in CSS
Background Image Refraction in CSS êŽë š
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>
#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;
}
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>
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;
}
Then we convert the container to a scroll container with overflow:hidden. Iâll tell you why in a moment.
#card-container {
/* ... */
overflow: hidden;
}
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;
}
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;
}
Note
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%; }
}
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);
/* ... */
}
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%; }
}
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:
Info

Liquid Glass on the Web
It's a complicated look! There may or may not be blurring, light refracts in tricky ways, the highlights are quite bright, and you've got to be very careful about text contrast accessibility.
You can limit how far the background-image of an element applies by using background-clip. That means you can apply different backgrounds to, say, the padding and border.
You might need to know this someday: you can style a div, put the div into SVG, then put the SVG in to CSS and use it as a repeating background.