SVG spinner: a loading animation in a few lines

How to make an SVG loading spinner: a ring that turns with CSS or with animateTransform, pulsing dots and a progress ring, with code to copy and adapt.

Published

A loading spinner is one circle with a dashed stroke, turned by an animation. In SVG that is one element and a few lines of CSS, sharp at any size. This is a complete spinner, and the file is 500 bytes:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="50" height="50">
  <style>
    .arc {
      transform-origin: 25px 25px;
      animation: spin 1s linear infinite;
    }
    @keyframes spin {
      to { transform: rotate(360deg); }
    }
  </style>
  <circle cx="25" cy="25" r="20" fill="none" stroke="silver" stroke-width="5"/>
  <circle class="arc" cx="25" cy="25" r="20" fill="none" stroke="teal" stroke-width="5"
    stroke-linecap="round" stroke-dasharray="31.4 94.2"/>
</svg>

A grey ring with a teal arc a quarter of the way round, turning once a second. Save it as spinner.svg and it works in an <img> tag, or paste it into the page.

How it works

  • Two circles. The first is the faint track. The second is the moving arc, drawn on top.
  • fill="none" leaves only the outline. stroke-width sets how thick the ring is. The radius is 20 and not 25 so that the stroke, which straddles the outline, stays inside the viewBox.
  • stroke-dasharray="31.4 94.2" turns the outline into one dash and one gap. The circle is 125.6 units round, which is 2 × π × 20. A dash of 31.4 is a quarter of that and the gap of 94.2 is the rest, so exactly one arc shows. SVG dashed lines explains dash patterns.
  • transform-origin: 25px 25px makes the arc turn about the centre of the drawing. Without it the arc swings round the top-left corner.
  • linear keeps the speed even. The default easing would make it surge and slow on every turn.

To change the look, change the numbers: a longer dash for a longer arc, 2s for a slower turn, stroke-linecap="butt" for square ends. Remove the first circle for an arc with no track.

The same spinner without CSS

SVG’s own animation element does the job with no stylesheet:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="50" height="50">
  <circle cx="25" cy="25" r="20" fill="none" stroke="teal" stroke-width="5"
    stroke-linecap="round" stroke-dasharray="31.4 94.2">
    <animateTransform attributeName="transform" type="rotate"
      from="0 25 25" to="360 25 25" dur="1s" repeatCount="indefinite"/>
  </circle>
</svg>

The centre of rotation is written into the values: 0 25 25 is an angle of 0 about the point 25,25. SVG animation covers <animateTransform>. Both versions play when the file is used as an image.

Pulsing dots

Three dots that fade one after another. Each dot runs the same animation, started a little later than the one before:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 20" width="60" height="20">
  <style>
    circle {
      fill: teal;
      animation: pulse 1s ease-in-out infinite;
    }
    @keyframes pulse {
      50% { opacity: 0.2; }
    }
  </style>
  <circle cx="10" cy="10" r="6"/>
  <circle cx="30" cy="10" r="6" style="animation-delay: 0.15s"/>
  <circle cx="50" cy="10" r="6" style="animation-delay: 0.3s"/>
</svg>

Putting it in a page

PlacementCodeNotes
Image<img src="spinner.svg" alt="Loading" width="24" height="24">Simplest. The colour is fixed in the file
CSS backgroundbackground: url(spinner.svg) center / 24px no-repeatFor a loading state added with a class
InlineThe <svg> pasted into the HTMLCan take the text colour and be changed by page CSS

For an inline spinner, replace the fixed colour with stroke="currentColor" and it takes the colour of the text around it, so one spinner suits a light button and a dark one:

<button type="button" disabled style="color: white; background: teal">
  <svg viewBox="0 0 50 50" width="16" height="16" aria-hidden="true">
    <circle cx="25" cy="25" r="20" fill="none" stroke="currentColor" stroke-width="5"
      stroke-linecap="round" stroke-dasharray="31.4 94.2">
      <animateTransform attributeName="transform" type="rotate"
        from="0 25 25" to="360 25 25" dur="1s" repeatCount="indefinite"/>
    </circle>
  </svg>
  Saving
