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:
See the Pen Background Focus: no position by Jay (@jsit) on CodePen.
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:
See the Pen Background Focus: pixel position by Jay (@jsit) on CodePen.
Let’s formalize it.
The x
value you’re using for background-position-x
is calculated like so:
(0.5 × [bounding box width]) - [x-coordinate]
0.5 × 200px - 300px
100px - 300px = -200px
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-x
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 background-position-x
of -200px
, so let’s convert that to percent: -200 / 800 = -25%, so:
See the Pen Background Focus: percentage (1st attempt) by Jay (@jsit) on CodePen.
Hm. That didn’t work at all. Maybe we need to use a positive value?
See the Pen Background Focus: percentage (2nd attempt) by Jay (@jsit) on CodePen.
That’s better, but it’s centered at, like… 250px? How about as a percentage of the bounding box width: 300 / 200 = 150%. That can’t be right…
See the Pen Background Focus: percentage (3rd attempt) by Jay (@jsit) on CodePen.
Yeah, that’s not right.
Let’s back up. What happens if we do this?
See the Pen Background Focus: percentage (4th attempt) by Jay (@jsit) on CodePen.
That feels like it kind of makes sense; background-position-x: 100%;
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?
See the Pen Background Focus: percentage (5th attempt) by Jay (@jsit) on CodePen.
At this point, I’m not surprised that didn’t work.
This doesn’t feel like the right path. Let’s back up.
the spec say?
What doesFor 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.
With background-position-x: 0;
, we were focused on 100px, or 12.5%. With background-position-x: 100%;
, we were focused on 700px, or 87.5%. How does background-position-x: 50%;
look, exactly?
See the Pen Background Focus: percentage (6th attempt) by Jay (@jsit) on CodePen.
50% is kind of like our “anchor” here; it’s the point at which our desired focal point and the corresponding background-position-x
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%.
See the Pen Background Focus: percentage (4th attempt) by Jay (@jsit) on CodePen.
With background-position-x
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:
See the Pen Background Focus: percentage (7th attempt) by Jay (@jsit) on CodePen.
Whew! So to focus on the rightmost edge of a background-image
that is 4 times the size of our bounding box, we need to set background-position-x: 116.667%;
.
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:
See the Pen Background Focus: percentage (8th attempt) by Jay (@jsit) on CodePen.
Nope.
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.
See the Pen Background Focus: percentage (9th attempt) by Jay (@jsit) on CodePen.
Cool!
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-x
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 ÷ bounding box width)
- p:
background-position-x
, 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-x
value should be:
(75% − 50%) × 4/3 + 50% = 83.333%
Yes, that sounds like it could work! Please please please:
See the Pen Background Focus: percentage (10th attempt) by Jay (@jsit) on CodePen.
Awesome!
And if you wanted to focus on 200px, or 25%, you would do:
(25% − 50%) × 4/3 + 50% = 16.667%
See the Pen Background Focus: percentage (11th attempt) by Jay (@jsit) on CodePen.
Wow.
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 bounding box 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 bounding box. And let’s again try to focus on the point at 200px. Here our equation would be:
(20% − 50%) × 5/4 + 50% = 12.5%
See the Pen Background Focus: percentage (12th attempt) by Jay (@jsit) on CodePen.
Oh my god. It works!
So to revisit our equation, with a variable background-to-bounding-box ratio:
(c − 50%) × z/(z − 1) + 50% = p
Let’s turn this into English:
Given a point on a
background-image
at 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-image
is z times as wide as its bounding boxthe correct
background-position-x
p (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-image
. - d:
background-image
focal point (in percent) to align to bounding box’s midpoint in order to get c to align to b in the bounding box; if d of thebackground-image
aligns with 50% of the bounding box, then c of thebackground-image
will align with b of the bounding box.
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
Or:
(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%
See the Pen Background Focus: percentage (13th attempt) by Jay (@jsit) on CodePen.
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%
See the Pen Background Focus: percentage (14th attempt) by Jay (@jsit) on CodePen.
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.
This is pretty nifty. I’m wondering if the four-value syntax (support for offsets from any edge) could help with this at all? https://developer.mozilla.org/en-US/docs/Web/CSS/background-position
background-position: right 3em bottom 10px;
Hm, nice thought, but it doesn’t seem to me like that would help. I haven’t used the four-value syntax, but as far as I can tell it only allows you to specify the bottom or right as alternative origins, but has no benefit for using percentages for the position.
Thanks!
Ok, last time I tried this, I “trialed and errored” for a while (a long one). Your formula is quite effective, actually and make totally sense to me. Cool.
Thanks for the thoughts and the sharing.
Yes, very cool, and thanks for going so far as to SASSify it for us also
I had to work out something similar for myself a few months back. Basically, I needed to figure out what percentages to apply to a background image in order to ensure the portion of the image the user selected previously was indeed being shown.
http://stackoverflow.com/a/35493320/937377
You’ll probably have to read the question and answer to fully understand it, but this is the formula I ended up with for calculating the vertical percentage to give background-image based on the desired y coordinates of the background image and its container:
Hey, nice to see this getting some momentum – been using this for spritemaps for quite some time – works on older browsers, too. You’ll find articles on this reaching back something like 10 years, but the tricky part is including this efficiently in a larger workflow.