Using getBoundingClientRect() to find the distance between two elements

Introduction

This is my first attempt to blog regularly (at least twice a month, or once every two weeks) about some of my findings and experiences learning JavaScript. I learn all sorts of things at work but I’m focussing on upskilling in JavaScript at the moment.

It’s no surprise that I’ve struggled to properly learn JavaScript over the years because of the lack of opportunity to build things with it. This post will detail a task I worked on over the short “week” between Boxing Day and New Year’s Eve.

I mentioned in my Hey 2017! post that I would usually push away anything I couldn’t do, and just stay in my comfort zone, hoping that someone else would deal with the task that was too hard. I wanted to stop doing this, and to actually give something a go. I was more determined and I really wanted to make it work.

Overview of the problem

I had to add some JavaScript logic to a design. This didn’t come up until I finished the original scope of the task, because there were some super edge-case scenarios in the product. The decision was made that these changes were still the best solution and would be better fixed now than trying to think of a workaround.

When you’re working with legacy code it can often be difficult to write ideal code or the code that would follow best practices. The legacy code can get in the way: it cannot be avoided and may conflict with the new functionality you want to add. That was the case here. I knew the easy way to fix the problem, but it couldn’t be done without spending a lot of time refactoring old code. And then, that code and the changes would have to be checked. It was too much work. Instead, I worked with our designer, Pat, to come up with the best solution, which is where we come to the logic that I had to write.

If this, then that

The thing with JavaScript problems, I find, is that it’s super important to quickly write or type out the logic you want to execute, or even say it out loud. Thanks to Jerry, I’ve been reminded that the first step is not to just jump into the code and try to fix the problem, but it’s to think of how the problem can be fixed before telling the computer what you want it to do.

In this case, my simple instruction for the computer was: “If the object is less than 50 pixels away, add this class”.

The problem

I work on our email builder, the tool that helps you build awesome emails. To explain the scenario a little bit: You can have multiple sections in an email. If you click on one section of your email in the email builder, some section controls appear. Pat had redesigned the controls so that they appeared directly above the section. Unfortunately, on the topmost section of the email, the new controls would become cut off by a fixed bar across the top of the window. I won’t go into detail but it involved some legacy code that could not be fixed immediately. The fixed bar is just one of the several pieces of UI that surround the canvas/play area of the email.

Diagram showing basic structure of the email builder
Basic structure of the builder – light area is the email, dark areas are building tools or other UI

The solution we proposed was to add a class, styled with CSS, to the section when it was active (that is, when it’s been clicked).

The CSS was simple. We wanted 55 pixels of breathing room above the element, to allow the new section controls to fit:

.has-top-margin {
  margin-top: 55px;
}

Now it was just a matter of writing the JavaScript to add has-top-margin to the section on a certain condition:

If the object is less than 50 pixels away, add this class.

So the condition is when the section is less than 50 pixels away from the top of the email. We want to ignore the fixed bar completely and focus on the area of the email itself.

However, we need to consider that scrolling down the page and the section disappearing out of view does not constitute “less than 50 pixels”. This should consider the whole viewport, not just what is visible in the window. The viewport is the visible part of the page within the browser, and that includes all areas that can be scrolled.

So I needed to grab the values of two things:

  1. The position of the section, relative to the top of the viewport
  2. The position of the scrollable part of the email builder, relative to the top of the viewport

I decided to store these into variables. It’s bound to get complicated, so having a reference is a good idea. (For the purpose of this post, assume that the value of element is already taken care of elsewhere in the codebase – it refers to the current section.)

var offset = element.getBoundingClientRect();
var scrollPosition = document.getElementById('email-body').getBoundingClientRect().top;

I think you can gather that document.getElementById will locate an element with a specific ID. You should already know that you can only use IDs once in a document, so this would be the only element with the ID of email-body. It’s a unique element. In this case, it represents the top of the body of the email builder, which has a starting position being right at the bottom of the fixed bar. That’s where we want to calculate our distances from.

When the user scrolls down and the active section disappears out of view, scrollPosition will be a negative value as it is a value relative to the top of the viewport. The top of the viewport will move further up, the further you scroll down the page.

The getBoundingClientRect() function

I have to admit, this is the best function since sliced bread. I was after something that would get the distance (in pixels) of the current element from the top of the viewport, and getBoundingClientRect() does just that. It gives you an object that’s a bit like this:

ClientRect {top: 276, right: 554, bottom: 294, left: 8, width: 546, …}

You can use getBoundingClientRect().top to grab the top value, getBoundingClientRect().right for the right value, and so on.

In this case I wanted the distance from the top. I chose not to include .top in the variable itself, but to use it later in the code when I actually needed it, for the sake of readability and so I could better understand the code.

Writing the condition

I wrote the if statement with the condition.

var offset = element.getBoundingClientRect();
var scrollPosition = document.getElementById('email-body').getBoundingClientRect().top;
if (offset.top - scrollPosition < 50) {
  // add the class
}

