wrong rendering on chrome on getBoundingClientRect()

699 Views Asked by At

I've an iframe containing few elements and all I want to do is to have an outline over the elements wherever I hover/click on the iframe. Like so:

enter image description here

As you can see in the image, I clicked on an h1 element and an outline has been shown so that I get to know what I'm selecting.

Ofcourse I don't want to mess-up with the code inside the iframe so I've the outlining div placed outside the iframe and set it to position: absolute. Then it's using getBoundingClientRect() to get the width, height and top from the iframe so that the div would be of same size and position too.

The problem is, it's getting the values right but not rendering properly.

For example, if you click on the button then here's how it looks like:

enter image description here

As you can see, the width is short from the right. I inspected the elements too, everything has right values: The width of the button is 72.61px and the width of the div is 72.6125 as well but still it's shorter from the right. (Also, if you zoom in, it gets fixed and if you zoom in again, it gets like this again) That seems super strange to me...

This only seems to be in Chrome but not in Firefox...

index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Test</title>

    <style>
      html {
        height: 100%;
      }
      body {
        padding: 20px 20px 20px 20px;
        height: 100%;
        margin: 0px;
      }
      /* Helper divs */
      div#hover-selector,
      div#click-selector {
        box-sizing: border-box;
        display: none;
        pointer-events: none;
        border: 1px solid rgb(76, 120, 255);
        position: absolute;
        border-radius: 0px;
        background-color: transparent;
      }
    </style>

    <script src="script.js" defer></script>
  </head>
  <body>
    <div
      id="frame_container"
      style="position: relative; width: 100%; height: 100%; overflow: hidden"
    >
      <iframe
        frameborder="0"
        title="Project"
        id="frame"
        style="width: 80%; height: 80%"
      ></iframe>
      <div id="click-selector"></div>
      <div id="hover-selector"></div>
    </div>
  </body>
</html>

page.html: (the code inside the iframe)

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style id="visually-default-stylesheet">
      * {
        box-sizing: border-box !important;
      }

      *:focus {
        outline: none !important;
      }

      html {
        scroll-behavior: smooth !important;
      }

      /* Remove default margin */
      html,
      body,
      h1,
      h2,
      h3,
      h4,
      p,
      li,
      figure,
      figcaption,
      blockquote,
      dl,
      dd {
        margin: 0;
      }

      h1 {
        font-family: "Open Sans", sans-serif;
        font-size: 32px;
        font-weight: 500;
        color: #000000;
        margin-bottom: 10px;
        width: auto;
      }

      p {
        font-family: "Open Sans", sans-serif;
        font-size: 15px;
        font-weight: 500;
        color: #3d5565;
        line-height: 28px;
        margin-bottom: 10px;
        width: auto;
      }

      button {
        font-size: 12px;
        font-family: "Open Sans", sans-serif;
        font-weight: 600;
        padding-top: 10px;
        padding-bottom: 10px;
        padding-left: 12px;
        padding-right: 12px;
        border-radius: 6px;
        background-color: #0f172a;
        color: white;
        border: 0;
        margin-bottom: 10px;
      }

      /* Set body's default width and height */
      body {
        padding: 10px;
        min-height: 100vh;
        user-select: none;
      }
    </style>
  </head>
  <body>
    <h1>Heading</h1>
    <p>
      Lorem ipsum dolor sit amet consectetur adipisicing elit. Possimus non
      impedit, sequi veniam deleniti culpa odit cumque eligendi ea rem,
      quibusdam, ullam asperiores atque delectus cupiditate reprehenderit
      distinctio pariatur temporibus.
    </p>
    <button>Click me</button>
  </body>
</html>

script.js:

const click_selector = document.getElementById("click-selector");
const hover_selector = document.getElementById("hover-selector");

const iFrame = document.getElementById("frame");

function calculateRect(element, selector) {
  let rect = element.getBoundingClientRect();
  selector.style.width = rect.width + "px";
  selector.style.height = rect.height + "px";
  selector.style.top = rect.top + "px";
  selector.style.left = rect.left + "px";
}

let clickedElement;
iFrame.src = "page.html";
iFrame.addEventListener("load", () => {
  let iFrameDoc = iFrame.contentDocument;
  pageHeight = iFrame.contentWindow.innerHeight;

  iFrameDoc.addEventListener("click", (e) => {
    clickedElement = e.target;
    calculateRect(clickedElement, click_selector, true);
    click_selector.style.display = "block";
  });

  iFrameDoc.addEventListener("mouseover", (e) => {
    hoveredElement = e.target;
    calculateRect(hoveredElement, hover_selector);
    hover_selector.style.display = "block";
  });

  // updating selector style on window resize
  window.addEventListener("resize", (e) => {
    if (clickedElement) {
      calculateRect(clickedElement, click_selector);
      hover_selector.style.display = "none";
    }
  });
});

Edit:

If I change line-height: 18px it works and stops the quirky behavior. I don't know why. But the question still remains, how line-height is influencing such behavior...

Also, each PC produces this behaviour at different zoom levels. Some at 100, some at 110 and so on... and some don't even have this.

1

There are 1 best solutions below

0
galalem On

I'm quite sure it has something to do with decimal values for px units. as 1 pixel is a single light on the screen. it almost never ends well if you count on'em.

I was going to recommend the use of Math.ceil() as suggested in the comments. but not flooring the top & left... Yet, at some point you gonna face bigger problem than a 1px offset:

As mentioned in the getBoundingClientRect() docs the function returns the width & height including the padding and the border width.

But

Once you set box-sizing: border-box as visible in your iframe code, this will NO Longer be the case. getBoundingClientRect() will now only includes the width/height and the padding but not the border width.

What I recommand is using the CSS property outline

as you already getting the element from the iframe why not just add the outline to the style (more precisely outline-color, outline-style and outline-width seperatly) in case you dont want to mess with original style you can store the initial values, set the new outline then on blur reset them back.

ouline docs

This approach is better because: 1 this is why the property is invented for, 2 you don't need to worry about overlaying other elements that By The Way may not give you same appearence if they don't have the same display (example: div with display block may not exactly fit onto a table with display table even though they have same widths, heights and paddings

One Last Thing to NOTE

in this code

  iFrameDoc.addEventListener("mouseover", (e) => {
    hoveredElement = e.target;
    calculateRect(hoveredElement, hover_selector);
    hover_selector.style.display = "block";
  });

you should keep in mind that e.target is always the deepest child. so if some how you have an icon in your button. example: font-awesome icon <i class="fas fa-plus"></i>. then the e.target will be the <i> tag NOT the button when you hover on the icon (not when you hove on the text).

So if you don't have control on the source of the iframe, be ready for some surprises.

Hope this helps :)