Skip to content

Puck issue 1456 overlay position#1461

Open
shannonhochkins wants to merge 10 commits intopuckeditor:mainfrom
shannonhochkins:puck-issue-1456-overlay-position
Open

Puck issue 1456 overlay position#1461
shannonhochkins wants to merge 10 commits intopuckeditor:mainfrom
shannonhochkins:puck-issue-1456-overlay-position

Conversation

@shannonhochkins
Copy link
Contributor

@shannonhochkins shannonhochkins commented Dec 11, 2025

Closes #1456

Description

Fix overlay positioning to fully cover components even when parents/targets apply CSS transforms or fixed positioning. We now rely on the browser’s transformed bounding boxes, add fixed-target handling, and resync overlays when style/class or geometry changes. Removed the legacy inverse-scale correction because getBoundingClientRect already reports transformed sizes/positions, so dividing by accumulated transforms caused misalignment under parent transforms.

Changes made

  • Use the element’s transformed getBoundingClientRect plus scroll offsets to set overlay left/top/width/height; drop accumulateTransform inverse-scaling
    • getBoundingClientRect() already returns the post-transform box: position, width, and height are in the final rendered coordinate space, including all parent transforms (scale/translate/rotate). Dividing by an accumulated scale double-corrects and shrinks/shifts the overlay when parents are transformed.
  • Detect fixed ancestors and set overlay position: fixed while skipping scroll offsets in that case.
  • Add a MutationObserver watching style and class to resync overlays when styles change.
  • Add a lightweight rAF rect watcher to resync when geometry moves due to transforms (e.g., carousel track translate/scale) without waiting for scroll/resize.
  • Keep ResizeObserver and ref reattachment logic; sync now depends only on getStyle.
  • Supports css transforms completely (scale, rotation, translation, even skew) (example screenshot below, try the same css in production ;) )
image

How to test

  • Update inline css of any component, change to position fixed, overlay should be positioned correctly
  • Update inline css of any component, or even a parent, add css transform scale3d(0.79, 0.79, 1) translate3d(20%, 0, 0); and you should see the overlay move to match the new position when hovering, even other sub components if added to a parent with transforms like scale3d(0.79, 0.79, 1) translate3d(20%, 0, 0); overlay should match the rendered element bounds.
  • Set a draggable ancestor to position: fixed; overlay should also be fixed and align while scrolling.
  • Apply style/class changes (e.g., add/remove CSS that shifts size/position) and confirm the overlay updates immediately.
  • transitions that manipulate inline styles even not on the target will now respect the new position and overlays will update (carousel example)
  • Test disableIframe=true to ensure all the above still works when no iframe is provided

I have done EXTENSIVE testing on this and it's pretty solid! Can't find any more issues

  • Multiple tests were done, comparing production to this branch, screen recordings below:
output.mp4

^ Fixes a bug in production in the puck demo that's present

And here's the difference in my software:

  • EDIT - This screen recording was taken before i added the raf helper, you can see the overlay sometimes jumps when the carousel moves, until you move your cursor or scroll/resize - this is fixed now, additionally the "fixed" popup overlay that animates, is also patched and now is pinned to the element directly (this was a separate side effect i removed with a way over-engineered solution) - now much simpler and very little changes to puck
output_small.mp4

@vercel
Copy link

vercel bot commented Dec 11, 2025

@shannonhochkins is attempting to deploy a commit to the Puck Team on Vercel.

A member of the Team first needs to authorize it.

@vercel
Copy link

vercel bot commented Jan 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
puck-demo Ready Ready Preview, Comment Jan 9, 2026 11:08am
puck-docs Ready Ready Preview Jan 9, 2026 11:08am

Copy link
Contributor

@chrisvxd chrisvxd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for slow review. Can confirm it works, but have performance concerns with the animation loop. See comment.

["style", "class"].includes(mutation.attributeName || "")
)
) {
sync();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably put this inside a requestAnimationFrame

Comment on lines +300 to +301
left: `${rect.left + scroll.x}px`,
top: `${rect.top + scroll.y}px`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm bit confused why the transforms have been removed here, and how they're getting applied

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did explain this in the main description of the PR a little more in detail, but a bounding box value is provided with transforms applied, including its parent(s), so theres no need to apply the transforms here

Comment on lines +332 to +353
const tick = () => {
const el = ref.current;

if (el) {
const rect = el.getBoundingClientRect();
const prev = lastRectRef.current;

const changed =
!prev ||
Math.abs(rect.x - prev.x) > 0.5 ||
Math.abs(rect.y - prev.y) > 0.5 ||
Math.abs(rect.width - prev.width) > 0.5 ||
Math.abs(rect.height - prev.height) > 0.5;

if (changed) {
lastRectRef.current = rect;
sync();
}
}

frame = requestAnimationFrame(tick);
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this animation loop is viable. The demo app now idles my MBP (M1 Max) CPU utilisation at 10%, up from under 2%. Can you find another solution?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can throttle it, the main benefit of the RAF is animations realistically, but theres something many other operations thst weren't not listening to that can affect position so i added the raf, would a throttled raf suffice?

@shannonhochkins
Copy link
Contributor Author

@chrisvxd I've made a few changes, are you able to check them again for me?

Perf change: removed the always-on overlay sync loop and switched to event-driven + throttled measurement.

Previously, each DraggableComponent started a requestAnimationFrame loop that ran continuously, even when the app was otherwise idle. That meant we were calling getBoundingClientRect() every frame and occasionally updating overlay state, which kept the renderer active and raised baseline CPU usage.

In this update:

  • Overlay sync is now coalesced via scheduleSync() (single rAF per frame). This prevents multiple rapid triggers from causing multiple React updates.

  • Resize handling stays event-driven via ResizeObserver, which calls scheduleSync() when the element’s dimensions change.

  • Scroll / window resize are handled via listeners (scroll uses capture to catch nested scroll containers), triggering scheduleSync() only when those events occur.

  • To still support position changes caused by animations/layout shifts/transforms (i.e. movement that doesn’t emit a scroll/resize/resizeObserver event), we added a throttled rect sampling loop while the component is active (measuring at ~10fps via MEASURE_EVERY_MS). When a rect delta is detected, we trigger scheduleSync().

Net effect: we keep overlay positioning correct during real movement (scroll/resize/animations), but avoid the previous “always hot” 60fps loop that increased idle CPU.

The throttled measurement loop is not started on hover, only when selected/dragging, to avoid many components spinning up trackers as the mouse moves through the editor.

@chrisvxd
Copy link
Contributor

I haven't yet reviewed the latest changes, but wanted to know if you'd considered using https://github.com/Shopify/position-observer for this?

@shannonhochkins
Copy link
Contributor Author

I haven't yet reviewed the latest changes, but wanted to know if you'd considered using https://github.com/Shopify/position-observer for this?

Haven't seen that before, happy to wire it in if you'd prefer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Highlighted component - Breaks when component is fixed

2 participants