![](https://envo.app/wp-content/uploads/2024/05/moving-backgrounds.jpg)
GSAP is the main driver there, but I imagine we could make a boiled-down version that simply translates each background layer from left to right at different speeds. Not as cool, of course, but certainly the baseline experience. Gotta make sure the start and end of each background image is consistent so it repeats seamlessly when the animation repeats.
// run the function on `mousemove`
container.addEventListener("mousemove", (e) => {
magnifier.setAttribute("class", "magnifier");
}
Lots of ways to do this, of course! It can certainly be optimized with Sass and/or CSS variables. Heck, maybe you can pull it off with a single <div>
If so, share it in the comments!
How so? Let’s look at a few examples I’ve seen floating around.
Show me more!
let container = document.querySelector(".container");
container.addEventListener("mousemove", function(e) {
container.style.backgroundPositionX = -e.offsetX * 1.32 + "px";
container.style.backgroundPositionY = -e.offsetY * 0.455 + "px";
}
);
// Magnifier image configurations
let magnify = 2;
let imgWidth = 500;
let imgHeight = 400;
magnifier.style.backgroundSize = imgWidth * magnify + "px " + imgHeight * magnify + "px";
Now, let’s turn to the JavaScript that pulls this all together. First we need to access the CSS variable defined earlier. We will use this in multiple places later on. Then we need get the mouse position within the container because that’s the value we’ll use for the the magnifier’s background position.
container.addEventListener("mouseleave", function() {
container.style.backgroundPosition = "0px 0px";
}
);
// Get the css variables
let root = window.getComputedStyle(document.documentElement);
let magnifier_width = root.getPropertyValue("--magnifer-width");
let magnifier_height = root.getPropertyValue("--magnifer-height");
let container = document.querySelector(".container");
let rect = container.getBoundingClientRect();
let x = (e.pageX - rect.left);
let y = (e.pageY - rect.top);
// Take page scrolling into account
x = x - window.pageXOffset;
y = y - window.pageYOffset;
:root {
--magnifer-width: 85;
--magnifer-height: 85;
}
.container {
width: 500px;
height: 400px;
background-size: cover;
background-image: url("/path/to/image.png");
background-repeat: no-repeat;
position: relative;
}
<div class="container"></div>
But there are times when the position or scale of a background image might sit somewhere between the poles of content and decoration. Context is king, right? If we change the background image’s position, it may convey a bit more context or experience.
let container = document.querySelector(".container");
container.addEventListener("mousemove", function(e) {
// Our function
}
);
The real trick is with JavaScript. We will use that to get the mouse position and the container’s offset, then convert that value to an appropriate scale to set the background-position
. First, let’s listen for mouse movements on the .container
element:
There are plenty of examples over at CodePen if you wanna get a better idea.
The big limitation for most ads, I’d wager, is the limited real estate. I don’t know if you’ve ever had to drop an ad onto a page, but I have and typically ask the advertiser for an image that meets exact pixel dimensions, so the asset fits the space.
It’s a bit touch-and-feel, but I’ve found that 1.32
and 0.455
work perfectly for the X and Y coordinates, respectively. We multiply the offsets by those values, append a px
unit on the result, then apply it to the background-position
values.
Paint a bigger picture
// create the magnifier
let magnifier = document.createElement("div");
container.append(magnifier);
.magnifier {
position: absolute;
width: calc(var(--magnifer-width) * 1px);
height: calc(var(--magnifer-height) * 1px);
border: 3px solid #000;
cursor: none;
background-image: url("/path/to/image.png");
background-repeat: no-repeat;
}
// Run the function on mouse move.
container.addEventListener("mousemove", (e) => {
magnifier.setAttribute("class", "magnifier");
// Get mouse position
let rect = container.getBoundingClientRect();
let x = (e.pageX - rect.left);
let y = (e.pageY - rect.top);
// Take page scrolling into account
x = x - window.pageXOffset;
y = y - window.pageYOffset;
// Prevent magnifier from exiting the container
// Then set top and left values of magnifier
if (x >= 0 && x <= container.clientWidth - magnifier_width) {
magnifier.style.left = x + "px";
}
if (y >= 0 && y <= container.clientHeight - magnifier_height) {
magnifier.style.top = y + "px";
}
});
We often think of background images as texture or something that provides contrast for legible content — in other words, not really content. If it was content, you’d probably reach for an <img>
anyway, accessibility and whatnot.
This pattern has been around for what feels like forever (Dylan Winn-Brown shared his approach back in 2016), but that’s just a testament (I hope) to its usefulness. The user gets more context as they zoom in and get a better idea of a sweater’s stitching or what have you.
That’s it for now! Pretty neat that we can use backgrounds for much more than texture and contrast. I’m absolutely positive there are tons of other clever interactions we can apply to backgrounds. Temani Afif did exactly that with a bunch of neat hover effects for links. What do you have in mind? Share it with me in the comments!
As far as styling goes, the important pieces are here:
.container {
background-image: url("/path/to/some/image.png");
background-repeat: no-repeat;
background-position: 0 0;
height: 400px;
width: 350px;
}
I don’t know if anything is cooler than Sarah Drasner’s “Happy Halloween” pen… and that’s from 2016! It is a great example that layers backgrounds and moves them at varying speeds to create an almost cinematic experience. Good gosh is that cool!
Notice how I’m taking scrolling into account. If there is overflow, subtracting the window pageX
and pageY
offsets will ensure the effect runs as expected.
We will first create the magnifier div. Next, we will create a mousemove
function and add it to the image container. In this function, we will give the magnifier a class attribute. We will also calculate the mouse position and give the magnifier the left and top values we calculated earlier.
// the x and y positions of the magnifier image
let magnify_x = x * magnify + 15;
let magnify_y = y * magnify + 15;
// set backgroundPosition for magnifier if it is within image
if (
x <= container.clientWidth - magnifier_width &&
y <= container.clientHeight - magnifier_height
) {
magnifier.style.backgroundPosition = -magnify_x + "px " + -magnify_y + "px";
}
Let’s go ahead and build the magnifier
when we hear a mousemove
event on the .container
:
The demo is super practical in lots of ways because it’s a neat approach for displaying ads in content. You have the sales pitch and an enticing image to supplement it.
But Chris’s demo alleviates the space issue. Hover the image and watch it both move and scale. The user actually gets more context for the product than they would have when the image was in its original position. That’s a win-win, right? The advertiser gets to create an eye-catching image without compromising context. Meanwhile, the user gets a little extra value from the newly revealed portions of the image.
<div class="ad-container">
<a href="#" target="_blank" rel="noopener">
<!-- Background image container -->
<div class="ad-image"></div>
</a>
<div class="ad-content">
<!-- Content -->
</div>
</div>
Chris Coyier has this neat little demo from several years back.
You’ll see that there are a number of ways to approach this. Some use JavaScript. Others are 100% CSS. I’m sure the JavaScript approaches are good for some uses cases, but if the goal is simply to subtly scale the image, CSS is perfectly suitable.
There’s two pieces to this: the container and the magnifier. The container is the only thing we need in the markup, as we’ll inject the magnifier element during the user’s interaction. So, behold our HTML!
We could spice things up a bit using multiple backgrounds rather than one. Or, better yet, if we expand the rules to use elements instead of background images, we can apply the same animation to all of the backgrounds and use a dash of animation-delay
to stagger the effect.
If you peek at the demo’s markup, you’ll notice it’s pretty much what you’d expect. Here’s an abridged version:
Bonus: Make it immersive
No doubt you’ve been to some online clothing store or whatever and encountered the ol’ zoom-on-hover feature.
As we get started, I’ll caution that there’s a fine line in these demos between images used for decoration and images used as content. The difference has accessibility implications where backgrounds are not announced to screen readers. If your image is really an image, then maybe consider an <img>
tag with proper alt
text. And while we’re talking accessibility, it’s a good idea to consider a user’s motion preference’s as well.
Have you seen the Ken Burns effect? It’s classic and timeless thing where an image is bigger than the container it’s in, then sorta slides and scales slow as a slug. Just about every documentary film in the world seems to use it for image stills. If you have an Apple TV, then you’ve certainly seen it on the screen saver.