NavigationFree

Connected Nodes

Four colour-coded nodes you drag with the mouse or a finger, linked by SVG lines that follow them in real time — an interactive graph in HTML, CSS and vanilla JavaScript, no library.

SVGJSDrag

Updated

A
B
C
D
Hover or click the scene to interact

This effect is free — the Navigation category contains 37 effects total, including 4 free. Effect.Labs has 840 vanilla effects. Explore the category →

Usage examples

Service topology 4 services live
4 services 4 active connections Drag a node to explore

① Dashboard — Microservice topology map

WhenIn a monitoring or architecture panel to display dependencies between services (API, database, cache, message queue) without loading a third-party graph library.
WhyThe SVG + CSS structure lays out the graph; the built-in drag-and-drop lets you rearrange the topology by hand to explore it. Dark background and domain-coded colours convey service type at a glance.
SettingsDefault CSS percentage positions kept, stroke-width: 2, stroke-dasharray: 6 4, 48 px nodes, default palette (indigo API, pink DB, green Cache, amber Queue).
My workspace › Concept map — Article

② Content editor — Article concept map

WhenIn a writing tool or wiki to visualize an article's structure (concepts, sections, themes) and links between ideas before drafting, with a central pivot node.
WhyA hub-and-spoke topology (one central node connected to the others) naturally models a mind map; dashed SVG lines stay readable on light or dark backgrounds without cluttering the interface.
SettingsHub node centered (top: 40%, left: 40%), stroke-width: 1 for a lighter look, short labels (4–5 chars max), colors aligned to the editorial brand.
⚙ Data pipeline

③ Low-code tool — Data processing pipeline editor

WhenIn a no-code interface where users visually connect processing blocks (Source → Filter → Transform → Output) by drag-and-drop linking.
WhyNodes and SVG lines are the UI primitive of any workflow editor; updating SVG coordinates on drag is native to the DOM — no canvas or third-party library required.
Settings4 nodes arranged diagonally, linear connections (a→b→c→d, no return d→a), short labels for processing steps (SRC, MAP, FLT, OUT).

How it works

Each node is a <div class="node-point"> set to position: absolute inside a .nodes-scene container that fills 100 % × 100 % of its parent. Initial coordinates are declared as percentages via the classes .node-a through .node-d (e.g. top: 20%; left: 15%) — no hard-coded pixel values. Each node carries a 135° linear gradient (two tones of the same hue) to simulate a subtle bevel; user-select: none prevents text selection during dragging.

A <svg class="node-lines"> covers the whole scene at position: absolute; inset: 0 with pointer-events: none to never block clicks on the nodes below. The four <line> elements (a→b, b→c, c→d, d→a) define the connections: drawn in light indigo (stroke: rgb(129,140,248)), width 2, stroke-dasharray: 6 4 (6 px dash, 4 px gap), opacity 0.6.

On hover, each node emits a glow halo via box-shadow with a 0.2s transition. The grab cursor (and grabbing while dragging, via :active) tells users the node can be moved.

The script computes each node's centre with getBoundingClientRect() and pins the x1/y1/x2/y2 endpoints of the four lines to it, on load and again 100 ms later (once percentage positions are applied). A mousedown or touchstart on a node grabs it; every mousemove/touchmove moves it — kept inside the scene — and recomputes the four lines, so they follow the node with no lag. Releasing (mouseup/touchend) drops it. It all fits in an IIFE of about 70 lines, with no global variable.

Accessibility

  • No prefers-reduced-motion handling in the code — the only animation is transition: box-shadow 0.2s on hover, non-repetitive; dragging simply follows the user's hand. Add @media (prefers-reduced-motion: reduce) { .node-point { transition: none } } to remove the animated glow.
  • Dragging works with mouse and touch, not the keyboard: nodes are <div> elements with no tabindex or role. For an accessible graph, add tabindex="0", role="button", move the active node with arrow keys, then call updateLines().
  • The <svg> lines element is purely decorative but lacks aria-hidden="true" — add it to prevent screen readers from traversing it unnecessarily.
  • Contrast fails in the default configuration: white text on amber (#f59e0b) and pink (#ec4899) backgrounds does not reach 4.5:1 WCAG AA at 0.75 rem — use dark text (color:#1e1b4b) or deeper background tones.

Browser compatibility

CSS + native SVG + about 70 lines of vanilla JavaScript (mouse and touch) — zero external dependencies, no canvas element.

Chrome 88+✓ Full
Firefox 87+✓ Full
Safari 15+✓ Full
Edge 88+✓ Full
Mobile iOS✓ Full — drag with a finger
Android Chrome✓ Full

Without JavaScript, the colour-coded nodes show in place but can't be moved, and the lines stay invisible (their endpoints aren't computed). If SVG is unsupported, only the lines disappear.

