The coordinate space bug that four rewrites couldn't fix

Published: (April 4, 2026 at 06:29 AM EDT)
3 min read
Source: Dev.to

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; localStorage overrides 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.

0 views
Back to Blog

Related posts

Read more »

10 Cool CodePen Demos (March 2026)

2026 F1 Drivers Custom Select using appearance: base-select Chris Bolson crafted one of the most impressive custom selects I've seen. It doesn’t even look like...