Focusing a `background-image` on a Precise Location with Percentages
Let’s say you have an element with a
background-image, where only part of the image is visible, because the image is bigger than the element itself. The rest is cropped away, outside the element.
Now you want to move that
background-image such that you’re focusing the center of the element on a specific point in it. You also want to do that with percentage values rather than pixels. We’re going to have to get clever.
This is going to involve some math.
Let’s use this as our image, which has markers for sizing:
And here’s our element, with that
background-image applied. Notice we can only see the (top) left of the image:
Now let’s say we want to align a specific point on the image with the center of that element. Say, the point at 300px in from the left.
Since we’re asking for this position in pixels, it’s straightforward. With no position defined, the
background-image “starts” with the point at 100 pixels at the center, so you need to move it to the left by 200 pixels:
Let’s formalize it.
x value you’re using for
background-position is calculated like so:
(0.5 × [viewport width]) - [x-coordinate] 0.5 × 200px - 250px 100px - 250px = -150px
It takes a second to figure out, but it’s nothing too taxing. You could have probably figured that out intuitively without needing to use a formula.
But what if you wanted to (or had to) express
background-position as a percentage? Shouldn’t be too hard, right? Let’s try using a percentage to get ourselves centered at 300px again. We had a
-200px, so let’s convert that to percent: -200 / 800 = -25%, so:
Hm. That didn’t work at all. Maybe we need to use a positive value?
That’s better, but it’s centered at, like… 250px? How about as a percentage of the viewport width: 300 / 200 = 150%. That can’t be right…
Yeah, that’s not right.
Let’s back up. What happens if we do this?
That feels like it kind of makes sense;
background-position: 100% 0; makes the
background-image flush-right and centered at 700px, or 7/8 the width of the image. But what if we wanted to center it at 100%? I guess we’d have to do… 9/8?
At this point, I’m not surprised that didn’t work.
This doesn’t feel like the right path. Let’s back up.
What does the spec say?
For example, with a value pair of ‘0% 0%’, the upper left corner of the image is aligned with the upper left corner of, usually, the box’s padding edge. A value pair of ‘100% 100%’ places the lower right corner of the image in the lower right corner of the area. With a value pair of ‘75% 50%’, the point 75% across and 50% down the image is to be placed at the point 75% across and 50% down the area.
Maybe we can reverse-engineer this.
On that last one, 112.5%, it was aligning the point at 112.5% across the
background-image with the point at 112.5% across the bounding box. That kind makes sense. The spec seems written to make it easy for only three values: 0%, 50%, and 100%. Any other value isn’t so intuitive.
background-position: 0;, we were focused on 100px, or 12.5%. With
background-position: 100% 0;, we were focused on 700px, or 87.5%. How does
background-position: 50% 0; look, exactly?
50% is kind of like our “anchor” here; it’s the point at which our desired focal point and the corresponding
background-position values are equal.
Let’s pretend we want to focus on 700px or 87.5%. We go 100% of the way from the center: 50% + 50%.
background-position set to
100%, the center of our bonding box has “panned” from the center of the image, 3/4 of the way to the rightmost edge (from 400px to 700px). If we want to “pan” to the rightmost edge, we need to go that extra 1/4, or 200px. 1/4 is 1/3 of 3/4, so we need to go a third more than we did a moment ago, or a total of 66.667% from the center:
Whew! So to focus on the rightmost edge of a
background-image that is 4 times the size of our viewport, we need to set
background-position: 116.667% 0;.
How the heck are we supposed to figure that out?
It’s a difference of 16.667% from the 100% we might expect. So if we wanted to focus on our original goal of 300px (or, 37.5%), we’d, uh, add 16.667%? There’s no way this is going to work:
If we wanted to focus on the leftmost edge, we’d probably subtract 16.667% from 0%, right? That sounds like it could be right.
To focus at 100% or 0%, you have to “overshoot” those values by a certain amount, when measured from the center.
So if we want to focus on 0%, or “100% of the way to the left of the center,” we have to subtract 66.667% from 50%. If we want to focus on 100%, or “100% of the way to the right of the center,” we have to add 66.667% to 50%.
I might have expected to have to add or subtract 50% to or from 50% to get to those edges: a 1:1 ratio of “how far I want to go from the center” to “what my
background-position value should be.” But instead, we have to use a 4:3 ratio. In other words, we have to use a value four-thirds more “away from the center.”
Things are getting a little hairy here, so let’s introduce some terms:
- c: Desired focal point (in percent) from leftmost edge of background image
- z: Zoom factor (background width ÷ viewport width)
background-position, to focus on c, given z
So we take a focal point’s distance from the center (c − 50), multiply it by 4/3, then add that result to “the center,” or 50.
If you wanted to focus on the point at 600px (or 75%), my
background-position value should be:
(75% − 50%) × 4/3 + 50% = 83.333%
Yes, that sounds like it could work! Please please please:
And if you wanted to focus on 200px, or 25%, you would do:
(25% − 50%) × 4/3 + 50% = 16.667%
Let’s generalize this:
(c − 50%) × 4/3 + 50% = p
So why 4/3? 4 is the ratio of our
background-image width to our viewport width; and 3 is… 1 less than 4. Could it be that simple? Let’s try a larger
background-image, this time 1000px wide, or 5 times the width of our viewport. And let’s again try to focus on the point at 200px. Here our equation would be:
(20% − 50%) × 5/4 + 50% = 12.5%
Oh my god. It works!
So to revisit our equation, with a variable background-to-viewport ratio:
(c − 50%) × z/(z − 1) + 50% = p
Let’s turn this into English:
Given a point on a
background-imageat location c…
- with c expressed as a percentage of the width of the image
- where c is intended to lie in the horizontal center of the background image’s bounding box
- and where the
background-imageis z times as wide as its bounding box
background-positionp (expressed as a percentage) is given by the following formula:
(c − 50%) × z/(z − 1) + 50% = p
Can we generalize this even more?
What if we wanted to align the point at 25% of the
background-image with the point at 75% of the bounding box? Yikes!
Let’s revisit our original formula:
(c − 50%) × z/(z − 1) + 50% = p
Now let’s introduce some new terms:
- b: Desired focal point (in percent) from the leftmost edge of the bounding box. Earlier we had assumed this to always be 50% so that the center of the bounding box would be focused on our target in the
background-imagefocal point (in percent) to align to bounding box’s midpoint in order to get c to align to b in the viewport; if d of the
background-imagealigns with 50% of the viewport, then c of the
background-imagewill align with b of the viewport.
Let’s think of it this way
To want to align position c of a
background-image with position b of a bounding box is to want to align some other position, d, of a
background-image with the center of the bounding box – and we already know how to do that. So can we figure out a way to derive d, the spot we need to be at 50%, from c, b, and z? Sure!
With our 800px wide
background-image, in a 200px-wide bounding box (z = 4), if we want to focus the rightmost edge of the bounding box (b = 100%) on the position at 600px (c = 75%) in the image, we would want the center of the bounding box to be focused on the point at 500px (d = 62.5%).
How do we get from c (75%) to d (62.5%)? Where does that -12.5% difference come from?
Well, our b was 100%, 50% greater than our old “default” b of 50%. And 12.5% is 1/4 of that; 1/4 is the inverse of our z of 4. Is that where our d comes from? That would be:
d = c + (50% - b)/z
Looks promising. Now we can substitute d in for c in the original formula:
(d − 50%) × z/(z − 1) + 50% = p
(c + (50% − b)/z - 50%) × z/(z − 1) + 50% = p
Whew! Let’s test this. Let’s try to align the position at 25% in our
background-image (200px) with the position at 75% in our bounding box. This would be:
p = (25% + (50% - 75%)/4 - 50%) × 4/(4 - 1) + 50% p = -31.25% × 1.333 + 50% p = 8.333%
Unbelievable! Let’s double check. How about the point at 87.5% in our
background-image (700px) aligned with the position at 33.333% in our bounding box:
p = (87.5% + (50% - 33.333%)/4 - 50%) × 4/(4 - 1) + 50% p = 41.6667% × 1.333 + 50% p = 105.555%
Looks good enough to me!
I’m sure there is something intuitive about this to certain types of people, but I am not one of those people.
Let’s build a Sass function that will do all this ridiculous math for us.
See the Pen Background Focus: percentage (final Sass function) by Jay (@jsit) on CodePen.
My head is spinning.
When I began tackling this problem I did not expect it to be this difficult, but what a journey. I hope guiding you through my thought process has been enlightening, and that you may at some point find value in our little Sass function.
All the Pens embedded in this article can be found in this collection.