Designer composable
useLayoutGeometry
BetaWhere layers sit and how they move: drags, X/Y/W/H, and align and distribute for one layer or several.
What it does
It measures layers as the browser renders them and writes their new positions through the session, so every move is undoable and reaches the code. It reads each frame’s layout from its classes, so a frame made absolute with Tailwind behaves like one set in the designer.
Locked layers (node.locked) stay where they are: drags, X/Y/W/H and align skip them, though they still count toward a selection’s bounds. Hidden layers (node.hidden) are left out of the canvas but stay in the code, and keep the place their classes give them.
Positions apply to layers in an absolute frame. Moving one adopts the frame first: every child’s current box is recorded, so nothing else jumps. Layers in flow, flex and grid frames are reordered on the stage instead.
Example
Try it
Click a box. Shift-click to select several, then align them or drag them together.
npm install @getdom/studio<script setup>
import '@getdom/studio/style.css';
import { onMounted, ref } from 'vue';
import { DomButton, DomDesignerStage } from '@getdom/studio';
import { DomEditorRender, createComponentCatalog, createEditorTypes, createWorkspace, useEditorSession, useLayoutGeometry } from '../../../lib/designer';
const source = `<template>
<div class="relative h-64 bg-secondary/40">
<div class="absolute left-[24px] top-[32px] grid h-12 w-16 place-items-center rounded-lg bg-primary text-primary-fg">A</div>
<div class="absolute left-[140px] top-[120px] grid h-20 w-14 place-items-center rounded-lg bg-primary text-primary-fg">B</div>
<div class="absolute left-[300px] top-[64px] grid h-14 w-24 place-items-center rounded-lg bg-primary text-primary-fg">C</div>
</div>
</template>`;
// Ids are counted, so a page rendered on the server matches when it hydrates.
let next = 0;
const session = useEditorSession({
workspace: createWorkspace([{ name: 'Boxes.vue', source }], { widths: [480], makeId: () => `box${next++}` }),
afterChange: () => layout.refresh(),
});
const { selected, selection, selectedIds, doc, historySize, undo, redo } = session;
const types = createEditorTypes(createComponentCatalog([]));
// Measures layers on the stage and moves them through the session.
const stage = ref(null);
const layout = useLayoutGeometry({ session, element: (id) => stage.value?.$el.querySelector(`[data-designer-id="${id}"]`) || null });
const { detected, canArrange, selectedNodes } = layout;
onMounted(() => layout.refresh());
const alignments = [['left', 'Left'], ['center', 'Centre'], ['right', 'Right'], ['top', 'Top'], ['middle', 'Middle'], ['bottom', 'Bottom']];
</script>
<template>
<div class="min-w-0 overflow-hidden rounded-xl border border-border">
<div class="flex flex-wrap items-center gap-1 border-b border-border px-2 py-1.5">
<DomButton v-for="[name, label] in alignments" :key="name" size="sm" variant="ghost" :disabled="!canArrange" @click="layout.align(name)">{{ label }}</DomButton>
<DomButton size="sm" variant="ghost" :disabled="!canArrange || selectedNodes.length < 3" @click="layout.distribute('horizontal')">Distribute</DomButton>
<span class="flex-1"></span>
<DomButton size="sm" variant="ghost" :disabled="!historySize.undo" @click="undo">Undo</DomButton>
<DomButton size="sm" variant="ghost" :disabled="!historySize.redo" @click="redo">Redo</DomButton>
</div>
<DomDesignerStage ref="stage" v-model="selected" v-model:selection="selection" positioning frame-scoped snapping @style-change="layout.patch">
<DomEditorRender :doc="doc" :types="types" :detected="detected" />
</DomDesignerStage>
<p class="border-t border-border px-3 py-2 text-xs text-muted-fg">{{ selectedIds.length ? `${selectedIds.length} selected.` : 'Click a box.' }} Shift-click to select several, then align them or drag them together.</p>
</div>
</template>
Use it
<script setup>
import { DomDesignerStage } from '@getdom/studio/vue';
import { DomEditorRender, useEditorSession, useLayoutGeometry } from '@getdom/studio/designer';
const session = useEditorSession({ workspace, afterChange: () => layout.refresh() });
const { selected, selection, doc } = session;
const layout = useLayoutGeometry({ session, element: (id) => findRendered(id) });
const { detected, canArrange } = layout;
</script>
<template>
<DomDesignerStage v-model="selected" v-model:selection="selection" positioning frame-scoped @style-change="layout.patch">
<DomEditorRender :doc="doc" :types="types" :detected="detected" />
</DomDesignerStage>
<button :disabled="!canArrange" @click="layout.align('top')">Align top</button>
<button :disabled="!canArrange" @click="layout.distribute('horizontal')">Distribute</button>
</template>Multiple selection lives in the session: selected is the primary layer and selection holds them all. Bind v-model:selection on the stage and v-model:values on DomDesignerLayers, and Shift-click works in both.
API
Options
| Name | Type | Default | Description |
|---|---|---|---|
| session | EditorSession | — | The session whose selection is measured and moved. |
| element | (id) => HTMLElement | null | — | The rendered element for a node id, in the artboard being edited. |
| isFrame | (node) => boolean | HTML containers | Whether a node lays out children. By default div, section, article and the other HTML containers. |
Returns
| Name | Type | Default | Description |
|---|---|---|---|
| align(alignment) | function | — | left, center, right, top, middle or bottom. One layer aligns inside its frame’s padding. Several align to the edges or centres of their own bounds, as in Figma. One undo step. |
| distribute(axis) | function | — | horizontal or vertical. Three or more layers get equal gaps; the outer two stay where they are. |
| canArrange | ComputedRef<boolean> | — | True when the selection shares an absolute frame, so align and distribute can place it by X and Y. |
| patch(event) | function | — | Applies a stage style-change: a drag, resize or arrow-key nudge. Layers moved together are one undo step. |
| setGeometry(key, value) | function | — | Sets left, top, width or height on the primary layer, for X, Y, W and H fields. |
| geometry | ComputedRef | — | The primary layer’s box in its frame: its placement, or else as rendered. |
| measure(id) | function | — | Any layer’s rendered box in its parent frame. |
| selectedNodes | ComputedRef | — | The selected layers, without any that sit inside another selected layer. |
| sharedParent | ComputedRef | — | The frame every selected layer shares, or null. |
| detected | Ref | — | Each frame’s layout as the browser resolves it from its classes. Pass to DomEditorRender. |
| refresh() | function | — | Reads frame layouts again after the DOM settles. Call it after changes and when an artboard renders. |
| layoutOf(id), modeOf(id) | function | — | A frame’s layout and its mode: flow, flex, grid or absolute. |
| parentMode, parentLayout | ComputedRef | — | The same for the primary layer’s parent, for an inspector. |
| captureOperations, adoptOperations | function | — | Operations that record children’s boxes, so a frame can switch to absolute without anything moving. |
The maths on its own
The composable is built on plain functions over rectangles. Use them in any editor, such as one that keeps positions in its own data.
import { alignRects, boundsOf, distributeRects } from '@getdom/studio/designer';
const rects = [{ id: 'a', left: 10, top: 20, width: 40, height: 40 }, { id: 'b', left: 100, top: 90, width: 48, height: 64 }];
alignRects(rects, 'top'); // both at top 20, the top of their bounds
alignRects(rects, 'left', frameBox); // both against the left of frameBox
distributeRects([...rects, c], 'horizontal'); // equal gaps, outer two stay putFunctions
| Name | Type | Default | Description |
|---|---|---|---|
| boundsOf(rects) | function | — | The smallest box around every rectangle. |
| alignRects(rects, alignment, bounds?) | function | — | New rectangles aligned to a line of bounds (the rectangles’ own by default), rounded to whole pixels. |
| distributeRects(rects, axis) | function | — | New rectangles with equal gaps along an axis, in the input order. |
| measureFrameChild(element, frame) | function | — | An element’s box in its frame’s CSS pixels, allowing for borders, scrolling and zoom. |
Related
Snapping while dragging is part of DomDesignerStage. All composables →