10

I am trying to scale a div, but keep the inside element at the same position and the same size. To do that, I use transform: scale(value) on wrapper and transform: scale(1/value) on the inside div.

The problem is, that the inside div shifts when I change scale. That only happens if width/height of wrapper is odd or not whole. It does not happen for even widths/height of the wrapper.

My goal is to have many child elements of wrapper that scale alongside wrapper, but only one that does not.

Take a look at this example to see problem in action (hover to scale).

Example with no issue, inner element stay fixed on scale (height and width of container are even):

https://jsfiddle.net/o16rau6u/5/

.wrapper {
  width: 200px;
  height: 200px;
  background-color: blue;
  position: relative;
}

.bg {
  width: 20px;
  height: 20px;
  display: inline-block;
  position: absolute;
  top: 50%;
  left: 50%;
  margin-top: -10px;
  margin-left: -10px;
  background-image: url("https://upload.wikimedia.org/wikipedia/commons/thumb/f/f9/Wiktionary_small.svg/350px-Wiktionary_small.svg.png");
  background-size: 100% 100%;
  background-repeat: no-repeat;
}

.wrapper:hover {
  transform: scale(2);
}

.wrapper:hover .bg {
  transform: scale(0.5);
}
<div id="wrapper" class="wrapper">
  <div id="bg" class="bg"></div>
</div>

Example with issue, the inner element move a little on scale (height and width of container are odd):

https://jsfiddle.net/o16rau6u/6/

.wrapper {
  width: 201px;
  height: 201px;
  background-color: blue;
  position: relative;
}

.bg {
  width: 20px;
  height: 20px;
  display: inline-block;
  position: absolute;
  top: 50%;
  left: 50%;
  margin-top: -10px;
  margin-left: -10px;
  background-image: url("https://upload.wikimedia.org/wikipedia/commons/thumb/f/f9/Wiktionary_small.svg/350px-Wiktionary_small.svg.png");
  background-size: 100% 100%;
  background-repeat: no-repeat;
}

.wrapper:hover {
  transform: scale(2);
}

.wrapper:hover .bg {
  transform: scale(0.5);
}
<div id="wrapper" class="wrapper">
  <div id="bg" class="bg"></div>
</div>

How can I fix this issue and avoid my elements to move on scale whataver the size of container is ?

PS : The example used above is a very simplified example to show the issue and it's not the needed output or the code used. So we are not looking for another way to achieve the same behavior above as it's pretty easy to be done.

Temani Afif
  • 245,468
  • 26
  • 309
  • 415
sanjihan
  • 5,592
  • 11
  • 54
  • 119
  • i added more explanation to the question and working snippet, hope it get more attention ;) and as you can see in my answer, it seems to be a bug related to the calculation of scale or something else am not able to see :) hope you will get more answers – Temani Afif Jan 01 '18 at 10:36
  • it seems you won't get more answer :( – Temani Afif Jan 07 '18 at 09:08
  • Bounty usually gets the most attention right before it expires :) there's hope :D – sanjihan Jan 07 '18 at 18:27
  • Just use a class to scale certain things up on hover. Upscaling AND downscaling at the same time is absolutely not needed. You are doing it wrong ;-). – Mr. Hugo Jan 08 '18 at 09:07
  • well i guess we will end up by saying it's a bug ... too bad i can't give my answer the bounty :p so it will go to one of the others – Temani Afif Jan 09 '18 at 11:21

3 Answers3

7

At the start I thought this is related to the calculation done by the browser and some rounding but it's seems to be bug. I have done a lot of test and whataver the value of the scale I use it always fail on odd value.

Here is a simple example with only scaleX

body:after {
  content: "";
  position: absolute;
  z-index: 999;
  top: 0;
  bottom: -200%;
  width: 2px;
  right: 50%;
  margin-right: -1px;
  background: rgba(0, 0, 0, 0.5);
}

.box {
  width: 200px;
  height: 100px;
  margin: 50px auto;
  background: blue;
  position: relative;
}

.inner {
  height: 20px;
  width: 20px;
  background: red;
  position: absolute;
  top: 50%;
  left: 50%;
  margin-left: -10px;
  text-align: center;
  color: #fff;
  margin-top: -10px;
}
<div class="box">
  <div class="inner">A</div>
</div>

<div class="box" style="transform:scaleX(2)">
  <div class="inner" style="transform:scaleX(0.5)">A</div>
</div>

<div class="box" style="width:201px;transform:scaleX(2)">
  <div class="inner" style="transform:scaleX(0.5)">A</div>
</div>

As you can see below, the browser seems to add an extra pixel to inner div, but if you look more closely the inner div has a correct size but it's being translated by 1px to the right. So the hover block of Dev Tools is positioned correctly but not element itself! So it seems that the browser correctly calculated the position but did a wrong painting.

