Konva Animated Progress Ring — Canvas Arc Loader Snippet

Konva Animated Progress Ring · Loaders · Plain HTML, CSS & JS · Live preview

CategoryLoaders

What's included

Features

Custom sceneFunc arc
A Konva.Shape draws the arc directly with canvas ctx.arc, not a prebuilt Arc node.
Real Konva.Animation loop
A requestAnimationFrame-backed callback drives the arc every frame.
Frame-rate-independent easing
Step size scales by frame.timeDiff so speed is consistent across refresh rates.
Ease-out settle
The step shrinks as the value approaches target, then snaps at a small threshold.
Live, redirectable target
Clicking a new target mid-animation eases from the current value, not zero.
Synced center label
The percentage text reads from the same variable driving the arc angle.
Custom shape attribute
progressAngle is stored on the shape itself and read back inside sceneFunc.
Clean track + arc layering
A static full-circle track sits beneath the animated progress arc.

About this UI Snippet

Konva Animated Progress Ring — A Real Render Loop, Not CSS

Screenshot of the Konva Animated Progress Ring snippet rendered live

Most progress rings on the web are CSS: a conic-gradient or a stroke-dashoffset trick on an SVG circle, eased with transition. Those work, but they hand control of the animation entirely to the browser's CSS engine — you can't easily read the current value mid-animation, ease it non-linearly based on live state, or drive it from anything other than a start/end pair. This snippet instead draws the ring with Konva on a <canvas> and drives it with an actual per-frame JavaScript loop, Konva.Animation.

Drawing an arc with a custom sceneFunc

Konva ships a Konva.Arc shape, but this snippet uses the more general Konva.Shape with a custom sceneFunc to make the underlying mechanism explicit:

ctx.arc(center.x, center.y, RADIUS, -Math.PI / 2, -Math.PI / 2 + angle, false); ctx.strokeShape(shape);

sceneFunc receives the raw 2D canvas context and the shape instance, and Konva calls it on every redraw. ctx.arc starts at -Math.PI / 2 (12 o'clock, since canvas angle 0 is 3 o'clock and increases clockwise) and sweeps to -Math.PI/2 + angle, where angle is stored as a custom attribute on the shape (shape.getAttr('progressAngle')) rather than hardcoded — the arc redraws itself correctly however that attribute changes. ctx.strokeShape(shape) hands the path off to Konva's own stroke/fill pipeline (respecting stroke, strokeWidth, lineCap set on the shape) instead of calling ctx.stroke() directly, so the shape still participates in Konva's caching and hit-detection systems.

The animation loop

Konva.Animation is Konva's wrapper around requestAnimationFrame: you give it a function and a layer, and it calls your function once per frame with a frame object carrying frame.time (elapsed ms since the animation started) and frame.timeDiff (ms since the previous frame), then automatically redraws the layer after your callback returns.

currentPercent += diff * Math.min(1, (frame.timeDiff / 1000) * 4);

Rather than linearly stepping toward the target by a fixed amount per frame (which would run at different speeds on different refresh rates), the step size is scaled by frame.timeDiff, making the animation's real-world duration consistent whether the browser is running at 60fps or 120fps. The diff * factor formula is an ease-out: the step size shrinks as currentPercent approaches targetPercent, so the ring decelerates into its final value rather than stopping abruptly — once the remaining difference drops under 0.15, the loop snaps to the exact target and calls anim.stop().

Restarting mid-animation

Each call to animateTo() stops any animation already in progress (if (anim) anim.stop()) before starting a new one from the *current* value of currentPercent — not from wherever the target buttons think the ring should be. That's why clicking a new percentage button while the ring is still moving smoothly redirects it rather than jumping or restarting from zero.

The label updates from the same loop

label.text(Math.round(currentPercent) + '%') is set inside the same animation callback as the arc angle, so the numeric label and the visual sweep are always perfectly in sync — there's a single source of truth (currentPercent) driving both.

Build with AI

Build, Understand, Optimize, and Extend It With AI

The core mechanism worth understanding here is the animation loop's easing, not the arc drawing. Paste the code into an AI assistant like Claude and ask it to explain exactly how frame.timeDiff makes the ring's animation speed independent of the browser's refresh rate, and why the step formula diff * Math.min(1, (frame.timeDiff/1000)*4) produces a decelerating ease rather than constant-speed motion. Then ask what would happen if targetPercent were changed mid-flight without stopping the previous Konva.Animation first \u2014 a good way to surface why anim.stop() before starting a new one matters. To extend it: interpolate the stroke color based on progress (e.g. amber below 50%, green above), add a pulsing glow effect when the ring hits 100%, support multiple independent rings on one page by removing the closure-based globals, or swap the linear ease-out for a proper cubic easing function.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build an animated circular progress ring using Konva.js (v9, from a CDN) in plain HTML, CSS, and JavaScript.

