#1829·puck

0.23: canvas per-item overlay (Duplicate/Delete, overrides.actionBar) never mounts, though selection state updates correctly

Author: brossifficCreated Sep 4, 2026Updated Sep 17, 2026
Labelstype: bug 🐛in triage

Summary

In @puckeditor/[email protected], selecting a component on the canvas correctly updates state.ui.itemSelector / usePuck().selectedItem and correctly updates the right-hand Fields panel — but the canvas's own per-item floating overlay (the one that hosts the native Duplicate/Delete buttons, and anything added via overrides.actionBar) never mounts. No error is thrown; the overlay's container ([data-puck-overlay] in the shipped bundle) simply never appears in the DOM.

Reproduction

Minimal config: a DropZone-based root component (legacy zones, not slots) with a couple of plain, non-resolving child components. No custom overrides, no plugins.

  1. Render <Puck config={config} data={data} onPublish={...} /> with iframe left at its default (enabled).
  2. Click any placed component on the canvas.
  3. Observe:
    • usePuck().appState.ui.itemSelector updates correctly to {index, zone} for the clicked node (verified with a debug hook reading it live).
    • The Fields panel (right sidebar) correctly re-renders to that component's fields.
    • No overlay appears. No Duplicate/Delete buttons, no drag handle chip, nothing — document.querySelectorAll('[data-puck-overlay]') is 0 both before and after the click.

Reproduced with:

  • Two different documents/templates (different component trees).
  • A component with no resolver/binding at all (a plain text/heading-style block) and one with an async data-fetching child — same result either way, ruling out a resolver/Suspense interaction.
  • Headless Chromium, headed Chromium under Xvfb, real mouse hover+move+click sequences, and a programmatic dispatch({type:"setUi", ui:{itemSelector}}) — same result every way.
  • With overrides.actionBar entirely absent from the config (so this is not specific to a custom override — Puck's own default Duplicate/Delete overlay has the identical symptom).

What I found tracing the shipped bundle

(Line refs are against the published @puckeditor/[email protected] npm tarball's dist/chunk-*.mjs, which will not match the source tree 1:1 — offered as a lead, not a citation.)

  • DraggableComponent's isSelected is a live store subscription (s.selectedItem?.props.id === componentId) and does flip correctly — confirmed via a debug hook.
  • The overlay's visibility is local [isVisible, setIsVisible] state, set inside a useEffect that is itself wrapped in startTransition:
    javascript
    useEffect(() => {
      startTransition(() => {
        if (hover || indicativeHover || isSelected) {
          scheduleSync();
          setIsVisible(true);
          ...
        } else {
          setIsVisible(false);
        }
      });
    }, [hover, indicativeHover, isSelected, iframe]);
  • The JSX gate for the overlay's createPortal(...) is dragFinished && isVisible. dragFinished defaults to true (useState(true)), so it should not be the blocker.
  • Despite isSelected becoming true (confirmed), isVisible never appears to commit — the overlay is simply absent from the DOM, with no console error/warning.

I was not able to narrow this further without instrumenting the built package itself, but the startTransition-wrapped setIsVisible looks like the most likely site to a reader — a transition that never commits (React 18) would exactly produce "the store state is right, everything reading it via a normal subscription re-renders, but this one local useState inside a startTransition never visibly updates."

Environment

  • @puckeditor/core: 0.23.0
  • React: 18.3.1
  • iframe: left at default (enabled)
  • Zones: legacy DropZone/zones shape (not slots) — Data.zones?: Record<string, Content>
  • Chromium (Playwright-driven, both headless and headed/Xvfb) and manual testing in a normal desktop browser session, same result

Impact

Any overrides.actionBar consumer (and Puck's own default Duplicate/Delete affordance) is effectively invisible on selection in this configuration — the feature works entirely at the state layer but has no on-canvas entry point.

Happy to share a minimal reproduction repo if useful — this was found while integrating overrides.actionBar into a production editor and isolating why a newly-added action never appeared.