7

I have three div with same heights equal to 1vh`.

My problem is they appear with different pixel size on screen. Sometimes they look equal but sometimes not, specially it happens after viewport resizing. Please run the snippet to see.

.samples {
  display:flex;
}

.container{
  display:flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: center;
  margin:10px 20px;
  height: 20vh;
  width:20vh;
}

.container div{
  background: #000;
  width:100%;
}

#set1 div{
  height:.3vh;
}

#set2 div{
  height:.7vh;
}

#set3 div{
  height:.9vh;
}

#set4 div{
  height:1.1vh;
}
<div class = "samples">

  <div class="container" id="set1" >
    <div></div>
    <div></div>
    <div></div>
  </div>

  <div class="container" id="set2" >
    <div></div>
    <div></div>
    <div></div>
  </div>

  <div class="container" id="set3" >
    <div></div>
    <div></div>
    <div></div>
  </div>
  
    <div class="container" id="set4" >
    <div></div>
    <div></div>
    <div></div>
  </div>
  
</div>
<div>
<h4>Above are four samples, each sample includes 3 identical div.</h4>
<h4>But depends on your screen size you may not see what expected</h4>
</div>

I took some screenshot for demonstrating what really happening during resize, as you can see div looks with different height despite all of them have 1vh height.

enter image description here

If I calculate 1vh manually in javascript and inject it as rounded 'px' to CSS then it works perfectly.(In snippet I have used .3, .7, .9, 1.1 vh for demonstration)

  
  const set1Height = Math.round(document.documentElement.clientHeight * .3 / 100);
  const set2Height = Math.round(document.documentElement.clientHeight * .7 / 100);
  const set3Height = Math.round(document.documentElement.clientHeight * .9 / 100);
  const set4Height = Math.round(document.documentElement.clientHeight * 1.1 / 100);
  

  document.documentElement.style.setProperty("--set1-height", `${set1Height}px`);
  document.documentElement.style.setProperty("--set2-height", `${set2Height}px`);
  document.documentElement.style.setProperty("--set3-height", `${set3Height}px`);
  document.documentElement.style.setProperty("--set4-height", `${set4Height}px`);
  
.samples {
  display:flex;
}

.container{
  display:flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: center;
  margin:10px 20px;
  height: 20vh;
  width:20vh;
}

.container div{
  background: #000;
  width:100%;
}

#set1 div{
  height: var(--set1-height);
}

#set2 div{
  height: var(--set2-height);
}

#set3 div{
  height: var(--set3-height);
}

#set4 div{
  height: var(--set4-height);
}
<div class = "samples">

  <div class="container" id="set1" >
    <div></div>
    <div></div>
    <div></div>
  </div>

  <div class="container" id="set2" >
    <div></div>
    <div></div>
    <div></div>
  </div>

  <div class="container" id="set3" >
    <div></div>
    <div></div>
    <div></div>
  </div>
  
    <div class="container" id="set4" >
    <div></div>
    <div></div>
    <div></div>
  </div>
  
</div>
<div>
<h4>Here, after calculating heights and round them to `px` they looks identical</h4>
</div>

My first thought was rounding floating point numbers cause this but if this is the case why CSS rounds a single float number to three different numbers?

My question is why this happening and is there any pure CSS solution to safely use viewport sizes and make sure they always appear as expected?

Makan
  • 641
  • 8
  • 13
  • Good question.In `#set3`, I tried `flex: 0 1 .9vh;` instead of `height: .9vh;` but it yielded the same result. (`flex` is the shorthand property for `flex-grow`, `flex-shrink`, `flex-basis`). – Rounin May 01 '20 at 08:43
  • 1
    Thanks @Rounin I tried without `flex` as well and got same result. Actually, I have used some extra css like `flex` to just show what happening. – Makan May 01 '20 at 08:50

4 Answers4

7

The problem you're running into here isn't in the CSS layer but is lower level - in the browser rendering engine, and to some extent the physical display hardware.

why CSS rounds a single float number to three different numbers?

