Rectangle overlap and hit testing
Find where two boxes overlap, or whether a point lies in a box, by comparing their edges with Math.max and Math.min.
O(1)Space O(1)4 practice problemsStep-through pictureThe idea
Screens are full of rectangles: elements, images, selection boxes, sprites. A box like the ones getBoundingClientRect() returns has an x and y for its top-left corner, plus a width and height. In screen coordinates y grows downward.
Two boxes overlap where both are present. The overlap's left edge is the larger of the two left edges, and its right edge is the smaller of the two right edges. The same goes for top and bottom. If the left edge ends up at or past the right edge, or the top at or past the bottom, they don't overlap.
A point is inside a box when it is between the left and right edges and between the top and bottom edges. Using "left edge included, right edge excluded" means a point on the shared edge of two neighboring boxes belongs to exactly one of them.
An everyday example
Two windows on a desk
Two paper windows lie on a desk, partly on top of each other. The shared region starts at whichever left edge is further right, and ends at whichever right edge is further left. Same for top and bottom. If those edges cross, the windows only touch or are apart, and there is no shared region. Poking the desk with a pen tip: it's on a window if it's between that window's left and right edges and between its top and bottom edges.
| In the example | In the code |
|---|---|
| The left edge further to the right | Math.max(a.x, b.x) |
| The right edge further to the left | Math.min(a.x + a.width, b.x + b.width) |
| The edges crossed, so nothing is shared | right <= left || bottom <= top |
| The pen tip lands on the window | containsPoint(rect, px, py) |
From the slow way to rectangle overlap and hit testing
- Start with the obvious idea: two boxes overlap if a corner of one is inside the other. It sounds right but fails. A wide box
{ x: 0, y: 0, width: 100, height: 20 }and a tall box{ x: 40, y: -30, width: 20, height: 90 }cross like a plus sign: they share a 20-by-20 square, yet no corner of either box is inside the other. - Split the question by direction. A point is in a box when its
xis inside the box's left-to-right range and itsyis inside the top-to-bottom range, and these two checks don't affect each other. So two boxes overlap exactly when their x ranges overlap and their y ranges overlap. One 2D question becomes two 1D questions. - Two ranges on a line share the part that starts after both have started and ends before either has ended. So the shared part starts at the larger start,
Math.max, and ends at the smaller end,Math.min. If that end is at or before that start, the ranges don't share anything. - Do this once for x and once for y, giving
left,right,top, andbottom. If either range is empty, returnnull; otherwise those four edges are the overlap box. For a single point, check the same ranges, with left and top edges included and right and bottom edges excluded. Those two functions are rectangle overlap and hit testing.
How it works
- Turn each box into edges: left
x, topy, rightx + width, bottomy + height. - The overlap's
leftis the larger left edge and itstopis the larger top edge. - The overlap's
rightis the smaller right edge and itsbottomis the smaller bottom edge. - If
right <= leftorbottom <= top, there is no overlap with positive area: returnnull. - Otherwise the overlap is
{ x: left, y: top, width: right - left, height: bottom - top }. - For a point
(px, py): it's inside whenx <= px < x + widthandy <= py < y + height.
Watch it run
This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.
See every step at onceThe same run, laid out side by side
The code
Point at a note to light up the lines it explains.
function intersection(a, b) {const left = Math.max(a.x, b.x);const top = Math.max(a.y, b.y);const right = Math.min(a.x + a.width, b.x + b.width);const bottom = Math.min(a.y + a.height, b.y + b.height);if (right <= left || bottom <= top) return null;return { x: left, y: top, width: right - left, height: bottom - top };}function containsPoint(rect, px, py) {return px >= rect.x && px < rect.x + rect.width && py >= rect.y && py < rect.y + rect.height;}
How fast it is
| Time | O(1) |
|---|---|
| Extra memory | O(1) |
Both functions do a fixed number of comparisons, whatever the boxes' sizes.
- Best case
O(1)Any two boxes: four
Math.maxorMath.mincalls and two comparisons. - Average case
O(1)The same fixed steps, whatever the sizes or positions of the boxes.
- Worst case
O(1)Still the same steps. The cost only grows when you compare many boxes, which is a different problem: all pairs of
nboxes takeO(n²).
Common mistakes
- Checking only whether a corner of one box is inside the other. Two boxes can cross like a plus sign with no corner inside the other.
- Treating boxes that only touch as overlapping, or the reverse. Decide on
<versus<=and follow the problem's rule. - Mixing up
ydirection. In screen coordinatesygrows downward, so the top edge is the smallery. - Computing the area of the union by adding areas. Overlapping parts are counted twice; subtract the overlap, or use a sweep line for many boxes.
When to use it
Look for these clues in a problem:
- Inputs are boxes with
x,y,width,height, or with left, top, right, bottom edges. - The question asks whether boxes overlap, how much they overlap, or which box is under a point.
- Collision detection in a game, or drop targets in drag and drop.
- Total area covered by several boxes (combine with coordinate compression or a sweep line).
Check yourself
Answer each one in your head first, then open it.
Why does checking the four corners of each box miss some overlaps?
Two boxes can cross like a plus sign, sharing a square in the middle while every corner of each box lies outside the other.
Why is the right edge excluded in containsPoint?
So that when boxes are placed side by side, a point on the shared edge belongs to exactly one box. With both edges included, a click on the edge would hit both.
If right is 160 and left is 100 but bottom is 40 and top is 50, what does intersection return?
null. The x ranges overlap, but bottom <= top, so the y ranges don't. Both axes must overlap.
What area do the two boxes in the trace cover together?
13800: 120 × 70 = 8400 plus 90 × 80 = 7200, minus the shared 1800, which would otherwise be counted twice.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 4 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingGo further
You don't need these to start practicing. Open one when you want more.
Why it works
The reasoning that makes it always right
A point (px, py) is in box a when a.x <= px < a.x + a.width and a.y <= py < a.y + a.height. It's in both boxes when px is at least both left edges, which is the same as at least left = Math.max(...), and below both right edges, which is the same as below right = Math.min(...). The same holds for py. So the set of shared points is exactly the box from left to right and from top to bottom.
That box is empty exactly when no px fits, which happens when right <= left, or when no py fits, when bottom <= top. Otherwise it has positive width and height, and the returned object describes it exactly. There's no loop, so the code always finishes after the same handful of steps.
Including the left and top edges and excluding the right and bottom ones means that when boxes sit side by side, sharing an edge at x = 160, a point on that edge belongs to exactly one of them: the one on the right. That's why the overlap check uses <= (touching boxes don't overlap) and containsPoint uses < on the far edges.
Going deeper
3 more ideas
Why max of the lefts and min of the rights
A point is in both boxes when it is right of both left edges, so right of the larger one, and left of both right edges, so left of the smaller one. The same holds for top and bottom. So the shared region is exactly the box between those edges, and it is empty when they cross.
Touching edges and which box wins a click
Two boxes that share only an edge, like one ending at x = 100 and the next starting at x = 100, give right === left, which the code treats as no overlap. For hit testing, including the left and top edges but not the right and bottom ones gives every point to exactly one box in a grid. When boxes are stacked, check them from the top layer down and return the first hit, as browsers do for clicks.
Many boxes
Checking every pair of n boxes costs O(n²). For many boxes, sort them by left edge and only compare boxes whose x ranges overlap (a sweep line), or put them in a grid of buckets so each box is only compared with its neighbors.
Work it by hand
Every step in a table, with why
Find where two boxes overlap, then hit-test four points against the overlap, including two that sit exactly on its edges.
a = { x: 40, y: 10, width: 120, height: 70 }, b = { x: 100, y: 50, width: 90, height: 80 }
| Step | Code | With the values | Result | What happens | Why |
|---|---|---|---|---|---|
| 1 | left | Math.max(40, 100) | 100 | start of shared x range | a starts at 40 and b at 100. A shared point must be right of both, so right of 100. |
| 2 | top | Math.max(10, 50) | 50 | start of shared y range | Screen y grows downward, so the larger top edge is the lower one. |
| 3 | right | Math.min(160, 190) | 160 | end of shared x range | a ends at 40 + 120 = 160 and b at 100 + 90 = 190. The shared part ends at the first of the two. |
| 4 | bottom | Math.min(80, 130) | 80 | end of shared y range | a ends at 10 + 70 = 80 and b at 50 + 80 = 130. |
| 5 | empty check | 160 <= 100 || 80 <= 50 | false | they overlap | Both ranges have positive length, so the boxes share an area. |
| 6 | return | 160 - 100, 80 - 50 | a 60 × 30 box at (100, 50) | the overlap box | Its area is 60 × 30 = 1800. |
| 7 | containsPoint | (100, 50) | true | top-left corner is in | px >= 100 and py >= 50 both hold: the left and top edges are included. |
| 8 | containsPoint | (160, 60) | false | right edge is out | 160 < 160 is false. The right edge belongs to whatever is next to the box. |
| 9 | containsPoint | (130, 79.5) | true | just above the bottom | Fractional coordinates are normal on screens; 79.5 < 80 holds. |
| 10 | containsPoint | (130, 80) | false | bottom edge is out | 80 < 80 is false, for the same reason as the right edge. |
The overlap is { x: 100, y: 50, width: 60, height: 30 }. Together the two boxes cover 8400 + 7200 - 1800 = 13800 square pixels, since the overlap would otherwise be counted twice.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| Rectangle overlap and hit testing | O(1) | O(1) | Two boxes, or one point and one box: always this. |
Check every pair of n boxes | O(n²) | O(1) | A few dozen boxes, such as a toolbar's buttons, where simplicity beats speed. |
| Sweep line over sorted boxes | O(n log n) | O(n) | Hundreds or thousands of boxes, when you need every overlapping pair or the total covered area. |
| Grid buckets | O(n) | O(n) | Many boxes of similar size spread over a screen, such as sprites in a game. The cost is on average, and grows if many boxes crowd into one bucket. |
Extra memory: O(1): four numbers and, when there is an overlap, one result object.
How the steps grow
- Rectangle overlap and hit testing
O(1) - Check every pair of
nboxesO(n²) - Sweep line over sorted boxes
O(n log n) - Grid buckets
O(n)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| Rectangle overlap and hit testing | 1 | 1 | 1 |
Check every pair of n boxes | 100 | 1 million | 1 trillion |
| Sweep line over sorted boxes | 33 | 9,966 | 20 million |
| Grid buckets | 10 | 1,000 | 1 million |
Inputs that need care
5 tricky inputs
- Touching edges:
{ x: 0, y: 0, width: 50, height: 50 }and{ x: 50, y: 10, width: 20, height: 20 } leftandrightare both 50, soright <= leftand it returnsnull. Change to<only if your rule says touching counts as overlapping.- Crossing like a plus sign:
{ x: 0, y: 0, width: 100, height: 20 }and{ x: 40, y: -30, width: 20, height: 90 } - It returns
{ x: 40, y: 0, width: 20, height: 20 }, even though no corner of either box is inside the other. - One box inside the other:
{ x: 0, y: 0, width: 50, height: 50 }and{ x: 10, y: 10, width: 10, height: 10 } - Every
Math.maxpicks the inner box's edge and everyMath.mintoo, so it returns the inner box,{ x: 10, y: 10, width: 10, height: 10 }. - A zero-width box:
{ x: 10, y: 10, width: 0, height: 30 } - Its left and right edges are equal, so any overlap has
right <= leftand it returnsnull. A line has no area to share. - A negative width from a drag:
{ x: 0, y: 0, width: -20, height: 50 } - Dragging a selection box up or to the left gives negative sizes, and the code returns
nullfor this box overlapping{ x: -15, y: 10, width: 10, height: 10 }, which it does cover. Normalize first:x = Math.min(x, x + width)andwidth = Math.abs(width).
Variations and follow-ups
Twists interviewers add
Intersection over union
To score how well two boxes match, divide the overlap area by the area they cover together: 1800 / 13800, about 0.13, for the trace. Image tools and object detectors use this to decide whether two boxes mark the same thing.
Distance from a point to a box
Clamp the point into the box, cx = Math.min(Math.max(px, x), x + width) and the same for y, and measure the distance to (cx, cy). It's 0 when the point is inside. Snapping and magnetic drag targets use this.
Many boxes
Checking every pair of n boxes costs O(n²). Sort boxes by left edge and sweep from left to right, comparing only boxes whose x ranges are still open, or drop boxes into grid buckets so each box is compared only with its neighbors.
Containment
Box b is fully inside box a when a.x <= b.x, a.y <= b.y, b.x + b.width <= a.x + a.width, and b.y + b.height <= a.y + a.height. Equivalently, intersection(a, b) equals b.
In an interview
Spot it, explain it, answer follow-ups
- Spot it from "do these rectangles overlap", "area of the overlap", "which element was clicked", "drag to select", "collision", or "total area covered by two rectangles".
- Explain it per axis: "The overlap starts at the larger of the two starts and ends at the smaller of the two ends. If the end isn't past the start on either axis, there's no overlap."
- Test four cases: touching edges, one box inside the other, a plus-sign crossing, and boxes that overlap on x but not on y. The plus sign is the one that breaks corner checks.
- A common follow-up: "total area covered by both". Answer
areaA + areaB - overlap. For many rectangles, mention the sweep line, since overlaps of overlaps get hard to subtract by hand.
Where frontend code uses it
The same idea in real UI code
IntersectionObserverreports anintersectionRectand anintersectionRatiobetween an element and the viewport, which is this overlap calculation done by the browser.- Drag-and-drop libraries such as dnd-kit offer collision-detection strategies like
rectIntersection, which picks the drop target that overlaps the dragged item most. document.elementFromPoint(x, y)hit-tests the page: it finds the topmost element whose box (and shape) contains the point.- Design tools select every layer that intersects a dragged selection box (a marquee).