Layout Canvas
Live Demos HubLayout Canvas
Interactive production-grade hypermedia component for HTMXUI.
Interactive Preview
x: 128, y: 128
Node: Trigger
When user clicks the button.
x: 512, y: 224
Node: Action
Send welcome email payload.
HTML Source
<div id="demo-canvas" hx-canvas="true" hx-snap="20" class="relative w-full h-[600px] border border-border rounded-lg overflow-hidden bg-muted/20"
style="background-image: radial-gradient(circle, #cbd5e1 1px, transparent 1px); background-size: 20px 20px;">
<!-- Toolbar -->
<div class="absolute top-4 left-1/2 -translate-x-1/2 flex items-center gap-2 bg-background/80 backdrop-blur border border-border rounded-full p-2 shadow-sm z-20">
<select id="component-spawner" onchange="spawnNode(this.value); this.value='';" class="h-8 rounded-md border border-input bg-background text-sm px-2 cursor-pointer hover:bg-muted">
<option value="">+ Spawn Component</option>
<option value="text">Text (Editable)</option>
<option value="shape">Shape</option>
<option value="image">Image</option>
<option value="button">UI: Button</option>
<option value="card">UI: Card</option>
<option value="badge">UI: Badge</option>
<option value="input">UI: Input</option>
</select>
<div class="w-px h-5 bg-border"></div>
<button onclick="document.getElementById('demo-canvas').innerHTML = ''" class="h-8 w-8 rounded-full hover:bg-accent flex items-center justify-center text-sm text-destructive" title="Clear Canvas">🗑️</button>
</div>
<!-- Mini Map -->
<div class="absolute bottom-4 right-4 w-32 h-24 bg-background border border-border rounded-md shadow-sm z-20 p-2 opacity-80 hover:opacity-100 transition-opacity pointer-events-none">
<div class="w-full h-full border border-primary/20 rounded relative">
<div class="absolute top-2 left-2 w-10 h-6 border-2 border-primary bg-primary/10 rounded-sm"></div>
</div>
</div>
<!-- Initial Nodes -->
<div id="node-1" hx-drag="true" class="absolute top-32 left-32 w-64 bg-background border border-border rounded-lg shadow-md transition-shadow hover:shadow-lg z-10 group">
<div class="absolute -top-6 left-0 text-[10px] bg-primary text-primary-foreground px-1.5 py-0.5 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50">
x: <span class="coord-x">128</span>, y: <span class="coord-y">128</span>
</div>
<div hx-drag-handle="true" class="px-4 py-2 border-b border-border font-medium text-sm flex justify-between items-center cursor-move bg-muted/30 select-none">
Node: Trigger <span class="h-2 w-2 rounded-full bg-green-500"></span>
</div>
<div class="p-4 space-y-2 text-sm text-muted-foreground">
<p>When user clicks the button.</p>
</div>
</div>
<svg id="canvas-svg" class="absolute inset-0 pointer-events-none w-full h-full z-0">
<path hx-connect="true" hx-connect-from="node-1" hx-connect-to="node-2"
fill="none" stroke="currentColor" class="text-primary" stroke-width="2" stroke-dasharray="4"/>
</svg>
<div id="node-2" hx-drag="true" class="absolute top-56 left-[512px] w-64 bg-background border border-border rounded-lg shadow-md border-primary/50 transition-shadow hover:shadow-lg z-10 group">
<div class="absolute -top-6 left-0 text-[10px] bg-primary text-primary-foreground px-1.5 py-0.5 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50">
x: <span class="coord-x">512</span>, y: <span class="coord-y">224</span>
</div>
<div hx-drag-handle="true" class="px-4 py-2 border-b border-border font-medium text-sm flex justify-between items-center cursor-move bg-muted/30 select-none">
Node: Action <span class="h-2 w-2 rounded-full bg-blue-500"></span>
</div>
<div class="p-4 space-y-2 text-sm text-muted-foreground">
<p>Send welcome email payload.</p>
</div>
</div>
</div>
<script>
let nodeCounter = 3;
function spawnNode(type) {
if (!type) return;
const canvas = document.getElementById('demo-canvas');
const id = 'node-' + (nodeCounter++);
const offset = (nodeCounter * 20) % 200;
let content = '';
let handle = '';
let extraClass = '';
if (type === 'text') {
content = `<div contenteditable="true" class="outline-none text-2xl font-bold p-2 min-w-[150px] focus:bg-accent/20 rounded cursor-text">Editable Text</div>`;
} else if (type === 'shape') {
content = `<div class="w-32 h-32 bg-primary/20 border-2 border-primary rounded-xl flex items-center justify-center font-medium">Shape</div>`;
} else if (type === 'image') {
content = `<div class="w-48 h-32 bg-muted rounded-xl flex items-center justify-center overflow-hidden border border-border"><img src="https://images.unsplash.com/photo-1707343843437-caacff5cfa74?q=80&w=400&auto=format&fit=crop" class="w-full h-full object-cover pointer-events-none"/></div>`;
} else if (type === 'button') {
content = `<button class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors bg-primary text-primary-foreground shadow hover:bg-primary/90 h-9 px-4 py-2 pointer-events-none">Button Component</button>`;
extraClass = 'p-1 hover:border-dashed hover:border-primary/50 border-2 border-transparent rounded';
} else if (type === 'card') {
content = `<div class="rounded-xl border border-border bg-card text-card-foreground shadow w-[250px]"><div class="p-6"><h3 class="font-semibold leading-none tracking-tight">Card Component</h3><p class="text-sm text-muted-foreground mt-2">Drop other components into me!</p></div></div>`;
extraClass = 'p-1 hover:border-dashed hover:border-primary/50 border-2 border-transparent rounded-xl';
} else if (type === 'badge') {
content = `<div class="inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80 pointer-events-none">Badge</div>`;
extraClass = 'p-1 hover:border-dashed hover:border-primary/50 border-2 border-transparent rounded';
} else if (type === 'input') {
content = `<input type="text" placeholder="Input component..." class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm pointer-events-none"/>`;
extraClass = 'p-1 hover:border-dashed hover:border-primary/50 border-2 border-transparent rounded';
}
const startX = 100 + offset;
const startY = 100 + offset;
const el = document.createElement('div');
el.id = id;
el.setAttribute('hx-drag', 'true');
el.className = `absolute z-10 group cursor-move ${extraClass}`;
el.style.top = startY + 'px';
el.style.left = startX + 'px';
el.innerHTML = `
<div class="absolute -top-6 left-0 text-[10px] bg-primary text-primary-foreground px-1.5 py-0.5 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50">
x: <span class="coord-x">${startX}</span>, y: <span class="coord-y">${startY}</span>
</div>
${content}
`;
canvas.appendChild(el);
}
</script>