Translating Viewport Coordinates Into Element-Local Coordinates Using Element.getBoundingClientRect()
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:
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:
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.
Want to use code from this post? Check out the license.
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.
@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:
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.
Yep, make sense now. Thanks for the clarification.
Hi, your demo is perfect but is it possible to set transform:scale(1.2) property to box and then its possible to get perfect mouse click point coordinates ?
@Bhavesh,
I don't have much experience with scaling via CSS. Are you suggesting that the
.getBoundingClientRect()
method doesn't taketransform
into account when performing the calculation? If so, that would be surprising to me. I would assume that this approach should "just work", regardless of how the Element is being rendered.@Ben,
I'm also encountering an issue when a parent element has a transform: scale applied to it. Any ideas how to take that data and modify the coordinates?
@Marc,
I have looking into finding the local click coordinates on an element where the parent has both
translate
andscale
applied e.g. transform: translate(200px, 200px) scale(1.5);Surprisingly, the MouseEvent.offsetX and offsetY were still local to the element and to the original scale - the target element was originally 250px height, and even thought he parent was scaled and transformed, clicking on the bottom edge of the target element gave me an offsetX of 250px (which I wasn't expecting)
@Marc, @Brian,
This is weird stuff. I will have to do some R&D on the use of
scale
. To be honest, I've never actually usedscale
in my own work, so I am not really sure how it works. I'll do some playing around.