Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: Kevin Roche and Seb Duggan
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: Kevin Roche@kroche ) and Seb Duggan@sebduggan )

Translating Viewport Coordinates Into Element-Local Coordinates Using Element.getBoundingClientRect()

By Ben Nadel on

Some user interaction events provide positional meta-data about the event in relation to the browser's viewport. For example, if you highlight text, the Selection API reports the bounding box of the selected Ranges in relation to the viewport. In order for your application to react to such events, it's not uncommon to have to translate the reported viewport coordinates into element-local coordinates to render subsequent user interface components. To do this, we can use the Element.getBoundingClientRect() method and some simple math.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

The Element.getBoundingClientRect() method reports the size and position of the contextual element relative to the browser's viewport. If we have other positional information that is also relative to the browser's viewport, we can calculate the element-local position by taking the difference between the two viewport-relative positions:


 
 
 

 
 Translating viewport-relative coordinates into element-relative coordinates using the .getBoundingClientRect() method. 
 
 
 

To see this in action, I've put together a simple demo that tracks mouse-clicks on the document (via event-delegation). It then takes the mouse-click viewport-coordinates and uses .getBoundingClientRect() to calculate the position of the click within a target element. The calculated Element-local position is then used to render a "dot" under the user's mouse.

  • <!doctype html>
  • <html lang="en">
  • <head>
  • <meta charset="utf-8" />
  • <title>
  • Translating Viewport Coordinates To Element-Local Coordinates Using .getBoundingClientRect()
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css" />
  • </head>
  • <body>
  •  
  • <h1>
  • Translating Viewport Coordinates To Element-Local Coordinates Using .getBoundingClientRect()
  • </h1>
  •  
  • <div class="box box-a"></div>
  • <div class="box box-b"></div>
  • <div class="box box-c"></div>
  • <div class="box box-d"></div>
  • <div class="box box-e"></div>
  • <div class="box box-f"></div>
  •  
  • <script type="text/javascript">
  •  
  • document.addEventListener( "click", handleClick, false );
  •  
  • function handleClick( event ) {
  •  
  • if ( ! event.target.classList.contains( "box" ) ) {
  •  
  • return;
  •  
  • }
  •  
  • // Get the VIEWPORT-relative coordinates of the click.
  • // --
  • // NOTE: The MouseEvent interface has a bunch of coordinate-related values,
  • // including offsetX and offsetY which may seem relevant to this demo. But,
  • // this demo is NOT about the MouseEvent - it's about coordinate translation.
  • // It's only coincidental that I'm using mouse events to drive it.
  • var viewportX = event.clientX;
  • var viewportY = event.clientY;
  •  
  • // Now that we have the VIEWPORT coordinates of the CLICK, we need to get the
  • // VIEWPORT position of the target element. This will give us coordinates
  • // that are operating in the same grid system. Luckily, that's exactly what
  • // the .getBoundingClientRect() method gives us!!
  • var boxRectangle = event.target.getBoundingClientRect();
  •  
  • // Now that we have the targets VIEWPORT coordinates and the click's VIEWPORT
  • // coordinates, we can take the difference between the two in order to
  • // translate the VIEWPORT coordinates into target-LOCAL coordinates.
  • var localX = ( viewportX - boxRectangle.left );
  • var localY = ( viewportY - boxRectangle.top );
  •  
  • // In this particular demo, we have to take into account the border of the
  • // box element since the .getBoundingClientRect() values will be relative to
  • // the outer-most boundary of the box.
  • var borderWidth = parseInt( window.getComputedStyle( event.target ).borderTopWidth, 10 );
  • localX -= borderWidth;
  • localY -= borderWidth;
  •  
  • // Now that we have the target-LOCAL coordinates, let's append a DOT element
  • // to the target container for proof of purchase.
  • var point = document.createElement( "div" );
  • point.classList.add( "point" );
  • point.style.left = ( localX + "px" );
  • point.style.top = ( localY + "px" );
  • event.target.appendChild( point );
  •  
  • console.log(
  • "Translating Viewport {", viewportX, ",", viewportY, "}",
  • "to Local {", localX, ",", localY, "}"
  • );
  •  
  • }
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, we're using event-delegation on the Document to listen for mouse-click events. We're then taking clicks that originate from within one of the boxes, and using the viewport-delta of the mouse coordinates and the box's bounding rectangle in order to append and position a "dot" element. And, when we run this code and click in one of the boxes, we get the following browser output:


 
 
 

 
 Translating click events into element-local positioned coordinates. 
 
 
 

The Element.getBoundingClientRect() method is an awesome feature that has standardized support in all major browsers, going back to IE9. In this case, you can see how easy it makes it to translate viewport-relative coordinates into element-local coordinates.



Looking For A New Job?

Ooops, there are no jobs. Post one now for only $29 and own this real estate!

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

I find your solution pretty clever and straight forward. Nothing complicated really. Depends on what you are trying to achieve really. You said "forget about the click coordinates", But I find them I bit tricky depending on the situation. For example I have a knob that should rotate in certaic circle with some radius, no matter how far my touch, or cursor goes away from that circle I use trigonometry to calculate the new coordinates of the draggable knob. So in this case it is really important what points for reference you are going to use. I ended up with a knob that jumped immediately some degrees even if I didn't drag it at all. The mistake was that I didn't calculate correctly the reference point.

Reply to this Comment

@Zlati,

Dang, that sounds complicated! The second you have to pull "radii" into a conversation, my brain starts to melt. I was good with math up until Trigonometry. Geometry was probably my last good math level. Especially the proofs -- need to know why one angle is the same angle based on some combination of postulates and laws, that was fun! That's how I used to spend my homeroom period.

But, start to get rotations, sins, cosigns, tangents, sequences, series, differentials ... my brain goes HOLD UP SON! :P

That all said, part of why I said "forget about the mouse event" was because I had just recently played around with the Selection API:

https://www.bennadel.com/blog/3439-creating-a-medium-inspired-text-selection-directive-in-angular-5-2-10.htm

... and, in that post, I looked at how the Selection API exposes a "bounding box" of the selected elements. That bounding box is the a DOMRect interface that is relative to the Viewport, just like the mouse coordinates. So, my only point was to say that there may be _other_ sources of coordinates beyond mouse events (such as the Selection API); and, I didn't want people to worry about why I chose event.clientX - which is Viewport relative - instead of event.pageX - which is Document relative.

Hope that makes sense.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.