This isn't right - the CSS is behaving correctly and is producing exactly the same height value for all three divs in each set. You can verify this by inspecting the elements in dev tools and looking at the calculated height property for each div.

However, that value isn't in whole pixels, it's a fraction of a pixel. And you've hit a classic problem in computer graphics - how do you display fractions of a pixel on a display with discrete pixels?

Short answer -- you can't, perfectly. That's why these three fractional pixel height divs ultimately render at what looks like different heights as you resize them and even move them around on screen. It's the rendering engine doing its best to make them look identical, with varying results.

The reason your manual 'rounding' solution in JS works is because, well, it rounds those heights to whole pixel values! When the pixel values are round, it's much easier for everything to line up perfectly (in the naive case, assuming there isn't fractional pixel padding, margin etc between the divs) and you are less likely to see differences.

Try it! If you remove the Math.round or even simply add 0.75 or something to the rounded result, you'll see the exact same issue with your JS solution, even though all the calculated values are obviously identical.

Again, the problem isn't that the values are different, it's that they're rendered differently by your browser / hardware. The only universally 'safe' workaround is to ensure that the values are in fact round pixel values.

Leading onto this question:

is there any pure CSS solution to safely use viewport sizes

In terms of the above, getting rounded pixel value results, unfortunately no. There is no way with CSS calc() or otherwise to round results dynamically.

As a general suggestion, if you need precise rendering down to the pixel for thin lines etc, responsive CSS probably isn't the tool you want to use. You probably want to stick with static pixel values, but if you really need things to be dynamic and grow and shrink as you change the viewport size, then yes probably you'll want to involve JS for greater control of the rendering.

davnicwil
  • 28,487
  • 16
  • 107
  • 123
  • +1 for the source of the problem and the clear explanation. But just some precision about this bit: _"your manual 'rounding' solution in JS works [...] When the pixel values are round, there is no rendering issue any more"_. That's not always true. The example provided works on my Full HD monitor, but even though CSS pixel values are rounded, I still reproduce the issue on my 4K monitor with scaling enabled. Pixel ratio also comes at play – blex May 01 '20 at 09:18
  • 1
    @blex good shout, you are right of course! It's 'less likely' rather than 'no' - have edited to clarify that nuance. – davnicwil May 01 '20 at 09:23
  • 1
    @davnicwil Thanks for clear explanation. It covers all parts of my question. – Makan May 01 '20 at 09:53
2

For me it looks like the following. The result differs by 1px. That reason is because the calculated height of a div is not an integer, it's a float with subpixel values. And a screen cannot display subpixels. So it depends on the position on screen if the browser decide to round to lower or higher value.

enter image description here

Brain Foo Long
  • 2,057
  • 23
  • 28
  • Thanks @Brain Foo Long In your screen they seems fine now, but if you could resize the snippet viewport then they start to change differently. I have used those samples with fractional numbers just to show what happening(However I failed with regard to your screen!). In reality I am using `1vh` (not float numbers) to build a hamburger menu, then sometimes depends on viewport size the menu lines thickness differ from each others. – Makan May 01 '20 at 09:21
1

I solved this by using 'px' in stead of 'vh'

.container div{
  background: #000;
  width:100%;
  height:6px;
}

Since, 'vh' is Relative to the viewport’s height, so it may fluctuate when resizing the viewport.

• 'px' is an absolute unit; but 'vh' is a relative unit.

Partha Paul
  • 189
  • 4
  • I think adding min-height in pixels would also fix the problem and still he can keep vh units. – skobaljic Mar 10 '20 at 11:36
  • By using 'px' instead of 'vh' it will work, but it is not relative to screen size. I can use javascript to make it relative, I edited my question to show how it is possible, but is not seems pretty solution. – Makan Mar 10 '20 at 13:38
  • 1
    vh is relative to screen size – Sagar May 01 '20 at 08:40
0

its weird but it always works for me use vw also for height and width. and also try min-height

arslan
  • 1,064
  • 10
  • 19