DEV Community

Frandy Slueue
Frandy Slueue

Posted on

The coordinate space bug that four rewrites couldn't fix

frandy-dev timeline component

I spent most of today's session on a bug that turned out to be architectural, not logical.

The setup

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.

The failed approaches

  1. Fixing the threshold — larger, smaller, speed-dependent
  2. Adding direction guards — only flash when approaching
  3. Fixing the position math — accounting for scrollLeft, cardWidths state
  4. Full rebuild of the detection loop

None of it worked consistently.

The actual problem

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 only agree at one specific state: scroll = 0, all cards open. Any deviation and they diverge.

The fix

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 < -10 || x > overlayWidth + 10) continue; // off-screen, skip
    nodes.push({ x, idx, color });
  }
}
Enter fullscreen mode Exit fullscreen mode

Stop calculating. Measure from the DOM. getBoundingClientRect() gives you the actual rendered position in the coordinate system you're already using.

Re-measure on scroll, card width changes, filter changes, resize. Use a dirty flag so you measure at most once per animation frame.

The light now flashes exactly on the node dot. Every time. Because it's reading reality.

Broader takeaway

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.


What else shipped

Theme system — full light/dark mode with 4 accents. localStorage beats 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.


Where things stand

The site is not shipped yet. Timeline desktop is done. Next is a design-only pass — mobile layout for every section, then admin. No new features. Getting everything looking right before it goes live.


Top comments (0)