Drag a box over a canvas of items, select what it touches. Four lines of geometry, and each one is a place implementations go wrong.
Try it, with the fuzz results computed live: https://dev48.infy.uk/design/day66-marquee-select.html
1. The band is anchor-to-pointer, not a path
const band = {
x: Math.min(anchor.x, p.x), y: Math.min(anchor.y, p.y),
w: Math.abs(p.x - anchor.x), h: Math.abs(p.y - anchor.y)
};
Grow the rectangle along the pointer path instead and a drag up-and-left produces negative width and selects nothing. The symptom looks like "it only works one way".
2. Overlap, not containment
const overlaps = (a, b) => !(a.x + a.w < b.x || b.x + b.w < a.x ||
a.y + a.h < b.y || b.y + b.h < a.y);
Four disjointness checks, all of which must fail. The positive version needs eight comparisons and a case analysis, and the case analysis is where the bugs live.
3. Modifiers are set algebra over a snapshot
Shift adds, Alt subtracts, Ctrl toggles — over the selection as it was at pointerdown.
The property that separates a snapshot base from a live one is not "does not flicker" (both change as the band changes). It is path independence: with the base captured once, the selection is a pure function of (base, band), so wandering the pointer away and back returns you to exactly the same selection. A live base accumulates, and a Ctrl-drag ends somewhere else entirely.
4. Content coordinates, not viewport
const toContent = (e) => ({
x: e.clientX - rect.left + el.scrollLeft,
y: e.clientY - rect.top + el.scrollTop
});
Two additions. Identical to the wrong version until the container scrolls mid-drag — after a 300px scroll the two select 18 items versus 11. Which is why it never shows up in a demo where nothing scrolls.
The measurement, and why it is the interesting part
All four fuzzed over 40,000 random drags against two independently written oracles — an interval-form overlap test and a bounded grid probe:
| variant | disagreements | rects that left the canvas |
|---|---|---|
| correct | 0 | 0 |
| path-based band | 7,244 | 0 |
| containment | 9,767 | 0 |
| unnormalised | 7,244 | 0 |
That last column is the whole point. Every broken variant keeps the rectangle inside the canvas. Every visible constraint holds. Only the invisible one breaks — so they pass review, pass a smoke test, and ship.
The technique that makes this possible: none of the four functions touch the DOM. Anchor, pointer, band and modifier in; a set of ids out. 40,000 drags run in milliseconds under node with no browser at all.
Part of a from-scratch series — one component a day, vanilla JS, one file, offline: https://dev48.infy.uk/designfromzero.php
Top comments (0)