The code

Copy the three blocks into your page. No dependencies.

HTML
<div class="anchor-demo">
    <div class="nodes-scene" id="nodesScene">
      <svg class="node-lines">
        <line id="line-ab" x1="0" y1="0" x2="0" y2="0"/>
        <line id="line-bc" x1="0" y1="0" x2="0" y2="0"/>
        <line id="line-cd" x1="0" y1="0" x2="0" y2="0"/>
        <line id="line-da" x1="0" y1="0" x2="0" y2="0"/>
      </svg>
      <div class="node-point node-a" data-node="a">A</div>
      <div class="node-point node-b" data-node="b">B</div>
      <div class="node-point node-c" data-node="c">C</div>
      <div class="node-point node-d" data-node="d">D</div>
    </div>
  </div>
CSS
.anchor-demo  {
   position: relative;
   width: 100%;
   height: 220px;
   display: flex;
   align-items: center;
   justify-content: center;
   overflow: hidden;
   }

.anchor-demo *  {
   box-sizing: border-box;
   }

.nodes-scene  {
   position: relative;
   width: 100%;
   height: 100%;
   }

.nodes-scene svg.node-lines  {
   position: absolute;
   inset: 0px;
   width: 100%;
   height: 100%;
   pointer-events: none;
   }

.nodes-scene svg.node-lines line  {
   stroke: rgb(129, 140, 248);
   stroke-width: 2;
   stroke-dasharray: 6, 4;
   opacity: 0.6;
   }

.node-point  {
   position: absolute;
   width: 48px;
   height: 48px;
   border-radius: 50%;
   display: flex;
   align-items: center;
   justify-content: center;
   font-size: 0.75rem;
   font-weight: 700;
   color: rgb(255, 255, 255);
   cursor: grab;
   user-select: none;
   z-index: 2;
   transition: box-shadow 0.2s;
   }

.node-point:active  {
   cursor: grabbing;
   }

.node-point:hover  {
   box-shadow: rgba(129, 140, 248, 0.6) 0px 0px 20px;
   }

.node-a  {
   background: linear-gradient(135deg, rgb(99, 102, 241), rgb(129, 140, 248));
   top: 20%;
   left: 15%;
   }

.node-b  {
   background: linear-gradient(135deg, rgb(236, 72, 153), rgb(244, 114, 182));
   top: 60%;
   left: 55%;
   }

.node-c  {
   background: linear-gradient(135deg, rgb(16, 185, 129), rgb(52, 211, 153));
   top: 25%;
   left: 70%;
   }

.node-d  {
   background: linear-gradient(135deg, rgb(245, 158, 11), rgb(251, 191, 36));
   top: 70%;
   left: 20%;
   }