To explain the calculation (offset.top - scrollPosition < 50): offset grabs the variable of the same name then appends .top to it, which is the distance from the current section to the top of the viewport. Now we want to subtract the position of the top of the email builder using the scrollPosition variable. This gives us the actual distance between the the top of the section and the top of the email. And that distance should be less than 50 pixels for the class to be added.

I won’t go off on too much of a tangent, but I did use console.log many times to log the values. :P If you’re new to JavaScript you may not know this, but you can write a little something to check if you’re looking for the right thing:

console.log("there is a section " + sectionPosition + "px away from the top of the builder");

Adding the class

In my situation I actually found it easier to add the class to the parent element of the section. Again, because of the way stuff is built, you may sometimes run into unexpected legacy code.

I used .classList, which refers to the classes present on the element. Then I used the .add() function to add the class.

var offset = element.getBoundingClientRect();
var scrollPosition = document.getElementById('email-body').getBoundingClientRect().top;
if (offset.top - scrollPosition < 50) {
  element.parentNode.classList.add('has-top-margin');
}

I put element.parentNode into a variable to make things a little easier to read.

var offset = element.getBoundingClientRect();
var parent = element.parentNode;
var scrollPosition = document.getElementById('email-body').getBoundingClientRect().top;
if (offset.top - scrollPosition < 50) {
  parent.classList.add('has-top-margin');
}

Normally this could be considered complete because we’ve added the class and that’s it. But these sections are dynamic and can be re-ordered. (This block of code actually already sits in a function that watches for changes on the page.) I needed to add an else statement to cover when the section stops being less than 50 pixels away. For example, someone might move the section further down the page.

So I remove the class using .remove(). This obviously won’t do anything if the class isn’t there to begin with, though.

var offset = element.getBoundingClientRect();
var parent = element.parentNode;
var scrollPosition = document.getElementById('email-body').getBoundingClientRect().top;
if (offset.top - scrollPosition < 50) {
  parent.classList.add('has-top-margin');
} else {
  parent.classList.remove('has-top-margin');
}

Now the problem with this code is that you can interact with a section multiple times, and so long as it satisfies the condition of being less than 50 pixels away, it will keep on adding the class. You will end up with multiple occurrences of has-top-margin. This is why I added a remove function to the if statement as well.

var offset = element.getBoundingClientRect();
var parent = element.parentNode;
var scrollPosition = document.getElementById('email-body').getBoundingClientRect().top;
if (offset.top - scrollPosition < 50) {
  parent.classList.remove('has-top-margin');
  parent.classList.add('has-top-margin');
} else {
  parent.classList.remove('has-top-margin');
}

It’s placed before the add() function so that when the condition is satisfied, any existing occurrences of has-top-margin will be removed before the class is added (again).

I did eventually convert this function to jQuery because we are already using the jQuery library – I might write a post on that too, but it’s pretty simple. I found this task challenging to write in vanilla JavaScript but I enjoyed it and was proud of myself at the end. :D

Disclaimer: Code samples may not reflect the code that I used. Some modifications were made due to confidentiality and to better explain the steps of this task.

Comments on this post

I am now officially scared to learn JavaScript now lol.
I haven’t even taken that class yet for my computer science major & I’m already dreading it LOL.

Hopefully once I’ve taken that class, I’ll understand some of this stuff. :/

This was really interesting to read! I’m so glad that you’ve gotten more comfortable with Javascript!

After reading this, I know that I still have a lot to learn about it. I only know some of the basics. Hopefully I’ll be able to expand my knowledge on it.

Wow, I’ve never realised how bad I am at coding languages until now. I barely understood any of the code in there, and I can’t imagine being able to work something out like that. You are so talented, Georgie! No wonder you’ve done so well for yourself in your career!

I’m now of to write something vaguely intelligent on your previous post, to make up for my terrible JavaScript knowledge…

Congrats on managing to do it!

*off to write

I’m showing myself up at writing now, too!

Boom. Very nicely done, and only in a few lines of code! I’ve never used getBoundingClientRect() but it seems really handy! Also it’s nice to see the solution in pure javascript instead of jQuery. I’m ashamed to say I know way more jQuery than javascript – I’d have to look up so much stuff! Anyway this is great, really simple explanation! Can’t wait for more in this series!

Thank you Becky! It means a lot coming from someone who knows a bit more than me in this area. 😆 I’ve realised that it helps to know both JavaScript and jQuery, and that it’s cool to understand how some functions in jQuery can be written in JavaScript. It can be easier to use jQuery sometimes even though you may not need to… 😎

Javascript is one of the languages that I feel is a must-learn, even with web designers today. Unfortunately for me, it’s also one of the languages that I struggle with too. I’m currently learning Ruby and Sinatra (not yet on Rails though) and it’s already tricky enough. jQuery is a lot easier for me to understand, but at the same time, I know that I shouldn’t rely too much on frameworks when it comes to building apps (but they’re so much easier to use!).

Good luck with your learning! You would probably do a lot better than I would in this field. :)