</button>

When the keyframes live in the page’s stylesheet and not in the SVG, they only reach an inline SVG. SVG in HTML compares the placements.

Tell screen readers too

A spinner says “wait” to someone who can see it and nothing to someone who cannot. Give the state some text. The status role makes an element a polite live region, so a screen reader announces text when it changes inside it. MDN’s advice is to have the element in the page first, empty, and add the words when loading starts:

<div role="status">
  <svg viewBox="0 0 50 50" width="24" height="24" aria-hidden="true">
    <circle cx="25" cy="25" r="20" fill="none" stroke="currentColor" stroke-width="5"
      stroke-dasharray="31.4 94.2"/>
  </svg>
  <span class="visually-hidden">Loading results</span>
</div>
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

The SVG is hidden from assistive technology with aria-hidden="true" and the words carry the meaning. When the spinner is an <img>, its alt text does the same job.

A ring that shows progress

When the amount done is known, show it. The same ring becomes a progress indicator by making the dash as long as the progress:

<svg viewBox="0 0 50 50" width="80" height="80" role="progressbar"
  aria-label="Upload" aria-valuemin="0" aria-valuemax="100" aria-valuenow="65">
  <circle cx="25" cy="25" r="20" fill="none" stroke="silver" stroke-width="5"/>
  <circle id="bar" cx="25" cy="25" r="20" fill="none" stroke="teal" stroke-width="5"
    pathLength="100" stroke-dasharray="65 100" transform="rotate(-90 25 25)"/>
  <text x="25" y="29" text-anchor="middle" font-size="10">65%</text>
</svg>
  • pathLength="100" tells the browser to measure the circle as 100 units round, so the dash length is the percentage. No circumference to work out.
  • rotate(-90 25 25) moves the start of the stroke from the right-hand side, where a circle’s outline begins, to the top.
  • To update it from script, set three things: bar.setAttribute('stroke-dasharray', percent + ' 100'), the text, and aria-valuenow on the <svg>.

Reduced motion

Some people set their device to reduce motion. A small spinner is mild, but the setting is easy to respect. Inside the <style> of the first example, this slows the turn:

@media (prefers-reduced-motion: reduce) {
  .arc { animation-duration: 4s; }
}

The other course is to stop the animation with animation: none and rely on the word “Loading”.

Common problems

What you seeCauseFix
The arc swings round the corner of the imageNo centre given for the rotationtransform-origin: 25px 25px, or the centre in the from and to values
The ring is clipped at the edgesThe radius plus half the stroke width is more than half the viewBoxReduce the radius
Two arcs show, or the arc jumpsThe dash and gap do not add up to the circumferenceMake them total 2 × π × radius, or use pathLength
The speed surges on each turnThe default easinglinear
It does not move in an <img>The keyframes are in the page’s CSSPut the <style> inside the SVG file, or use <animateTransform>
The colour will not changeThe SVG is an <img> or a backgroundInline it and use currentColor
The progress ring starts at three o’clockThat is where a circle’s outline startstransform="rotate(-90 25 25)"

Questions

Why not an animated GIF? A GIF has a fixed size in pixels, and each of its pixels is either fully transparent or fully solid, so its edge looks rough on any background but the one it was made for. The SVG spinners on this page are 370 to 500 bytes and sharp at any size.

CSS or animateTransform? Either. CSS is more familiar and can honour reduced motion with a media query. <animateTransform> needs no <style> block. Both run inside an image.

Can it be done with HTML and CSS alone? Yes, with a bordered <div> and border-radius. SVG earns its place when the spinner needs a rounded end, a gradient, an exact arc length or a progress value.

How do I check a spinner file? Open it in the SVG viewer, which shows the file the way an <img> tag does. If it turns there, it will turn in a page.