enter image description here

The same issue appear if we simply apply scale on the container. So it's not because the scale of inner element:

body:after {
  content: "";
  position: absolute;
  z-index: 999;
  top: 0;
  bottom: -200%;
  width: 2px;
  right: 50%;
  margin-right: -1px;
  background: rgba(0, 0, 0, 0.5);
}

.box {
  width: 200px;
  height: 100px;
  margin: 50px auto;
  background: blue;
  position: relative;
}

.inner {
  height: 20px;
  width: 20px;
  background: red;
  position: absolute;
  top: 50%;
  left: 50%;
  margin-left: -10px;
  text-align: center;
  color: #fff;
  margin-top: -10px;
}
<div class="box" style="transform:scaleX(2)">
  <div class="inner">A</div>
</div>

<div class="box" style="width:201px;transform:scaleX(2)">
  <div class="inner">A</div>
</div>

enter image description here


Even if we use floating value with scale where we can say there is some rouding and complex calculation, we have correct output with even values and issue with odd values:

Example with scale(1.25) & scale(1/1.25):

body:after {
  content: "";
  position: absolute;
  z-index: 999;
  top: 0;
  bottom: -200%;
  width: 2px;
  right: 50%;
  margin-right: -1px;
  background: rgba(0, 0, 0, 0.5);
}

.box {
  width: 200px;
  height: 100px;
  margin: 50px auto;
  background: blue;
  position: relative;
}

.inner {
  height: 20px;
  width: 20px;
  background: red;
  position: absolute;
  top: 50%;
  left: 50%;
  margin-left: -10px;
  text-align: center;
  color: #fff;
  margin-top: -10px;
}
<div class="box">
  <div class="inner">A</div>
</div>

<div class="box" style="transform:scaleX(1.25)">
  <div class="inner" style="transform:scaleX(0.8)">A</div>
</div>

<div class="box" style="width:201px;transform:scaleX(1.25)">
  <div class="inner" style="transform:scaleX(0.8)">A</div>
</div>

Example with scale(1.33) & scale(1/1.33):

body:after {
  content: "";
  position: absolute;
  z-index: 999;
  top: 0;
  bottom: -200%;
  width: 2px;
  right: 50%;
  margin-right: -1px;
  background: rgba(0, 0, 0, 0.5);
}

.box {
  width: 200px;
  height: 100px;
  margin: 50px auto;
  background: blue;
  position: relative;
}

.inner {
  height: 20px;
  width: 20px;
  background: red;
  position: absolute;
  top: 50%;
  left: 50%;
  margin-left: -10px;
  text-align: center;
  color: #fff;
  margin-top: -10px;
}
<div class="box">
  <div class="inner">A</div>
</div>

<div class="box" style="transform:scaleX(1.33)">
  <div class="inner" style="transform:scaleX(calc(1 / 1.33))">A</div>
</div>

<div class="box" style="width:201px;transform:scaleX(1.33)">
  <div class="inner" style="transform:scaleX(calc(1 / 1.33))">A</div>
</div>
Temani Afif
  • 245,468
  • 26
  • 309
  • 415
  • Yes, you're right. it's a painting issue, the Chrome has such effect, except the FireFox browser. But the getBoundingClientRect returns correct values as the DevTools show. – Avirtum Jun 09 '21 at 11:22
3

Just don't put one of these divs into another, instead put both of them into the third div like this:

.wrapper {
  width: 201px;
  height: 201px;
  position: relative;
}

.div-1 {
  width: 100%;
  height: 100%;
  background-color: blue;
}

.div-1:hover {
  transform: scale(2);
}

.div-2 {
  width: 20px;
  height: 20px;
  display: inline-block;
  position: absolute;
  top: 50%;
  left: 50%;
  margin-top: -10px;
  margin-left: -10px;
  background-image: url("https://upload.wikimedia.org/wikipedia/commons/thumb/f/f9/Wiktionary_small.svg/350px-Wiktionary_small.svg.png");
  background-size: 100% 100%;
  background-repeat: no-repeat;
}
<div class="wrapper">
  <div class="div-1"></div>
  <div class="div-2"></div>
</div>

This way you just wont be needed to scale the inner div back to it's original height and width.

Cheslab
  • 1,896
  • 2
  • 17
  • 27
  • 2
    this code is part ofa bigger Angular program. What is being scaled, is the component it self. The children elements are inside component and are part of the logic needed. I cannot put them outside of component. Therefore I cannot utilize your code. But still thanks. – sanjihan Dec 31 '17 at 17:08
  • 2
    @sanjihan do you really need to scale the wrapper? If it would have a solid color background in real application, may be an outline would do? Say, `outline: solid blue 100px;` instead of `transform: scale(2);` (with your original html code)? – Cheslab Dec 31 '17 at 17:25
