When to Use
Driving the Studio canvas through browser automation: the drag-and-drop sequence that actually commits, and why the high-level helpers silently do nothing. Read before any skill that moves a component on the canvas via Playwright MCP.
Referenced by build-section, build-connected-template, build-freeform-template and studio-tour, which all drive the same canvas.
LLM Execution Caveat: Drag-Drop Works, but Only With the Right Sequence
Studio’s canvas is a React-DnD iframe. Palette tiles listen on mousedown / mousemove / mouseup (NOT HTML5 native drag), and the drop COMMITS only when mousemove fires intermediate events between mousedown and mouseup. The high-level dragTo() helper fires HTML5 dragstart/drop which Studio does not honor. You must use page.mouse.down() / page.mouse.move({steps}) / page.mouse.up() directly.
Stable selectors (verified by execution):
- Palette tile: [data-builder-component="true"][data-node-type="<type>"] where <type> is e.g. doc-hero, doc-card, repeater, header, box. (Section tiles use the section’s composition UID as the type.)
- Canvas iframe: [data-testid="canvas-iframe"]
- Drop slot inside the iframe: [data-composable-studio-slot="true"] (the ="true" filter is required. Without it you can match elements that have the attribute but aren’t active drop targets)
- Layers row title (to select a node for deletion or inspection): [data-testid="layer-editable-title-container"]
- Node IDs (to verify a drop committed): [data-composable-studio-id] inside the FrameLocator
The drop sequence (proven working pattern):
const item = page.locator('[data-builder-component="true"][data-node-type="doc-hero"]');
const frame = page.frameLocator('[data-testid="canvas-iframe"]');
const slot = frame.locator('[data-composable-studio-slot="true"]').first();
await item.hover(); // 1. position cursor over palette tile
await page.mouse.down(); // 2. mousedown → posts PARENT_DRAG_START to iframe
const sb = await slot.boundingBox();
await page.mouse.move(sb.x + sb.width / 2, // 3. move cursor in STEPS — required for mousemove events to fire
sb.y + sb.height / 2,
{ steps: 10 });
await slot.hover(); // 4. final settle on the slot (FrameLocator handles cross-frame)
await page.mouse.up(); // 5. mouseup → commits the drop
The page.mouse.move({steps: 10}) between mousedown and mouseup is the critical detail. Without intermediate mousemove events, the iframe’s drag-tracking code never registers the path and the drop is silently swallowed.
Anti-phantom guardrail. Always verify a NEW data-composable-studio-id appeared inside the FrameLocator after each drop:
const idsBefore = await frame.locator('[data-composable-studio-id]')
.evaluateAll(els => els.map(e => e.getAttribute('data-composable-studio-id')));
// ... drop sequence ...
await page.waitForTimeout(800);
const idsAfter = await frame.locator('[data-composable-studio-id]')
.evaluateAll(els => els.map(e => e.getAttribute('data-composable-studio-id')));
const newIds = idsAfter.filter(id => !idsBefore.includes(id));
if (newIds.length === 0) {
throw new Error('Drop did not commit; do not continue.');
}
If newIds.length === 0: stop and surface the failure. Do not fabricate completion.
Sibling drops after the root slot is consumed. Once a component is dropped at the canvas root, [data-composable-studio-slot="true"] may return zero matches because the root slot is now occupied. To add siblings, hover the edge of an existing node. Studio reveals a drop indicator there. Alternatively wrap children in a container (box, vstack, hstack) and drop subsequent siblings into the container’s slot.
Execution-path matrix:
| Path | Drag-drop status |
|---|---|
| Human in their own Studio browser | Yes. Native: this is how authors use Studio every day |
| Playwright with direct page.mouse.down/move/up access | Yes. Use the proven sequence above |
| Playwright dragTo() only | No. Fires HTML5 drag events Studio does not honor |
| Synthetic DragEvent dispatched from page-context JS | No. Same reason |
What ALSO works programmatically (verified):
- Click a Layers row and press Delete, which removes the node and persists
- Click the Save button, which persists the composition. The button grays out post-save
- Switch right-panel tabs (Settings / Design / Data) via direct DOM clicks
- Open Configuration / URL Pattern / Schema Picker modals via their action buttons
- Read iframe canvas state via frameLocator (read-only operations)
- Switch palette accordion sections (Basic / Media / Container / Smart Containers / Registered Components / HTML Elements) via direct DOM clicks