About this snippet

Map out your ideas with our advanced drag and drop flowchart builder. Add nodes dynamically from any step, edit text inline, and watch the glowing SVG lines adapt instantly.

Size
8.4 KB
Uses
HTML, CSS, JavaScript
Loads
Nothing external
Views
208
<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);
Esc
↑↓ moveEnter select

Start from a template

Open an .html file, or HTML, CSS, JS, Markdown, Less or TypeScript files. You can also drop them here. Styles and scripts go into their panes.

Share

Source

Keyboard shortcuts

Save