Moving Backgrounds
February 9, 2023We 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.
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.
How so? Let’s look at a few examples I’ve seen floating around.
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.
Show me more!
Chris Coyier has this neat little demo from several years back.
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.
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.
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.
If you peek at the demo’s markup, you’ll notice it’s pretty much what you’d expect. Here’s an abridged version:
<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>
We could probably quibble over the semantics a bit, but that’s not the point. We have a container with a linked-up <div>
for the background image and another <div>
to hold the content.
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;
}
Not bad, right? We give the container some dimensions and set a background image on it that doesn’t repeat and is positioned by its bottom-left edge.
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:
let container = document.querySelector(".container");
container.addEventListener("mousemove", function(e) {
// Our function
}
);
From here, we can use the container’s offsetX
and offsetY
properties. But we won’t use these values directly, as the value for the X coordinate is smaller than what we need, and the Y coordinate is larger. We will have to play around a bit to find a constant that we can use as a multiplier.
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.
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";
}
);
Lastly, we can also reset the background positions back to the original if the user leaves the image container.
container.addEventListener("mouseleave", function() {
container.style.backgroundPosition = "0px 0px";
}
);
Since we’re on CSS-Tricks, I’ll offer that we could have done a much cheaper version of this with a little hover transition in vanilla CSS:
Paint a bigger picture
No doubt you’ve been to some online clothing store or whatever and encountered the ol’ zoom-on-hover feature.
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.
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!
<div class="container"></div>
In the CSS, we will create width
and height
variables to store the dimensions of the of the magnifier glass itself. Then we’ll give that .container
some shape and a background-image
:
: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;
}
There are some things we already know about the magnifier before we even see it, and we can define those styles up-front, specifically the previously defined variables for the .maginifier
‘s width
and height
:
.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;
}
It’s an absolutely-positioned little square that uses the same background image file as the .container
. Do note that the calc function is solely used here to convert the unit-less value in the variable to pixels. Feel free to arrange that however you see fit as far as eliminating repetition in your code.
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.
// 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;
What we need is basically a mousemove
event listener on the .container
. Then, we will use the event.pageX
or event.pageY
property to get the X or Y coordinate of the mouse. But to get the exact relative position of the mouse on an element, we need to subtract the position of the parent element from the mouse position we get from the JavaScript above. A “simple” way to do this is to use getBoundingClientRect()
, which returns the size of an element and its position relative to the viewport.
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.
Let’s go ahead and build the magnifier
when we hear a mousemove
event on the .container
:
// create the magnifier
let magnifier = document.createElement("div");
container.append(magnifier);
Now we need to make sure it has a class name we can scope to the CSS:
// run the function on `mousemove`
container.addEventListener("mousemove", (e) => {
magnifier.setAttribute("class", "magnifier");
}
The example video I showed earlier positions the magnifier outside of the container. We’re gonna keep this simple and overlay it on top of the container instead as the mouse moves. We will use if
statements to set the magnifier’s position only if the X and Y values are greater or equal to zero, and less than the container’s width or height. That should keep it in bounds. Just be sure to subtract the width and height of the magnifier from the X and Y values.
// 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";
}
});
Last, but certainly not least… we need to play with the magnifier’s background image a bit. The whole point is that the user gets a BIGGER view of the background image based on where the hover is taking place. So, let’s define a magnifier we can use to scale things up. Then we’ll define variables for the background image’s width and height so we have something to base that scale on, and set all of those values on the .magnifier
styles:
// Magnifier image configurations
let magnify = 2;
let imgWidth = 500;
let imgHeight = 400;
magnifier.style.backgroundSize = imgWidth * magnify + "px " + imgHeight * magnify + "px";
Let’s take the X and Y coordinates of the magnifier’s image and apply them to the .magnifier
element’s background-position
. As before with the magnifier position, we need to subtract the width and height of the magnifier from the X and Y values using the CSS variables.
// 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";
}
Tada!
Make it cinematic
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.
There are plenty of examples over at CodePen if you wanna get a better idea.
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.
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.
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!
Bonus: Make it immersive
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!
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.
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!