Percent becomes pixels on drag and/or copy-and-paste

What are you trying to do? (please be as specific as possible and include relevant screenshots, code snippets)

Position markers absolutely inside a slot of our own code component, in % so each marker lands correctly at every breakpoint (the map stage is a different size per breakpoint).

Dragging a marker on the canvas rewrites the percentages as pixels, silently:

before   left: 48.66%    top: 63.4%
after    left: 214.5px   top: 229.203125px

214.5px is the same position at the current stage size, so after one drag the marker is only correct at that one width. It rewrites the base values, so a desktop drag breaks the other breakpoints too.

It looks like a bug rather than intended behaviour because the unit survives when the parent is a Plasmic container — same component, same panel, same stage:

  • marker is a direct child of the code component’s slot → drag writes left: 214.5px; top: 229.203125px

  • marker wrapped in a free box (position: relative; width: 100%; height: 100%) in that same slot → drag writes left: 58.58%; top: 27.42%

The slot’s own box isn’t the cause: the code-component instance already has position: absolute; left: 0; top: 0; width: 100%; height: 100%.

Expected: a value the author entered as % stays % after a drag or copy-paste and or duplicate element. Failing that, compute % when the parent has a definite size, which a slot filling its parent does.

What are the reproduction steps?

  1. Register a code component that renders a <div> with a children slot:

    registerComponent(Panel, {
      name: 'Panel',
      importPath: './Panel',
      props: { children: { type: 'slot' } },
    });
    
    
  2. Put a Panel in a free container of fixed size (e.g. 800×400); set the instance to position: absolute; left: 0; top: 0; width: 100%; height: 100%.

  3. In the Panel’s slot, add a box: position: absolute; left: 40%; top: 40%. Confirm the Position fields read 40%.

  4. Drag it a short distance → fields now read px.

  5. Contrast: in the same slot, add a free box at width: 100%; height: 100%, put another absolute box inside it at 40% / 40%, drag that one → stays %.

Relevant links:

  • Project / branch: <studio.plasmic.app URL> /
  • Host: Next.js 14 App Router, @plasmicapp/loader-nextjs 2.0.17, @plasmicapp/host ^2.0.11

GIF:
Screen Recording 2026-09-29 at 16.15.44.mov video-to-gif output image

Follow-up comment (root cause, plasmic/plasmic @ 74dbf5b)

Traced it in platform/wab. The drag-time writer updateDimProp (client/FreestyleManipulator.tsx:280-291) reuses the authored unit, so % survives the drag itself. The drop-time writer adoptFreePositionType (client/operations/insert-tpl.ts:668-678) writes px unconditionally.

Whether the drop-time one runs comes down to if (layoutParent === curLayoutParent) return; in adoptLayoutParentContainerStyle (insert-tpl.ts:447), whose comment says an unchanged parent should preserve existing styles. With a container parent both sides are the same TplTag and it returns early. With a code-component slot parent, layoutParent() builds a fresh SlotSelection on every call (shared/TplQuery.ts:655-697), so === is false even though the parent never changed, and the px reset runs. SlotSelection.equals exists (shared/core/slots.ts:65-71) but isn’t used here. The same failure at insert-tpl.ts:492-497 writes the px value to every variant setting, which is why one desktop drag breaks the other breakpoints.

Fix could be either comparing with SlotSelection.equals at :447, or having adoptFreePositionType reuse the existing unit the way updateDimProp does

Hey,

Thanks for providing the details. I’m taking a look into it.

1 Like

@jorge_pizzati Quick update that this is fixed now.