DEV Community

Cover image for How to Build Smooth Loading Animations Using Parametric Curves
Alan West
Alan West

Posted on

How to Build Smooth Loading Animations Using Parametric Curves

Every frontend developer has been there. You need a loading indicator, so you reach for the same tired spinner CSS you've copy-pasted since 2018. It rotates. It's boring. Your designer hates it. You hate it.

The real problem isn't laziness — it's that most loading animations are built with simple rotate transforms or hard-coded keyframe steps. They look mechanical because they are mechanical. What if you could generate fluid, organic-feeling motion paths using actual math?

That's where parametric curves come in. I've been experimenting with them for loading indicators recently, and the difference in visual quality is genuinely striking. Let me walk you through the approach.

What Are Parametric Curves (and Why Should You Care)?

A parametric curve defines x and y positions as separate functions of a single parameter — usually t (time). The classic example is a Lissajous curve:

x(t) = A * sin(a * t + δ)
y(t) = B * sin(b * t)
Enter fullscreen mode Exit fullscreen mode

By tweaking a, b, and the phase shift δ, you get an infinite variety of smooth, looping paths. Figure-eights, pretzel shapes, orbital patterns — all from two simple sine functions.

The key insight: when the ratio a/b is rational, the curve closes on itself. That's exactly what you want for a loading animation — a smooth, repeating loop that doesn't have a visible "reset" point.

The Problem With Standard Spinners

Here's the typical loading spinner you see everywhere:

/* The spinner everyone copies from Stack Overflow */
.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #3498db;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
Enter fullscreen mode Exit fullscreen mode

It works. But the motion is perfectly uniform, which reads as robotic to the human eye. Real-world motion has acceleration, deceleration, and path variation. That's what parametric curves give you for free.

Step 1: Generate the Path With JavaScript

Let's create a Lissajous-based loading indicator using SVG and a bit of JS to compute the path:

function generateLissajousPath(a, b, delta, steps = 200) {
  const points = [];
  const A = 40; // x amplitude
  const B = 40; // y amplitude

  for (let i = 0; i <= steps; i++) {
    const t = (i / steps) * Math.PI * 2;
    const x = A * Math.sin(a * t + delta) + 50; // offset to center in viewBox
    const y = B * Math.sin(b * t) + 50;
    points.push(`${x.toFixed(2)},${y.toFixed(2)}`);
  }

  // Close the path by returning to start
  return `M${points[0]} ` + points.slice(1).map(p => `L${p}`).join(' ') + ' Z';
}

// Classic figure-eight: a=2, b=1, delta=0
const path = generateLissajousPath(2, 1, 0);
Enter fullscreen mode Exit fullscreen mode

This gives you an SVG path string that traces the curve. The ratio a:b = 2:1 produces a clean figure-eight. Try 3:2 for something more complex, or 5:4 if you want something that feels almost chaotic but still loops perfectly.

Step 2: Animate a Dot Along the Path

Now use SVG's built-in animateMotion to move an element along that path:

<svg viewBox="0 0 100 100" width="120" height="120">
  <!-- The curve itself (optional, looks cool as a faint trail) -->
  <path
    id="curve"
    d="M50,50 ..." 
    fill="none"
    stroke="#e0e0e0"
    stroke-width="1"
    opacity="0.3"
  />

  <!-- The animated dot -->
  <circle r="5" fill="#6366f1">
    <animateMotion
      dur="2s"
      repeatCount="indefinite"
      path="M50,50 ..."  
      calcMode="spline"
      keySplines="0.4 0 0.2 1"
      keyTimes="0;1"
    />
  </circle>
</svg>
Enter fullscreen mode Exit fullscreen mode

The calcMode="spline" with a custom keySplines value adds easing to the motion, so the dot accelerates and decelerates naturally along the curve. This is where the magic happens — the combination of a non-trivial path and smooth easing creates motion that feels alive.

Step 3: Add Multiple Dots for Extra Effect

