SVG stroke-dasharray animation: how to create comet/droplet head + trailing fading tail on rounded-rect path?

2 weeks ago 18
ARTICLE AD BOX

Minimal env:
React/Next.js + Tailwind, but issue is pure SVG/CSS
Shape is a rounded rectangle (rect with rx)
I animate stroke-dashoffset using a computed --trace-len (see below) so the loop is seam-free.

Goal:
Essentially I am struggling to design a trace of my sidebar element on my website. I have managed to get the trace to follow the sidebar as desired, but I am struggling in implementing my desired design.
I want the trace to look like a water droplet with a glowing thick head and a trickling tail behind it.
Droplet head - circular bead-like (bright core + soft glow gradually dissipating)
Tail - trickles behind the head (thinner and dimmer and dwindles off) (tail must dwindle off)
Head and tail must stay phase-aligned and not produce "reset/glitch"
Must keep it CSS-driven (no SMIL animateMotion, no JS per-frame) (unless necessary)

What I have already tried:

Tried animation-delay or --trace-start phase shifts, caused artifacts (tail sometimes appears ahead/behind due to two-dash symmetry)

Tried a moving <circle> with SMIL animateMotion, but it was unreliable and resulted in stationary circle

Current implementation uses extremely short dash for head, longer dash for tail; looks better but not “water droplet trickle”

CODE:
Current svg implemenation in page.tsx:

<svg className="zibio-filter-trace-svg" width={traceBox.w} height={traceBox.h} viewBox={`0 0 ${traceBox.w} ${traceBox.h}`} preserveAspectRatio="none" > <defs> <filter id="zibioTraceGlow" x="-40%" y="-40%" width="180%" height="180%"> <feGaussianBlur stdDeviation="1.6" result="blur" /> <feMerge> <feMergeNode in="blur" /> <feMergeNode in="SourceGraphic" /> </feMerge> </filter> </defs> {/* glow under everything */} <rect className="zibio-filter-trace-rect zibio-filter-trace-glow" filter="url(#zibioTraceGlow)" x={0.5} y={0.5} width={traceBox.w - 1} height={traceBox.h - 1} rx={Math.max(0, traceBox.r - 0.5)} ry={Math.max(0, traceBox.r - 0.5)} /> {/* tail echo behind core (phase-lagged via CSS delay) */} <rect className="zibio-filter-trace-rect zibio-filter-trace-tail" x={0.5} y={0.5} width={traceBox.w - 1} height={traceBox.h - 1} rx={Math.max(0, traceBox.r - 0.5)} ry={Math.max(0, traceBox.r - 0.5)} /> {/* crisp core on top */} <rect className="zibio-filter-trace-rect zibio-filter-trace-core" x={0.5} y={0.5} width={traceBox.w - 1} height={traceBox.h - 1} rx={Math.max(0, traceBox.r - 0.5)} ry={Math.max(0, traceBox.r - 0.5)} /> </svg>

Current globals.css (where the animation actually occurs)

/* PROOF: red outline, stroke-width 3, rounded linecap */ .zibio-filter-trace-rect { filter: drop-shadow(0 0 6px color-mix(in oklab, var(--trace-glow) 28%, transparent)) drop-shadow(0 0 16px color-mix(in oklab, var(--trace-glow) 14%, transparent)); stroke-width: 4 ; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; shape-rendering: geometricPrecision; stroke-dasharray: var(--trace-dash, calc(var(--trace-len) / 9)) var(--trace-gap, calc((var(--trace-len) - 2 * var(--trace-dash, calc(var(--trace-len) / 9))) / 2)) var(--trace-dash, calc(var(--trace-len) / 9)) var(--trace-gap, calc((var(--trace-len) - 2 * var(--trace-dash, calc(var(--trace-len) / 9))) / 2)); /* dash length, gap length */ opacity: 0.9 ; animation: zibio-trace-run 33s linear infinite ; animation-timing-function: linear; } @keyframes zibio-trace-run { from { stroke-dashoffset: var(--trace-start, 0px); } to { stroke-dashoffset: calc(-1 * (var(--trace-start, 0px) - var(--trace-len))); } } .zibio-trace-layer, .zibio-filter-trace-svg { overflow: visible; } .zibio-filter-sheet { --trace-stroke: var(--accent); --trace-glow: var(--glow-color, var(--accent)); } .zibio-filter-trace-rect { fill: none; stroke-linecap: round; stroke-linejoin: round; } /* Glow: thicker + blurred, driven by --trace-glow */ /* Glow follows the core segment, slightly longer and softer */ .zibio-filter-trace-glow { --trace-dash: calc(var(--trace-len) / 370); /* longer than core*/ stroke: var(--trace-glow); stroke-width: 6.0; opacity: 0.22; /*kept faint for embedded feel*/ stroke-linecap: round; } /* Core (the head) : slightly thicker + brighter on top */ .zibio-filter-trace-core { --trace-dash: calc(var(--trace-len) / 370); /* shorter than glow */ stroke: var(--trace-stroke); stroke-width: 3.2; opacity: 0.98; } /* Tail: thinner + dimmer, slightly phase-lagged (behind) */ .zibio-filter-trace-tail { --trace-dash: calc(var(--trace-len) / 28); /* shortest */ stroke: var(--trace-stroke); stroke-width: 1.05; opacity: 0.30; }

Current Result:
Please see the first attached image. You can see the small "head" I have which glows brighter (the trace-core in globals.css), there is a also a trace-glow but its hard to see, it covers the same area as the head, but this is NOT what it should be doing, (please see the goal above, it should essentially cover the head and tail and dwindle off, if the goal can be achieved without it thats fine as well) and then you can see the long tail, which has less opacity and is less bright but again it should not be like this it shouldn't be uniform.

Technique suggestion:
is there a way in pure SVG/CSS to make the tail fade along its length (gradient following dash) while animation stroke-dashoffset, without using motion paths?
How can I create a bead-like head + tapered tail where thickness decreased along the tail while both move and behave like one element

ULTIMATUM:
essentially please see goal, I need an element that appears like a water droplet, I have trace logic all done. But I am struggling in animating the element, it should have a head that is thick and glows and a tail that follows trickling off after it with glow dwindling.

Anyway I can do that, if anyone knows please let me know, even if it does not necessarily confine to my above rules, if you can tell me how to essentially generate that element, I can try to find a way to implement it into my code.

Read Entire Article