0

Browsers are notoriously bad at calculating stuff. There was a time when web developer math stated that (in some browsers) 33.33% times 3 was larger than 100% (but that was 14 years ago). Things have gotten much better since then, but don't rely on it. Doing resize tricks like this is not the way to go.

It seems to me that you want to resize the wrapper, while keeping the background size the same. To do so, you are using a complex transform trick, which (unprefixed) excludes 17% of all internet users. That is improper browser support and another reason not to do this.

This effect can be easily achieved with 99.99% browser support, working on all sizes.

.wrapper {
  width: 402px;
  height: 402px;
  background-color: blue;
  position: relative;
}

.bg {
  width: 20px;
  height: 20px;
  display: block;
  position: absolute;
  top: 201px;
  left: 201px;
  margin-top: -10px;
  margin-left: -10px;
  background-image: url("https://upload.wikimedia.org/wikipedia/commons/thumb/f/f9/Wiktionary_small.svg/350px-Wiktionary_small.svg.png");
  background-size: 100% 100%;
  background-repeat: no-repeat;
}

.wrapper:hover {
  width: 4020px;
  height: 4020px;
}
<div id="wrapper" class="wrapper">
  <div id="bg" class="bg"></div>
</div>

If you want it to be responsive (you do!), this should do the trick:

* {padding: 0; margin: 0;}
html, body {height: 100%;}

.wrapper {
  width: 50vw;
  background-color: blue;
  position: relative;
  padding-bottom: 50%;
}

.bg {
  width: 20px;
  height: 20px;
  display: block;
  position: absolute;
  top: 25vw;
  left: 25vw;
  margin-top: -10px;
  margin-left: -10px;
  background-image: url("https://upload.wikimedia.org/wikipedia/commons/thumb/f/f9/Wiktionary_small.svg/350px-Wiktionary_small.svg.png");
  background-size: 100% 100%;
  background-repeat: no-repeat;
}

.wrapper:hover {
  width: 500vw;
  padding-bottom: 500%;
}
<div id="wrapper" class="wrapper">
  <div id="bg" class="bg"></div>
</div>
Mr. Hugo
  • 11,887
  • 3
  • 42
  • 60
  • Another alternative is simple in this case but the intention of the question is NOT to have an alternative way, but to explain the behavior and provide a fix for this scale issue (if there is one of course) or to highlight a potential bug... the example was very simplifed in order to show the issue. you think we are going to bother ourself with a complex scale transformation when having a small image and a background ? of course not, It's used with a more complex code and it's needed in this code. – Temani Afif Jan 08 '18 at 08:43
  • and i also though about calculation but as you can notice you may multiple by 2 and then divide by two where there is no need to round or any decimal stuffs ... it's simply work with even value and not odd value. You may check my more simplified example : https://stackoverflow.com/a/48048934/8620333 – Temani Afif Jan 08 '18 at 08:45
  • I get your point, but 'no need for rounding' and 'no rounding' are two different things. I tend to handle CSS calculations the same way I handle [SQL floats](https://stackoverflow.com/a/18701865/2397550). I think not relying on them is the common approach (for more than 10 years). I like your question, but the result does not surprise me. – Mr. Hugo Jan 08 '18 at 09:02
  • it doesn't surpire you but it does suprise me :) ... if it wasn't working at all i would not be surprised, if i always have the same result i would not be surprised ... but if for a value of `200px` it works fine and not for `201px`, then i got surprised, especially when the calculation is pretty easy ... we multiply everything by 2 and then we divide some by 2 (so in all the case it's divisible by 2). So for me it's strange since now browser handle very well rotation, 3D perspective, skew, etc – Temani Afif Jan 08 '18 at 09:11
  • True.... good point. I am interested in the outcome. Seems like ANOTHER silly calculation bug in CSS, which is the point I am trying to make: this is not the first time super simple CSS calculations fail. – Mr. Hugo Jan 08 '18 at 09:15
  • since we got no more relevant answer and since i cannot award my bounty to my answer, you will get it :) ... and i think this will end up with a bug, as my answer got many upvotes so i guess no one find a clue for it ;) By the way i updated my answer with better explanation .. and it's seems a bug in the paiting process as the calculation seems to be ok. – Temani Afif Jan 09 '18 at 21:58
  • Thank you. You could file the bug at the [chromium bug tracker](https://bugs.chromium.org/p/chromium/issues/list) with a reference to this tread. Maybe they can find the answer. – Mr. Hugo Jan 09 '18 at 22:36