JavaScript (fx-0476)
(function() {
  // ============================================
  // 2. Connected Nodes - draggable with SVG lines
  // ============================================
  (function(){
    var scene = document.getElementById('nodesScene');
    if(!scene) return;
    var nodes = scene.querySelectorAll('.node-point');
    var lineAb = document.getElementById('line-ab');
    var lineBc = document.getElementById('line-bc');
    var lineCd = document.getElementById('line-cd');
    var lineDa = document.getElementById('line-da');
    if(!lineAb || !lineBc || !lineCd || !lineDa) return;
    var lines = { ab: lineAb, bc: lineBc, cd: lineCd, da: lineDa };
    var nodeMap = {};
    nodes.forEach(function(n){ nodeMap[n.dataset.node] = n; });

    function getCenter(el){
      var r = el.getBoundingClientRect();
      var sr = scene.getBoundingClientRect();
      return { x: r.left - sr.left + r.width/2, y: r.top - sr.top + r.height/2 };
    }
    function updateLines(){
      var a=getCenter(nodeMap.a), b=getCenter(nodeMap.b), c=getCenter(nodeMap.c), d=getCenter(nodeMap.d);
      lines.ab.setAttribute('x1',a.x); lines.ab.setAttribute('y1',a.y);
      lines.ab.setAttribute('x2',b.x); lines.ab.setAttribute('y2',b.y);
      lines.bc.setAttribute('x1',b.x); lines.bc.setAttribute('y1',b.y);
      lines.bc.setAttribute('x2',c.x); lines.bc.setAttribute('y2',c.y);
      lines.cd.setAttribute('x1',c.x); lines.cd.setAttribute('y1',c.y);
      lines.cd.setAttribute('x2',d.x); lines.cd.setAttribute('y2',d.y);
      lines.da.setAttribute('x1',d.x); lines.da.setAttribute('y1',d.y);
      lines.da.setAttribute('x2',a.x); lines.da.setAttribute('y2',a.y);
    }
    updateLines();

    var dragging = null, offsetX = 0, offsetY = 0;
    nodes.forEach(function(n){
      n.addEventListener('mousedown', function(e){
        e.preventDefault();
        dragging = n;
        var r = n.getBoundingClientRect();
        offsetX = e.clientX - r.left;
        offsetY = e.clientY - r.top;
      });
      n.addEventListener('touchstart', function(e){
        e.preventDefault();
        dragging = n;
        var r = n.getBoundingClientRect();
        var t = e.touches[0];
        offsetX = t.clientX - r.left;
        offsetY = t.clientY - r.top;
      }, {passive:false});
    });
    function onMove(cx, cy){
      if(!dragging) return;
      var sr = scene.getBoundingClientRect();
      var x = cx - sr.left - offsetX;
      var y = cy - sr.top - offsetY;
      x = Math.max(0, Math.min(sr.width - 48, x));
      y = Math.max(0, Math.min(sr.height - 48, y));
      dragging.style.left = x + 'px';
      dragging.style.top = y + 'px';
      updateLines();
    }
    document.addEventListener('mousemove', function(e){ onMove(e.clientX, e.clientY); });
    document.addEventListener('touchmove', function(e){ if(dragging){ e.preventDefault(); onMove(e.touches[0].clientX, e.touches[0].clientY); }}, {passive:false});
    document.addEventListener('mouseup', function(){ dragging = null; });
    document.addEventListener('touchend', function(){ dragging = null; });

    // Animate initial line draw
    setTimeout(updateLines, 100);
  })();
})();

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
.anchor-demo height 220px Container height. Set to 100% or 360px to fill a full card or hero section.
.node-point width / height 48px Node diameter. 64 px for longer labels, 32 px for a dense graph with many nodes.
stroke (svg line) rgb(129,140,248) SVG line color. Replace with your accent color or vary per connection type (required vs optional).
stroke-width (svg line) 2 Line thickness. 1 = subtle, 3–4 = strong connections. Can encode edge weight in a weighted graph.
stroke-dasharray (svg line) 6 4 Dash pattern. '0' = solid line (direct link), '2 6' = fine dotted (weak link), '8 3 2 3' = dash-dot.
opacity (svg line) 0.6 Line opacity. 1 = full visibility, 0.3 = receding connections that put nodes in the foreground.
Node backgrounds (.node-a through .node-d background) 4 gradients (indigo, pink, green, amber) Swap for your brand colors or encode node state: green = healthy, red = error, grey = offline.
drag bounds (JS, lines 59-60) 48 Node size in px, used to keep it fully inside the scene. If you change the node size, use the same value here.

FAQ

The lines stop following my nodes after the window is resized — why?

Lines are recomputed on load and on every drag, not on resize: if the scene changes size, percentage-positioned nodes move but the lines wait for the next drag. Add window.addEventListener('resize', updateLines); right after the updateLines(); call (line 34).

How do I add a fifth node and new connections?

Add a <div class="node-point"> with data-node="e" and its CSS coordinates, then a <line id="line-ae"> in the SVG. Fetch it next to the other lines (lines 9-12) and add its endpoints in updateLines() (lines 23-33) — dragging works for the new node automatically.

Can I add directional arrows to connections?

Yes: define a <marker> inside an SVG <defs> block, then apply marker-end="url(#arrow)" to the target <line> elements. Existing CSS styles (color, width, dashes) stay untouched — only the SVG markup changes.