06Multi-Series Loading Whose Each Line Settles With a Damped Cascade
Several series skeletons appear together, then settle into their real values one by one on a delay computed by multiplying the series index by a damping factor, so nothing snaps into place all at once.
Best forA place where a comparison chart overlaying several metrics receives its values
--lengetTotalLengthanimationDelay

<main class="stage">
<div class="cm is-demo" id="cm">
<svg class="cm__svg" id="cmSvg" viewBox="0 0 200 100" role="img" aria-label="Three-series trend chart" aria-busy="true">
<line class="cm__axis" x1="4" y1="92" x2="196" y2="92" />
<path class="cm__flat" d="M4,60 L196,60" />
<path class="cm__series" id="cmSeries0" data-i="0" d="M4,70 L44,52 L84,64 L124,34 L164,46 L196,26" />
<path class="cm__series" id="cmSeries1" data-i="1" d="M4,50 L44,66 L84,40 L124,58 L164,30 L196,44" />
<path class="cm__series" id="cmSeries2" data-i="2" d="M4,30 L44,38 L84,20 L124,42 L164,18 L196,32" />
</svg>
<button type="button" class="cm__reload" id="cmReload">Reload chart</button>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 변수 3개
$duration: $dur-loop;
$easing: $ease-out;
$color: $chart-1;
@include stage($stage-ink);
.cm { width: min(300px, 88vw); }
.cm__svg { width: 100%; height: auto; overflow: visible; }
.cm__axis { stroke: rgba($subject-cream, .22); stroke-width: 1; }
.cm__flat {
fill: none; stroke: rgba($subject-cream, .3); stroke-width: 2; stroke-dasharray: 5 5;
stroke-linecap: round; animation: cmFlatFade $duration $easing infinite;
}
.cm__series { fill: none; stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; animation: cmDraw $duration $easing infinite; }
#cmSeries0 { stroke: $chart-1; }
#cmSeries1 { stroke: $chart-2; }
#cmSeries2 { stroke: $chart-3; }
.cm__reload {
display: block; margin: $sp-4 auto 0; appearance: none; border: none; cursor: pointer;
background: rgba($subject-cream, .12); color: $subject-cream; font-weight: 700; font-size: 13px;
padding: $sp-2 $sp-4; border-radius: $r-pill;
}
.cm__reload:focus-visible { outline: 2px solid $subject-cream; outline-offset: 2px; }
@keyframes cmFlatFade {
0% { opacity: 1; }
8% { opacity: 1; }
16% { opacity: 0; }
84% { opacity: 0; }
94% { opacity: 1; }
100% { opacity: 1; }
}
@keyframes cmDraw {
0% { stroke-dashoffset: var(--len); }
10% { stroke-dashoffset: var(--len); }
40% { stroke-dashoffset: 0; }
84% { stroke-dashoffset: 0; }
100% { stroke-dashoffset: var(--len); }
}
@media (max-width: 340px) {
.cm { width: min(190px, 80vw); }
.cm__reload { margin-top: $sp-2; font-size: 11px; padding: $sp-1 $sp-3; }
}
@media (prefers-reduced-motion: reduce) {
// 실사용 경로 적용: 자동 루프를 멈추고 완성된 계열만 보인다
.cm__flat { animation: none; opacity: 0; }
.cm__series { animation: none; stroke-dashoffset: 0; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.cm {
width: min(300px, 88vw);
}
.cm__svg {
width: 100%;
height: auto;
overflow: visible;
}
.cm__axis {
stroke: rgba(255, 247, 230, 0.22);
stroke-width: 1;
}
.cm__flat {
fill: none;
stroke: rgba(255, 247, 230, 0.3);
stroke-width: 2;
stroke-dasharray: 5 5;
stroke-linecap: round;
animation: cmFlatFade 2s ease-out infinite;
}
.cm__series {
fill: none;
stroke-width: 2.5;
stroke-linecap: round;
stroke-linejoin: round;
animation: cmDraw 2s ease-out infinite;
}
#cmSeries0 {
stroke: #2f6df6;
}
#cmSeries1 {
stroke: #c2361a;
}
#cmSeries2 {
stroke: #2b9d74;
}
.cm__reload {
display: block;
margin: 16px auto 0;
appearance: none;
border: none;
cursor: pointer;
background: rgba(255, 247, 230, 0.12);
color: #fff7e6;
font-weight: 700;
font-size: 13px;
padding: 8px 16px;
border-radius: 999px;
}
.cm__reload:focus-visible {
outline: 2px solid #fff7e6;
outline-offset: 2px;
}
@keyframes cmFlatFade {
0% {
opacity: 1;
}
8% {
opacity: 1;
}
16% {
opacity: 0;
}
84% {
opacity: 0;
}
94% {
opacity: 1;
}
100% {
opacity: 1;
}
}
@keyframes cmDraw {
0% {
stroke-dashoffset: var(--len);
}
10% {
stroke-dashoffset: var(--len);
}
40% {
stroke-dashoffset: 0;
}
84% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: var(--len);
}
}
@media (max-width: 340px) {
.cm {
width: min(190px, 80vw);
}
.cm__reload {
margin-top: 8px;
font-size: 11px;
padding: 4px 12px;
}
}
@media (prefers-reduced-motion: reduce) {
.cm__flat {
animation: none;
opacity: 0;
}
.cm__series {
animation: none;
stroke-dashoffset: 0;
}
}(function () {
var STEP_MS = 220;
var DAMPING = 0.55;
var root = document.getElementById('cm');
var reload = document.getElementById('cmReload');
var series = [0, 1, 2].map(function (i) { return document.getElementById('cmSeries' + i); });
// 계산 겹: 계열 지연을 감쇠(등비) 누적으로 계산한다 — 뒤 계열일수록 늘어나는 간격이 줄어들며 안정된다
function seriesDelay(i) {
var total = 0;
for (var k = 1; k <= i; k += 1) { total += STEP_MS * Math.pow(DAMPING, k - 1); }
return Math.round(total);
}
series.forEach(function (path, i) {
var len = Math.round(path.getTotalLength());
path.style.setProperty('--len', String(len));
path.style.strokeDasharray = String(len);
path.style.strokeDashoffset = String(len);
path.style.animationDelay = seriesDelay(i) + 'ms';
});
function human() {
root.classList.remove('is-demo');
root.querySelector('#cmSvg').setAttribute('aria-busy', 'true');
var lastDelay = 0;
series.forEach(function (path, i) {
var len = parseInt(path.style.getPropertyValue('--len'), 10);
path.getAnimations().forEach(function (a) { a.cancel(); });
path.style.animation = 'none';
path.style.strokeDashoffset = String(len);
var delayMs = seriesDelay(i);
lastDelay = delayMs;
window.setTimeout(function () {
path.animate(
[{ strokeDashoffset: len }, { strokeDashoffset: 0 }],
{ duration: 420, easing: 'ease-out', fill: 'forwards' }
);
}, 200 + delayMs);
});
window.setTimeout(function () { root.querySelector('#cmSvg').setAttribute('aria-busy', 'false'); }, 200 + lastDelay + 500);
}
reload.addEventListener('click', human);
reload.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); human(); }
});
}());import { useEffect, useRef } from 'react';
import styles from './ChartLoadingMultiSeriesCascade.module.scss';
const STEP_MS = 220;
const DAMPING = 0.55;
const PATHS = [
'M4,70 L44,52 L84,64 L124,34 L164,46 L196,26',
'M4,50 L44,66 L84,40 L124,58 L164,30 L196,44',
'M4,30 L44,38 L84,20 L124,42 L164,18 L196,32'
];
export default function ChartLoadingMultiSeriesCascade() {
const rootRef = useRef<HTMLDivElement>(null);
const svgRef = useRef<SVGSVGElement>(null);
const reloadRef = useRef<HTMLButtonElement>(null);
const seriesRefs = useRef<SVGPathElement[]>([]);
useEffect(() => {
const root = rootRef.current;
const svg = svgRef.current;
const reload = reloadRef.current;
if (!root || !svg || !reload) return undefined;
const series = seriesRefs.current!;
if (!series || series.length !== PATHS.length) return undefined;
// 계산 겹: 계열 지연을 감쇠(등비) 누적으로 계산한다 — 뒤 계열일수록 늘어나는 간격이 줄어들며 안정된다
function seriesDelay(i: number) {
let total = 0;
for (let k = 1; k <= i; k += 1) { total += STEP_MS * Math.pow(DAMPING, k - 1); }
return Math.round(total);
}
series.forEach((path, i) => {
const len = Math.round(path.getTotalLength());
path.style.setProperty('--len', String(len));
path.style.strokeDasharray = String(len);
path.style.strokeDashoffset = String(len);
path.style.animationDelay = seriesDelay(i) + 'ms';
});
const timers: number[] = [];
function human() {
root!.classList.remove(styles['is-demo']);
svg!.setAttribute('aria-busy', 'true');
let lastDelay = 0;
series.forEach((path, i) => {
const len = parseInt(path.style.getPropertyValue('--len'), 10);
path.getAnimations().forEach((a) => a.cancel());
path.style.animation = 'none';
path.style.strokeDashoffset = String(len);
const delayMs = seriesDelay(i);
lastDelay = delayMs;
timers.push(window.setTimeout(() => {
path.animate(
[{ strokeDashoffset: len }, { strokeDashoffset: 0 }],
{ duration: 420, easing: 'ease-out', fill: 'forwards' }
);
}, 200 + delayMs));
});
timers.push(window.setTimeout(() => { svg!.setAttribute('aria-busy', 'false'); }, 200 + lastDelay + 500));
}
function onClick() { human(); }
function onKey(e: KeyboardEvent) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); human(); } }
reload.addEventListener('click', onClick);
reload.addEventListener('keydown', onKey);
return () => {
reload.removeEventListener('click', onClick);
reload.removeEventListener('keydown', onKey);
timers.forEach((id) => window.clearTimeout(id));
};
}, []);
return (
<main className={styles.stage}>
<div className={styles.cm + ' ' + styles['is-demo']} ref={rootRef}>
<svg className={styles.cmSvg} ref={svgRef} viewBox="0 0 200 100" role="img" aria-label="계열 3종 추이 그래프" aria-busy="true">
<line className={styles.cmAxis} x1={4} y1={92} x2={196} y2={92} />
<path className={styles.cmFlat} d="M4,60 L196,60" />
{PATHS.map((d, i) => (
<path
key={i}
id={'cmSeries' + i}
className={styles.cmSeries}
data-i={i}
d={d}
ref={(el: SVGPathElement | null) => { if (el) seriesRefs.current![i] = el; }}
/>
))}
</svg>
<button type="button" className={styles.cmReload} ref={reloadRef}>그래프 다시 불러오기</button>
</div>
</main>
);
}06 Multi-Series Loading Whose Each Line Settles With a Damped Cascade — Measured area 3.492% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · Chart Loading Animation — 9 Computed Patterns
The post this demo lives in
Chart Loading Animation — 9 Computed PatternsAmong nine chart loading animation patterns, a bar's delay comes from its column, a heatmap cell's from diagonal distanc See all demos in this post
If you like this effect - similar demos
Loaders03 Button Whose Spinner Morphs Into a Success CheckmarkLoading Button UI — 9 States, Spinner to Success
Loaders03 Loader Whose Back Card Recomputes Depth Order to Move to the Front With Each StepCSS 3D Cube Animation Loaders, 9 Patterns
Loaders02 Orb Pulse Whose Breathing Widens With a Growing Wait-time ValueAI Loading Animation: 9 Patterns by One Value