UI designer

Vue Designer Stage Component

Beta

Selection and optional absolute positioning over your own HTML.

Try it

Drag this HTML heading

Use in your application

Mark slot elements with unique data-designer-id values. v-model controls selection. positioning enables drag and resize and emits style-change { id, style }; apply it to your model. Use positioned containing blocks within the stage. Rotations and independently transformed ancestors are not supported in this beta. interactive passes clicks through. Apps own undo, persistence and export.

vue
<DomDesignerStage v-model="selected" positioning @style-change="applyStyle">
	<h2 data-designer-id="title" :style="titleStyle">Real HTML</h2>
</DomDesignerStage>
Open the complete social-card example →

Layer identity and styling

Your application creates unique layer IDs and marks the HTML with data-designer-id. The stage reads those IDs to link selection and style-change events to your model; it does not create IDs, CSS classes or stylesheet rules. Border and shadow editors update records on the selected layer, and the application binds those values to CSS. Layer z-index controls stacking only. Editor overlays and snap guides are separate from exported content.

Optional equal-spacing snaps

Set spacing-snapping to enable a separate translation snap stage. It defaults to false and works independently of snapping, which enables edge and centre alignment. Alignment wins on an axis; spacing fills only axes without an alignment match. Hold Control to bypass both. Equal spacing does not alter resizing or keyboard nudges.

Repeat an existing gap

A 10px horizontal gap with independent alignment and spacing switches. The same behaviour works vertically.

The first two layers have a 10px gap. Move the third left to repeat it. Hold Control to bypass both snap stages.

First
Second
Drag me

Third layer X: 270px. The repeated-gap target is 210px.

Install
npm install @getdom/studio
vue
<script setup>
import '@getdom/studio/style.css';
import { ref } from 'vue';
import { DomDesignerStage } from '@getdom/studio';

const selected = ref('third');
const spacing = ref(true);
const alignment = ref(true);
const layers = ref([
	{ id: 'first', label: 'First', style: { left: '30px', top: '70px', width: '80px', height: '70px' } },
	{ id: 'second', label: 'Second', style: { left: '120px', top: '70px', width: '80px', height: '70px' } },
	{ id: 'third', label: 'Drag me', style: { left: '270px', top: '70px', width: '80px', height: '70px' } },
]);

/** Applies the snapped or unsnapped geometry to application-owned layer state. */
function applyStyle({ id, style }) {
	const layer = layers.value.find((item) => item.id === id);
	if (layer) Object.assign(layer.style, style);
}
</script>

<template>
	<div class="w-full space-y-3">
		<div class="flex flex-wrap gap-4 text-sm">
			<label><input v-model="spacing" type="checkbox" /> Equal spacing</label>
			<label><input v-model="alignment" type="checkbox" /> Alignment</label>
		</div>
		<p class="text-xs text-muted-fg">The first two layers have a 10px gap. Move the third left to repeat it. Hold Control to bypass both snap stages.</p>
		<div class="overflow-auto">
			<DomDesignerStage v-model="selected" positioning :snapping="alignment" :spacing-snapping="spacing" class="relative h-[220px] w-[440px] bg-secondary" @style-change="applyStyle">
				<div v-for="layer in layers" :key="layer.id" :data-designer-id="layer.id" :style="layer.style" class="absolute grid place-items-center rounded-lg border border-border bg-canvas text-sm">{{ layer.label }}</div>
			</DomDesignerStage>
		</div>
		<p class="text-xs text-muted-fg">Third layer X: {{ layers[2].style.left }}. The repeated-gap target is 210px.</p>
	</div>
</template>

At pointer-down, the stage snapshots eligible visible layers, excluding itself, the dragged layer and its ancestors/descendants. A gap is a positive distance to the next non-overlapping neighbour in a shared row or column. The moving layer must overlap that shared row/column. Targets extend the gap before and after the pair; they do not distribute items, edit padding or enforce collision avoidance.

Spacing uses the existing 6-screen-pixel acquisition and 10-screen-pixel release distances at any zoom. Rulers show the reference and repeated gaps in document pixels. The interval sweep builds O(n log n) indexes once per gesture, with O(n) targets; pointer updates search only targets within the snap range and reuse that snapshot. Turning spacing off skips its index construction entirely.