Requirements:
- A Konva.Stage and Layer sized to fit a ring with a static background track circle (a Konva.Circle with just a stroke, no fill) and a progress arc drawn as a Konva.Shape with a custom sceneFunc that calls ctx.arc(centerX, centerY, radius, -Math.PI/2, -Math.PI/2 + angle, false) followed by ctx.strokeShape(shape), where angle is read from a custom shape attribute (via getAttr/setAttr) rather than a hardcoded value.
- A centered Konva.Text label showing the current percentage, positioned in the middle of the ring.
- Animate the arc using a real Konva.Animation (NOT a CSS transition and not a plain setInterval) whose callback receives a frame object and uses frame.timeDiff to scale the per-frame step, so the animation speed is consistent across different display refresh rates.
- The animation should ease out: the step size should shrink as the current value approaches the target (e.g. step = (target - current) * some rate-limited factor), snapping to the exact target once the remaining difference is very small, then stopping the Konva.Animation.
- Keep the current percentage in a variable outside the animation closure so that starting a new animateTo(target) call while a previous one is still running continues smoothly from the current value instead of jumping.
- Provide 4 buttons for different target percentages (e.g. 35%, 68%, 92%, 100%) that each call animateTo with their value, always stopping any in-flight animation first.
- Update the center label text every frame from the same value driving the arc, so they never desync.
- Style it as a dark circular panel with a colored progress stroke and rounded line caps.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

Requires
<div class="kpr-stage">
  <div class="kpr-head">
    <span class="kpr-tag">Konva · Animation loop</span>
    <h2>Animated Progress Ring</h2>
    <p>A canvas arc animated frame-by-frame with Konva.Animation, not CSS.</p>
  </div>
  <div id="kprContainer" class="kpr-container"></div>
  <div class="kpr-controls">
    <button class="kpr-btn" data-target="35">35%</button>
    <button class="kpr-btn" data-target="68">68%</button>
    <button class="kpr-btn" data-target="92">92%</button>
    <button class="kpr-btn" data-target="100">100%</button>
  </div>
</div>

Step by step

How to Use

  1. 1
    Add the Konva CDNInclude konva.min.js from the CDN panel — it attaches a global Konva object.
  2. 2
    Paste HTML, CSS, and JSA ring animates in to 68% on load.
  3. 3
    Click a percentage buttonThe ring eases toward the new target from wherever it currently is.
  4. 4
    Read the live labelThe center percentage updates every frame from the same value driving the arc.
  5. 5
    Adjust the easing feelChange the 4 multiplier in the timeDiff calculation for a faster or slower settle.
  6. 6
    Reuse for other metricsCall animateTo(n) from anywhere — a fetch callback, a form handler, a timer.

Real-world uses

Common Use Cases

File upload / processing progress
A precise, JS-driven indicator for long-running operations.
Dashboard KPI rings
Goal-completion or utilization rings on a metrics dashboard.
Teaching canvas animation loops
A concrete reference for Konva.Animation vs. CSS transitions.
Onboarding step trackers
A ring showing how far through a multi-step flow a user is.
Gamified progress meters
XP or completion rings that visibly ease toward new values.

Got questions?

Frequently Asked Questions

Konva.Arc works too, but a custom sceneFunc makes the drawing mechanism explicit and gives full control over exactly how the angle attribute maps to the canvas arc call \u2014 useful when you want non-standard behavior later, like a gapped or dashed progress arc.

Konva.Animation calls its callback with a frame object every animation frame. frame.time is elapsed milliseconds since the animation started; frame.timeDiff is milliseconds since the previous frame. Scaling the per-frame step by timeDiff keeps the animation\u2019s real-world speed consistent regardless of the display\u2019s refresh rate.

The step formula is diff * factor, where diff is the remaining distance to the target. As the ring gets closer, diff shrinks, so the step shrinks too \u2014 a simple ease-out. A small threshold (0.15) then snaps the last fraction of a percent to the exact target and stops the loop.

animateTo() stops the existing Konva.Animation and starts a new one, but currentPercent is a variable outside the animation closure, so the new animation continues from wherever the ring currently is rather than resetting to zero or jumping.

setAttr/getAttr ties the value to the Konva node itself, which keeps sceneFunc self-contained \u2014 it reads everything it needs from the shape it\u2019s drawing, rather than depending on an external closure variable, which matters more once you have multiple progress rings on the same page.

Inside the animation callback, interpolate an RGB color based on currentPercent / 100 (e.g. lerp between two color arrays) and set progressArc.stroke(interpolatedColor) each frame alongside the angle update.