The coordinate space bug that four rewrites couldn't fix
Source: Dev.to
The bug
I spent most of today’s session on a bug that turned out to be architectural, not logical.
frandy.dev has an animated timeline section. Cards sit in a horizontal track. You can fold them to peek width by dragging the right edge. A neon light travels across three spine tracks and flashes when it reaches each card’s node dot.
The flash timing was wrong — off by varying amounts depending on scroll position and card state.
What didn’t work
- Fixing the threshold (larger, smaller, speed‑dependent)
- Adding direction guards (only flash when approaching)
- Fixing the position math (accounting for
scrollLeft, card widths state) - Full rebuild of the detection loop
None of those attempts worked consistently.
The coordinate‑space mismatch
The traveling light existed in viewport space – pixels from the left edge of the visible overlay.
Node positions were calculated in card space – accumulated pixel widths across all cards, starting from the left of the entire track (including scrolled‑off cards).
These two coordinate systems only agree in one specific state: scroll = 0 and all cards open. Any deviation and they diverge.
The fix: measure from the DOM
function measure() {
const oRect = overlay.getBoundingClientRect();
const dots = document.querySelectorAll("[data-tl-node]");
for (const dot of dots) {
const r = dot.getBoundingClientRect();
const x = r.left + r.width / 2 - oRect.left;
if (x overlayWidth + 10) continue; // off‑screen, skip
nodes.push({ x, idx, color });
}
}- Stop calculating positions manually.
- Use
getBoundingClientRect()to obtain the actual rendered position in the same coordinate system the light already uses. - Re‑measure on scroll, card‑width changes, filter changes, and resize.
- Use a dirty flag so you measure at most once per animation frame.
Result
The light now flashes exactly on the node dot—every time—because it’s reading reality.
When your coordinate math keeps drifting from what you see on screen, you’re probably in the wrong coordinate space. The browser already computed the correct answer;
getBoundingClientRect()hands it to you.
Additional polish
- Theme system – full light/dark mode with four accents;
localStorageoverrides admin default. Desktop dropdown, mobile slide‑down sheet that swaps visibility with BackToTop on scroll. - Timeline UX – rubber‑band drag, spring physics, sticky first card, double‑tap to open panel, card index watermark, breathing peek dot.
- 3‑track spine pulse – comet tail, speed variation, escort offset, node flash with spin + ripple, future card color fallback, clean restart.
- UI polish – chip/TabBar borders fixed, BackToTop realigned, section padding and nav height increased, text opacity improved.
Next steps
The site is not shipped yet. Timeline desktop is done. The next phase is a design‑only pass: mobile layout for every section, then admin. No new features—just getting everything looking right before it goes live.