SVG + CSS recipe

Marching Arrows

The whole effect is four small tricks stacked together: a dashed stroke whose offset never stops sliding, a dot riding the same path, curves fanning out of one shared origin, and a node that breathes. No library, no build step — just SVG and CSS.

1 · Contract Created

CPQ generates Contract + Enterprise Flex Sub

2 · Drawdown Pool

Order Operations sets up the committed amount

Rate cards — one per product
Manage
Lead
Ready

↓ this request draws against Lead

3 · Drawdown Request

Submitted via Account Hub on the partner's behalf

4 · Notice Created

Auto-approval conditions are evaluated

Conditions met?
NO NEEDS REVIEW YES FAST PATH

Manual Review

Approver queue, SLA clock starts

Auto-Approved

Notice clears, drawdown posts

How it's built

1 · The marching dash

This is the whole trick. Give a path a dashed stroke, then animate stroke-dashoffset forever. Nothing moves — the gaps do. The one rule that matters: the offset you animate to must be an exact multiple of the dash period (dash + gap), or the loop visibly stutters on repeat.

.track {
  fill: none;
  stroke: #d8dde5;
  stroke-width: 2;
  stroke-dasharray: 7 7;        /* period = 14 */
  stroke-linecap: round;
  animation: march 900ms linear infinite;
}

@keyframes march {
  to { stroke-dashoffset: -28; }  /* 2 × 14 — seamless */
}
Negative offset marches forward along the path direction; positive marches backward. Swap the sign to reverse a flow without touching the geometry.

2 · The dot that rides the path

Those coloured pips travelling down the spine are a plain <circle> with an <animateMotion> child pointed at the same path via <mpath>. Reusing the path by id means the dot can never drift out of sync with the line it's riding — change the curve and the dot follows.

<path id="p1" class="track" d="M20,2 L20,50"/>

<circle r="3.6" fill="#c4174a">
  <animateMotion dur="2.2s" repeatCount="indefinite">
    <mpath href="#p1" xlink:href="#p1"/>
  </animateMotion>
</circle>
Ship both href and xlink:href, and declare xmlns:xlink on the <svg>. Older WebKit only honours the xlink form, and the failure is silent — the dot just parks at the origin.

3 · Fanning out of one origin

Every branch starts at the same point and ends above its target card. Give each a cubic bezier whose first control point sits straight below the origin — that's what makes all three leave vertically before peeling apart, instead of splaying out like a starburst. Stagger the dots with begin so they read as three streams, not one pulse.

<!-- origin (280,6) → targets at x = 122 / 280 / 438 -->
<path id="bL" d="M280,6 C280,46 122,42 122,84" marker-end="url(#mk-blue)"/>
<path id="bM" d="M280,6 L280,84"               marker-end="url(#mk-purple)"/>
<path id="bR" d="M280,6 C280,46 438,42 438,84" marker-end="url(#mk-green)"/>

<circle r="3.4" fill="#a78bfa">
  <animateMotion dur="2.6s" begin="0.3s" repeatCount="indefinite">
    <mpath href="#bM" xlink:href="#bM"/>
  </animateMotion>
</circle>

Arrowheads come from one <marker> per colour. fill="context-stroke" would collapse those into a single marker, but support is patchy enough that a handful of explicit markers is the safer trade.

4 · Layout: HTML cards, SVG only for the wire

The cards are ordinary flex-column HTML — that's why the text wraps and reflows properly. Only the connectors are SVG. Straight links are a narrow fixed-size <svg> centred between two cards; branches are a full-width one scaled uniformly, so stroke weights stay proportional. To let arrowheads land inside the group panel, pull the panel up and put the SVG above it in the stack.

.branch-svg { position: relative; z-index: 2; }

.group {
  position: relative;
  z-index: 1;
  margin-top: -26px;   /* slide up under the arrow tips */
  padding-top: 34px;   /* ...then push content back down */
}

5 · Turning it off

CSS can stop CSS, but it can't stop SMIL — a prefers-reduced-motion block will freeze the dashes and leave the dots flying. Kill those through the SVG DOM instead, with pauseAnimations() on each root.

@media (prefers-reduced-motion: reduce) {
  .track, .decision { animation: none !important; }
}
if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
  document.querySelectorAll('svg').forEach(s => s.pauseAnimations());
}