Search code examples
javascriptcssmarginviewportcss-variables

Responsive float box loses bottom margin in mobile view, FF and Chrome passing viewport values differently into CSS, and other confounding mysteries


I put together a page that has a .floatbox, which sets the top and bottom margin based on the variable --currentViewportHeight (basically, I dynamically detect it with JS and insert it into my CSS, on page load and resize); --autoTopBottomMargin is set arbitrarily to 0.1 because this is how big I want the top and bottom margins to be. The following class allows me to have very nice, dynamic top and bottom margins.

.floatbox {
  position: relative;
  width: 500px;
  left: 50%;
  transform: translate(-50%);
  margin-top: calc(var(--currentViewportHeight) * 1px * var(--autoTopBottomMargin));
  margin-bottom: calc(var(--currentViewportHeight) * 1px * var(--autoTopBottomMargin));
  padding-top: 20px;
}

Then, when the size of the viewport becomes less than the the size of .floatbox (with a little wiggle room—600px, to be precise), I go into the responsive spec. 0.00176 determines the perfect relative size and right-hand margin in one go.

@media only screen and (max-width: 600px) {
  .floatbox {
    position: absolute;
    transform-origin: left top;      
    transform: scale(calc(var(--currentViewportWidth) * 0.00176));
    top: calc(var(--currentViewportWidth) * 1px * var(--repositionFactor));
    left: calc(var(--currentViewportWidth) * 1px * var(--repositionFactor));
    margin-top: unset;
  }
}

Here, I use --currentViewportWidth (also detected with JS and inserted dynamically into my CSS) to perfectly scale .floatbox, so it's sized exactly right and with the exactly right margins on any device.

Seemingly, everything is just fine...except, no matter what I do, I cannot set margin-bottom for the responsive spec, by using either static or dynamic values. On mobile, it just sticks to the bottom of the screen.

Update: The plot thickens. It appears that my calculation of --currentViewportWidth doesn't pass the same value in Firefox (where I've been testing mainly) and Chrome. My function is exceedingly simple, and yet the script seems to pass wildly different values into the CSS.

function detectViewport() {
  // Calculate the viewport width and pass it into JS and CSS variables
  let viewportWidth = window.innerWidth;
  document.querySelector(':root').style.setProperty('--currentViewportWidth', viewportWidth);
  
  // Calculate the viewport height and pass it into JS and CSS variables
  let viewportHeight = window.innerHeight;
  document.querySelector(':root').style.setProperty('--currentViewportHeight', viewportHeight);
}

Solution

  • OK, I have finally figured it out. While I still have no idea why FF and Chrome report different values, I did figure out a couple other things, which allowed me to solve my issue.

    1. My JavaScript was eating my decimal points... Just for background information, I have this in the main part of my stylesheet:
    :root {
      /* MOBILE: Initial value for variable updated at onLoad() and onResize() events
         for transform:scale(calc()), top:calc(), and left:calc() operations
         on .floatbox. */
      --currentViewportWidth: 1;
    
      /* MOBILE: Magic number for top:calc() and left:calc() operations on .floatbox */
      --repositionFactor: 0.05;
    
      /* DESKTOP AND MOBILE: Initial value for variable updated at onLoad() and onResize()
         events for height:calc() operations for .floatbox-container and for
         margin-top:calc() operations on .floatbox. */
      --currentViewportHeight: 1;
    
      /* DESKTOP AND MOBILE: Initial value for height:calc() operations on
         .floatbox-container */
      --currentFloatboxHeight: 1;
    
      /* DESKTOP: Magic number for height:calc() operations for .floatbox-container
        and for margin-top:calc() operations for .floatbox. */
      --autoTopBottomMargin: 0.1;
    }
    

    While I haven't looked into whether window.innerWidth or window.innerHeight also cause this issue, .offsetHeight, .clientHeight, and .scrollHeight all definitely did some suspicious rounding when I began to apply them to the .floatbox class. After hours of experimentation, I figured out how to get the height with all decimal digits intact and then keep just two digits (I believe this is all the precision browsers care about). This is how I did it:

      // Calculate the viewport height and pass it into JS and CSS variables.
      // Preserve all decimal digits and then round them to two places.
      let floatboxHeight = document.querySelector('.floatbox').getBoundingClientRect().height.toFixed(2);
      document.querySelector(':root').style.setProperty('--currentFloatboxHeight', floatboxHeight);
    
    1. Now that my variable --currentFloatboxHeight actually expressed the size of the .floatbox class, I was free to do stuff with it. For mobile (tl;dr, I have some transform: scale(calc() bits that take care of this elsewhere), I just had to unset the top and bottom margins and add the number that worked for my use case within a media query.
      .floatbox-container {
        height: calc(var(--currentFloatboxHeight) * 1px + 35px);
      }
      
      .floatbox {
        position: absolute;
        transform-origin: left top;      
        transform: scale(calc(var(--currentViewportWidth) * 0.00176));
        top: calc(var(--currentViewportWidth) * 1px * var(--repositionFactor));
        left: calc(var(--currentViewportWidth) * 1px * var(--repositionFactor));
        margin-top: unset;
        margin-bottom: unset;
      }
    
    1. For reasons I still don't understand (here, FF was perfectly happy but desktop Chrome was behaving like mobile Chrome—both v.114—with .floatbox still sticking to the bottom of the page); for reasons too complex to explain here, floatbox is positioned relatively for desktop but absolutely for mobile—but this shouldn't have mattered because FF displayed what it should have. In the end, I opted to make a "universal" desktop .floatbox-container class, unset the bottom margin in the desktop CSS and moved its calculation into the height for .floatbox-container. This is what I ended up with.
    .floatbox-container {
      height: calc(var(--currentFloatboxHeight) * 1px + var(--currentViewportHeight) * 1px * var(--autoTopBottomMargin));
    }
    
    .floatbox {
      position: relative;
      width: 500px;
      left: 50%;
      transform: translate(-50%);
      margin-top: calc(var(--currentViewportHeight) * 1px * var(--autoTopBottomMargin));
      background-color: white;
      padding-top: 20px;
      border: 5px double black;
      filter: drop-shadow(5px 5px 10px black);
    }