One dot is good. Three dots with staggered timing is chef's kiss:

function createLoader(container, { a, b, delta, dots = 3, duration = 2 }) {
  const path = generateLissajousPath(a, b, delta);
  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  svg.setAttribute('viewBox', '0 0 100 100');
  svg.setAttribute('width', '120');
  svg.setAttribute('height', '120');

  for (let i = 0; i < dots; i++) {
    const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
    circle.setAttribute('r', String(4 - i)); // each dot slightly smaller
    circle.setAttribute('fill', `hsl(240, 80%, ${55 + i * 15}%)`); // gradient of colors
    circle.setAttribute('opacity', String(1 - i * 0.25));

    const motion = document.createElementNS('http://www.w3.org/2000/svg', 'animateMotion');
    motion.setAttribute('dur', `${duration}s`);
    motion.setAttribute('repeatCount', 'indefinite');
    motion.setAttribute('path', path);
    // Stagger each dot by offsetting its start
    motion.setAttribute('begin', `${(i * duration) / dots}s`);

    circle.appendChild(motion);
    svg.appendChild(circle);
  }

  container.appendChild(svg);
}

// Usage
createLoader(document.getElementById('loader'), {
  a: 3,
  b: 2,
  delta: Math.PI / 4,
  dots: 3,
  duration: 3
});
Enter fullscreen mode Exit fullscreen mode

The stagger creates a follow-the-leader effect along the parametric path. Because the path itself is mathematically smooth and closed, the dots chase each other in a continuous loop with no visible seam.

Step 4: Build a Playground to Explore Parameters

Honestly, half the fun is tweaking the parameters and watching the shapes change. If you're building this into a design system or component library, adding a simple playground is worth the effort.

The key parameters to expose:

  • a and b — frequency ratio (integer values keep the curve closed)
  • delta (δ) — phase shift (controls the "rotation" of the shape)
  • duration — animation speed
  • dot count and size — visual density
  • trail opacity — whether to show the underlying curve path

A few range sliders wired up to re-render the SVG is all you need. Keep a and b as integer-only inputs (1–8 range works well) so users always get clean loops.

Beyond Lissajous: Other Parametric Options

Lissajous curves are just the starting point. Some other parametric equations that make great loaders:

  • Rose curves: r = cos(kθ) in polar form — produces petal-like patterns
  • Spirograph (hypotrochoid): more parameters, more complexity, gorgeous results
  • Epitrochoid: the pattern you get from the outside of a rolling circle

The same approach works for all of them — compute the path from the equation, render it as an SVG path, animate along it.

Performance Considerations

A few things I learned the hard way:

  • Pre-compute the path. Don't recalculate on every frame. Generate the SVG path string once and let the browser's native animateMotion handle the rest.
  • SVG animateMotion is GPU-friendly. It's composited efficiently in most browsers, similar to CSS transforms. Avoid JS-driven requestAnimationFrame loops for the actual motion if you can help it.
  • Keep the step count reasonable. 200 points is plenty for a smooth curve. Going to 1000 just bloats the DOM with no visible improvement.
  • Use will-change: transform on the container if you notice any jank, but test first — premature will-change can actually hurt performance by consuming extra memory.

When Not to Use This

Let's be real — if your loading state is 200ms, nobody's going to appreciate your beautiful parametric animation. These shine for:

  • Long-running operations (file uploads, AI inference, data processing)
  • Full-page loading screens
  • Skeleton states where you want visual interest
  • Apps where polish is part of the brand

For a quick inline spinner next to a button? The boring border-radius spinner is probably fine.

Wrapping Up

Parametric curves are one of those "why didn't I think of this sooner" approaches. The math is simple, the results look premium, and once you have the generator function, you can create hundreds of unique loading patterns just by changing a few numbers. The combination of mathematically perfect loops and CSS/SVG easing produces motion that feels genuinely organic.

Next time you're about to paste that same border-top spinner, maybe reach for a sine function instead.

Top comments (0)