<div class="workspace" id="workspace">
<!-- The SVG layer sits completely behind the HTML nodes -->
<svg class="svg-layer" id="svg-layer"></svg>
<!-- Root Node -->
<div class="flow-node" id="node-root" style="left: 400px; top: 100px;">
<div class="node-header">
<div class="editable" contenteditable="true">Start Node</div>
</div>
<div class="node-body editable" contenteditable="true">Project Kickoff</div>
<button class="add-node-btn">+</button>
</div>
</div>*, *::before, *::after {
box-sizing: border-box;
}
body {
font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
margin: 0;
background-color: #020617;
background-image: radial-gradient(circle, #1e293b 1px, transparent 1px);
background-size: 30px 30px;
min-height: 100vh;
overflow: hidden;
}
.workspace {
position: relative;
width: 100vw;
height: 100vh;
}
.svg-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
.flow-line {
fill: none;
stroke: #0ea5e9;
stroke-width: 3;
stroke-dasharray: 8 8;
animation: dashAnim 1s linear infinite;
filter: drop-shadow(0 0 6px rgba(14, 165, 233, 0.6));
}
@keyframes dashAnim {
to { stroke-dashoffset: -16; }
}
.flow-node {
position: absolute;
width: 240px;
background: rgba(15, 23, 42, 0.9);
backdrop-filter: blur(12px);
border: 1px solid rgba(14, 165, 233, 0.4);
border-top: 4px solid #0ea5e9;
border-radius: 12px;
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.5);
z-index: 10;
cursor: grab;
user-select: none;
display: flex;
flex-direction: column;
transition: box-shadow 0.2s, border-color 0.2s;
}
.flow-node:hover {
border-color: #38bdf8;
}
.flow-node:active {
cursor: grabbing;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.8);
}
.node-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.05);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.editable {
color: #f8fafc;
font-weight: 600;
outline: none;
padding: 4px;
border-radius: 4px;
border: 1px solid transparent;
transition: border-color 0.2s, background 0.2s;
cursor: text;
flex-grow: 1;
}
.editable:focus {
border-color: rgba(14, 165, 233, 0.5);
background: rgba(0, 0, 0, 0.3);
}
.delete-btn {
background: transparent;
color: #94a3b8;
border: none;
font-size: 1.25rem;
cursor: pointer;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
justify-content: center;
align-items: center;
transition: background 0.2s, color 0.2s;
margin-left: 8px;
}
.delete-btn:hover {
background: #ef4444;
color: #ffffff;
}
.node-body {
padding: 16px;
color: #cbd5e1;
font-size: 0.95rem;
font-weight: 400;
line-height: 1.5;
margin-bottom: 8px; /* Room for the add button */
}
.add-node-btn {
position: absolute;
bottom: -16px;
left: 50%;
transform: translateX(-50%);
width: 32px;
height: 32px;
border-radius: 50%;
background: #10b981;
color: #ffffff;
border: 3px solid #0f172a;
display: flex;
justify-content: center;
align-items: center;
font-size: 1.25rem;
font-weight: bold;
cursor: pointer;
box-shadow: 0 4px 10px rgba(0,0,0,0.4);
transition: background 0.2s, transform 0.2s;
z-index: 20;
}
.add-node-btn:hover {
background: #059669;
transform: translateX(-50%) scale(1.1);
}const workspace = document.getElementById("workspace");
const svgLayer = document.getElementById("svg-layer");
let activeNode = null;
let offsetX = 0;
let offsetY = 0;
let nodeCounter = 1;
function drawLines() {
while (svgLayer.firstChild) {
svgLayer.removeChild(svgLayer.firstChild);
}
const nodes = document.querySelectorAll(".flow-node");
for (let i = 0; i < nodes.length; i++) {
const child = nodes.item(i);
const parentId = child.getAttribute("data-parent");
if (parentId) {
const parent = document.getElementById(parentId);
if (parent) {
const r1 = parent.getBoundingClientRect();
const r2 = child.getBoundingClientRect();
// Bottom center of parent
const c1x = r1.left + (r1.width / 2);
const c1y = r1.top + r1.height;
// Top center of child
const c2x = r2.left + (r2.width / 2);
const c2y = r2.top;
// Control points for smooth bezier curve
const cp1x = c1x;
const cp1y = c1y + 50;
const cp2x = c2x;
const cp2y = c2y - 50;
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("class", "flow-line");
path.setAttribute("d", "M " + c1x + " " + c1y + " C " + cp1x + " " + cp1y + ", " + cp2x + " " + cp2y + ", " + c2x + " " + c2y);
svgLayer.appendChild(path);
}
}
}
}
function deleteNodeAndChildren(nodeId) {
const node = document.getElementById(nodeId);
if (node) {
node.remove();
}
// Recursively find and delete any children attached to this node
const nodes = document.querySelectorAll(".flow-node");
for (let i = 0; i < nodes.length; i++) {
const child = nodes.item(i);
if (child.getAttribute("data-parent") === nodeId) {
deleteNodeAndChildren(child.id);
}
}
}
function startDrag(e, node) {
// Prevent dragging when clicking interactive elements
if (e.target.classList.contains("editable") || e.target.classList.contains("delete-btn") || e.target.classList.contains("add-node-btn")) {
return;
}
activeNode = node;
const rect = node.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
node.style.zIndex = 100;
}
function attachListeners(node) {
node.addEventListener("pointerdown", function(e) {
startDrag(e, node);
});
const delBtn = node.querySelector(".delete-btn");
if (delBtn) {
delBtn.addEventListener("click", function() {
deleteNodeAndChildren(node.id);
drawLines();
});
}
const addBtn = node.querySelector(".add-node-btn");
if (addBtn) {
addBtn.addEventListener("click", function() {
createNewNode(node.id);
});
}
const editables = node.querySelectorAll(".editable");
for (let i = 0; i < editables.length; i++) {
editables.item(i).addEventListener("pointerdown", function(e) {
e.stopPropagation();
});
editables.item(i).addEventListener("input", function() {
drawLines();
});
}
}
function createNewNode(parentId) {
nodeCounter++;
const parent = document.getElementById(parentId);
const pr = parent.getBoundingClientRect();
const newNode = document.createElement("div");
const newId = "node-" + nodeCounter;
newNode.id = newId;
newNode.className = "flow-node";
newNode.setAttribute("data-parent", parentId);
// Offset new node slightly below and randomly to the side of the parent
const newX = pr.left + (Math.random() * 100 - 50);
const newY = pr.top + 150;
newNode.style.left = newX + "px";
newNode.style.top = newY + "px";
const header = document.createElement("div");
header.className = "node-header";
const title = document.createElement("div");
title.className = "editable";
title.setAttribute("contenteditable", "true");
title.innerText = "Step " + nodeCounter;
const delBtn = document.createElement("button");
delBtn.className = "delete-btn";
delBtn.innerText = "×";
header.appendChild(title);
header.appendChild(delBtn);
const bodyDiv = document.createElement("div");
bodyDiv.className = "node-body editable";
bodyDiv.setAttribute("contenteditable", "true");
bodyDiv.innerText = "Enter details here...";
const addBtn = document.createElement("button");
addBtn.className = "add-node-btn";
addBtn.innerText = "+";
newNode.appendChild(header);
newNode.appendChild(bodyDiv);
newNode.appendChild(addBtn);
workspace.appendChild(newNode);
attachListeners(newNode);
drawLines();
}
// Attach listeners to the initial root node
const rootNode = document.getElementById("node-root");
attachListeners(rootNode);
window.addEventListener("pointermove", function(e) {
if (!activeNode) return;
const x = e.clientX - offsetX;
const y = e.clientY - offsetY;
activeNode.style.left = x + "px";
activeNode.style.top = y + "px";
drawLines();
});
window.addEventListener("pointerup", function() {
if (activeNode) {
activeNode.style.zIndex = 10;
activeNode = null;
}
});
window.addEventListener("resize", drawLines);
setTimeout(drawLines, 100);Nothing to show yetType HTML, CSS or JavaScript and your page appears here.
Click any element to find its code Esc
Check this code before you run it
It came from a shared link. It runs with fewer permissions, so it cannot open pop-ups, submit forms or start downloads.
0.00s / 0.00s
Nothing has animated yet. Hover, click or type in the preview and each CSS animation, transition or
element.animate() call shows up here as it plays.