How to make a circle arc with gradient + border-radius? [duplicate] - html

I want to create a ring-shaped process spinner with CSS3 or JavaScript, similar to the loading progress spinner in Android.
The spinner should rotate continuously and be filled with a solid colour that fades out along the rim (i.e. a conical gradient) as in this picture:
How can I achieve this?

This would be trivially easy if only CSS or SVG had conical gradients! Until the conic-gradient() notation matures and gains support, we can approximate the effect by slicing up the gradient and covering the seams somehow.
Below you will find two solutions. The first solution uses an embedded SVG image; the second uses multiple CSS gradients and pseudo-elements.
Both start with a single div with a keyframe animation applied to make it rotate:
HTML:
<div class="spinner"></div>
CSS:
#keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spinner {
animation: rotate 1s linear infinite;
height: 200px;
width: 200px;
}
You can use a progress element if you prefer, but you will find it a pain to style. Also note that unless you're using something like prefixfree.js, you'll need to add the vendor-prefixed versions of the #keyframes at-rule and the transform and animation properties.
The SVG solution
#keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spinner {
animation: rotate 1s linear infinite;
background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwLDAgMjAwLDIwMCI+PGRlZnM+PGNsaXBQYXRoIGlkPSJyaW5nIj48cGF0aCBkPSJNMjAwLDEwMEExMDAsMTAwLDAsMSwxLDE5Ny44MSw3OS4yMUwxODguMDMsODEuMjlBOTAsOTAsMCwxLDAsMTkwLDEwMHoiLz48L2NsaXBQYXRoPjxmaWx0ZXIgaWQ9ImJsdXIiIHg9IjAiIHk9IjAiPjxmZUdhdXNzaWFuQmx1ciBpbj0iU291cmNlR3JhcGhpYyIgc3RkRGV2aWF0aW9uPSIzIiAvPjwvZmlsdGVyPjxwYXRoIGlkPSJwIiBkPSJNMjUwLDEwMEExNTAsMTUwLDAsMCwxLDI0Ni43MiwxMzEuMTlMMTAwLDEwMEEwLDAsMCwwLDAsMTAwLDEwMHoiIGZpbGw9ImN5YW4iLz48L2RlZnM+PGcgY2xpcC1wYXRoPSJ1cmwoI3JpbmcpIj48ZyBmaWx0ZXI9InVybCgjYmx1cikiIHRyYW5zZm9ybT0icm90YXRlKC02IDEwMCAxMDApIj48dXNlIHhsaW5rOmhyZWY9IiNwIiBmaWxsLW9wYWNpdHk9IjAiIHRyYW5zZm9ybT0icm90YXRlKDAgMTAwIDEwMCkiLz48dXNlIHhsaW5rOmhyZWY9IiNwIiBmaWxsLW9wYWNpdHk9Ii4wMyIgdHJhbnNmb3JtPSJyb3RhdGUoMTIgMTAwIDEwMCkiLz48dXNlIHhsaW5rOmhyZWY9IiNwIiBmaWxsLW9wYWNpdHk9Ii4wNyIgdHJhbnNmb3JtPSJyb3RhdGUoMjQgMTAwIDEwMCkiLz48dXNlIHhsaW5rOmhyZWY9IiNwIiBmaWxsLW9wYWNpdHk9Ii4xIiB0cmFuc2Zvcm09InJvdGF0ZSgzNiAxMDAgMTAwKSIvPjx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGwtb3BhY2l0eT0iLjE0IiB0cmFuc2Zvcm09InJvdGF0ZSg0OCAxMDAgMTAwKSIvPjx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGwtb3BhY2l0eT0iLjE3IiB0cmFuc2Zvcm09InJvdGF0ZSg2MCAxMDAgMTAwKSIvPjx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGwtb3BhY2l0eT0iLjIiIHRyYW5zZm9ybT0icm90YXRlKDcyIDEwMCAxMDApIi8+PHVzZSB4bGluazpocmVmPSIjcCIgZmlsbC1vcGFjaXR5PSIuMjQiIHRyYW5zZm9ybT0icm90YXRlKDg0IDEwMCAxMDApIi8+PHVzZSB4bGluazpocmVmPSIjcCIgZmlsbC1vcGFjaXR5PSIuMjgiIHRyYW5zZm9ybT0icm90YXRlKDk2IDEwMCAxMDApIi8+PHVzZSB4bGluazpocmVmPSIjcCIgZmlsbC1vcGFjaXR5PSIuMzEiIHRyYW5zZm9ybT0icm90YXRlKDEwOCAxMDAgMTAwKSIvPjx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGwtb3BhY2l0eT0iLjM0IiB0cmFuc2Zvcm09InJvdGF0ZSgxMjAgMTAwIDEwMCkiLz48dXNlIHhsaW5rOmhyZWY9IiNwIiBmaWxsLW9wYWNpdHk9Ii4zOCIgdHJhbnNmb3JtPSJyb3RhdGUoMTMyIDEwMCAxMDApIi8+PHVzZSB4bGluazpocmVmPSIjcCIgZmlsbC1vcGFjaXR5PSIuNDEiIHRyYW5zZm9ybT0icm90YXRlKDE0NCAxMDAgMTAwKSIvPjx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGwtb3BhY2l0eT0iLjQ1IiB0cmFuc2Zvcm09InJvdGF0ZSgxNTYgMTAwIDEwMCkiLz48dXNlIHhsaW5rOmhyZWY9IiNwIiBmaWxsLW9wYWNpdHk9Ii40OCIgdHJhbnNmb3JtPSJyb3RhdGUoMTY4IDEwMCAxMDApIi8+PHVzZSB4bGluazpocmVmPSIjcCIgZmlsbC1vcGFjaXR5PSIuNTIiIHRyYW5zZm9ybT0icm90YXRlKDE4MCAxMDAgMTAwKSIvPjx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGwtb3BhY2l0eT0iLjU1IiB0cmFuc2Zvcm09InJvdGF0ZSgxOTIgMTAwIDEwMCkiLz48dXNlIHhsaW5rOmhyZWY9IiNwIiBmaWxsLW9wYWNpdHk9Ii41OSIgdHJhbnNmb3JtPSJyb3RhdGUoMjA0IDEwMCAxMDApIi8+PHVzZSB4bGluazpocmVmPSIjcCIgZmlsbC1vcGFjaXR5PSIuNjIiIHRyYW5zZm9ybT0icm90YXRlKDIxNiAxMDAgMTAwKSIvPjx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGwtb3BhY2l0eT0iLjY2IiB0cmFuc2Zvcm09InJvdGF0ZSgyMjggMTAwIDEwMCkiLz48dXNlIHhsaW5rOmhyZWY9IiNwIiBmaWxsLW9wYWNpdHk9Ii42OSIgdHJhbnNmb3JtPSJyb3RhdGUoMjQwIDEwMCAxMDApIi8+PHVzZSB4bGluazpocmVmPSIjcCIgZmlsbC1vcGFjaXR5PSIuNyIgdHJhbnNmb3JtPSJyb3RhdGUoMjUyIDEwMCAxMDApIi8+PHVzZSB4bGluazpocmVmPSIjcCIgZmlsbC1vcGFjaXR5PSIuNzIiIHRyYW5zZm9ybT0icm90YXRlKDI2NCAxMDAgMTAwKSIvPjx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGwtb3BhY2l0eT0iLjc2IiB0cmFuc2Zvcm09InJvdGF0ZSgyNzYgMTAwIDEwMCkiLz48dXNlIHhsaW5rOmhyZWY9IiNwIiBmaWxsLW9wYWNpdHk9Ii43OSIgdHJhbnNmb3JtPSJyb3RhdGUoMjg4IDEwMCAxMDApIi8+PHVzZSB4bGluazpocmVmPSIjcCIgZmlsbC1vcGFjaXR5PSIuODMiIHRyYW5zZm9ybT0icm90YXRlKDMwMCAxMDAgMTAwKSIvPjx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGwtb3BhY2l0eT0iLjg2IiB0cmFuc2Zvcm09InJvdGF0ZSgzMTIgMTAwIDEwMCkiLz48dXNlIHhsaW5rOmhyZWY9IiNwIiBmaWxsLW9wYWNpdHk9Ii45MyIgdHJhbnNmb3JtPSJyb3RhdGUoMzI0IDEwMCAxMDApIi8+PHVzZSB4bGluazpocmVmPSIjcCIgZmlsbC1vcGFjaXR5PSIuOTciIHRyYW5zZm9ybT0icm90YXRlKDMzNiAxMDAgMTAwKSIvPjx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGwtb3BhY2l0eT0iMSIgdHJhbnNmb3JtPSJyb3RhdGUoMzQ4IDEwMCAxMDApIi8+PC9nPjwvZz48L3N2Zz4=') no-repeat;
height: 200px;
width: 200px;
}
<div class="spinner"></div>
Tested and working in IE 10, Chrome and Firefox.
Caveats
Changing the inner or outer radius of the ring is more painful than you might imagine, as it would require editing the clip path values. It's outside the scope of this answer to explain how to calculate it, but suffice to say it took a bit of geometry. I'll try to put a generator on GitHub if I get time.
How the SVG version works
That big blob of gibberish is just a Base64 encoded SVG image. Run it through a Base64 decoder and you'll see the original SVG image.
Here's the full image nicely indented and commented so you can see exactly how it works:
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0,0 200,200">
<defs>
<!-- Ring shape centred on 100, 100 with inner radius 90px, outer
radius 100px and a 12 degree gap at 348. -->
<clipPath id="ring">
<path d="M 200, 100
A 100, 100, 0, 1, 1, 197.81, 79.21
L 188.03, 81.29
A 90, 90, 0, 1, 0, 190, 100 z"/>
</clipPath>
<!-- Very simple Gaussian blur, used to visually merge sectors. -->
<filter id="blur" x="0" y="0">
<feGaussianBlur in="SourceGraphic" stdDeviation="3" />
</filter>
<!-- A 12 degree sector extending to 150px. -->
<path id="p" d="M 250, 100
A 150, 150, 0, 0, 1, 246.72, 131.19
L 100, 100
A 0, 0, 0, 0, 0, 100, 100 z" fill="cyan"/>
</defs>
<!-- Clip the blurred sectors to the ring shape. -->
<g clip-path="url(#ring)">
<!-- Blur the sectors together to make a smooth shape and rotate
them anti-clockwise by 6 degrees to hide the seam where the
fully opaque sector blurs with the fully transparent one. -->
<g filter="url(#blur)" transform="rotate(-6 100 100)">
<!-- Each successive sector increases in opacity and is rotated
by a further 12 degrees. -->
<use xlink:href="#p" fill-opacity="0" transform="rotate( 0 100 100)"/>
<use xlink:href="#p" fill-opacity="0.03" transform="rotate( 12 100 100)"/>
<use xlink:href="#p" fill-opacity="0.07" transform="rotate( 24 100 100)"/>
<use xlink:href="#p" fill-opacity="0.1" transform="rotate( 36 100 100)"/>
<use xlink:href="#p" fill-opacity="0.14" transform="rotate( 48 100 100)"/>
<use xlink:href="#p" fill-opacity="0.17" transform="rotate( 60 100 100)"/>
<use xlink:href="#p" fill-opacity="0.2" transform="rotate( 72 100 100)"/>
<use xlink:href="#p" fill-opacity="0.24" transform="rotate( 84 100 100)"/>
<use xlink:href="#p" fill-opacity="0.28" transform="rotate( 96 100 100)"/>
<use xlink:href="#p" fill-opacity="0.31" transform="rotate(108 100 100)"/>
<use xlink:href="#p" fill-opacity="0.34" transform="rotate(120 100 100)"/>
<use xlink:href="#p" fill-opacity="0.38" transform="rotate(132 100 100)"/>
<use xlink:href="#p" fill-opacity="0.41" transform="rotate(144 100 100)"/>
<use xlink:href="#p" fill-opacity="0.45" transform="rotate(156 100 100)"/>
<use xlink:href="#p" fill-opacity="0.48" transform="rotate(168 100 100)"/>
<use xlink:href="#p" fill-opacity="0.52" transform="rotate(180 100 100)"/>
<use xlink:href="#p" fill-opacity="0.55" transform="rotate(192 100 100)"/>
<use xlink:href="#p" fill-opacity="0.59" transform="rotate(204 100 100)"/>
<use xlink:href="#p" fill-opacity="0.62" transform="rotate(216 100 100)"/>
<use xlink:href="#p" fill-opacity="0.66" transform="rotate(228 100 100)"/>
<use xlink:href="#p" fill-opacity="0.69" transform="rotate(240 100 100)"/>
<use xlink:href="#p" fill-opacity="0.7" transform="rotate(252 100 100)"/>
<use xlink:href="#p" fill-opacity="0.72" transform="rotate(264 100 100)"/>
<use xlink:href="#p" fill-opacity="0.76" transform="rotate(276 100 100)"/>
<use xlink:href="#p" fill-opacity="0.79" transform="rotate(288 100 100)"/>
<use xlink:href="#p" fill-opacity="0.83" transform="rotate(300 100 100)"/>
<use xlink:href="#p" fill-opacity="0.86" transform="rotate(312 100 100)"/>
<use xlink:href="#p" fill-opacity="0.93" transform="rotate(324 100 100)"/>
<use xlink:href="#p" fill-opacity="0.97" transform="rotate(336 100 100)"/>
<use xlink:href="#p" fill-opacity="1" transform="rotate(348 100 100)"/>
</g>
</g>
</svg>
This is minified, Base64 encoded and used as an inline CSS background image. You may also serve it as a separate file if you prefer. Technically, it should be possible to embed the image without the Base64 encoding, but right now that only works in Chrome.
The pure CSS solution
This solution uses separate linear gradients in each quadrant and relies on visual similarity to cover up the seams. The ring shape is formed using pseudo-elements.
#keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spinner {
animation: rotate 1s linear infinite;
background: cyan;
border-radius: 50%;
height: 200px;
width: 200px;
position: relative;
}
.spinner::before,
.spinner::after {
content: '';
position: absolute;
}
.spinner::before {
border-radius: 50%;
background:
linear-gradient(0deg, hsla(0, 0%, 100%, 1 ) 50%, hsla(0, 0%, 100%, 0.9) 100%) 0% 0%,
linear-gradient(90deg, hsla(0, 0%, 100%, 0.9) 0%, hsla(0, 0%, 100%, 0.6) 100%) 100% 0%,
linear-gradient(180deg, hsla(0, 0%, 100%, 0.6) 0%, hsla(0, 0%, 100%, 0.3) 100%) 100% 100%,
linear-gradient(360deg, hsla(0, 0%, 100%, 0.3) 0%, hsla(0, 0%, 100%, 0 ) 100%) 0% 100%
;
background-repeat: no-repeat;
background-size: 50% 50%;
top: -1px;
bottom: -1px;
left: -1px;
right: -1px;
}
.spinner::after {
background: white;
border-radius: 50%;
top: 3%;
bottom: 3%;
left: 3%;
right: 3%;
}
<div class="spinner"></div>
Tested and working in IE 10, Chrome and Firefox.
Caveats
Unlike the SVG solution, this only works against a solid background colour. It also requires modification in several places if you want to change that colour, which is a pain.
How the pure CSS version works
To start with, the spinner is styled as a circle with a uniform background colour. This will be the colour of the spinning gradient.
.spinner {
background: cyan;
border-radius: 50%;
/* ... */
}
Set things up so that we can overlay the pseudo-elements on top of the spinner:
.spinner {
/* ... */
position: relative;
}
.spinner::before,
.spinner::after {
content: '';
position: absolute;
}
This is the tricky bit. Each quadrant of the :before pseudo-element is set to a different linear gradient starting with opaque white and progressively becoming more and more transparent. Towards the centre it's easy to see where the gradients join up, but notice how around the outside the colours are close enough together that they appear to join up smoothly.
.spinner::before {
border-radius: 50%;
background:
linear-gradient(0deg, hsla(0, 0%, 100%, 1 ) 50%, hsla(0, 0%, 100%, 0.9) 100%) 0% 0%,
linear-gradient(90deg, hsla(0, 0%, 100%, 0.9) 0%, hsla(0, 0%, 100%, 0.6) 100%) 100% 0%,
linear-gradient(180deg, hsla(0, 0%, 100%, 0.6) 0%, hsla(0, 0%, 100%, 0.3) 100%) 100% 100%,
linear-gradient(360deg, hsla(0, 0%, 100%, 0.3) 0%, hsla(0, 0%, 100%, 0 ) 100%) 0% 100%
;
background-repeat: no-repeat;
background-size: 50% 50%;
top: -1px;
bottom: -1px;
left: -1px;
right: -1px;
}
This is positioned so that it goes slightly over the edge of the spinner because if we position it right to the edge a faint rim of the background colour is visible.
Finally, hide the middle bit using the ::after pseudo-element to make a ring shape:
.spinner::after {
background: white;
border-radius: 50%;
top: 3%;
bottom: 3%;
left: 3%;
right: 3%;
}
Et voilá!

We can easily create this with only single div.
.loader {
--border-width: 10px;
height: 200px;
width: 200px;
border-radius: 50%;
/* 0.5px's are needed to avoid hard-stopping */
--mask: radial-gradient(
farthest-side,
transparent calc(100% - var(--border-width) - 0.5px),
#000 calc(100% - var(--border-width) + 0.5px)
);
-webkit-mask: var(--mask);
mask: var(--mask);
/* we're using two half linear-gradient which is masked by the radial-gradient */
background: linear-gradient(to top, rgba(0,255,226, 1), rgba(0,255,226, 0.5)) 100% 0/50% 100% no-repeat,
linear-gradient(rgba(0,255,226, 0.5) 50%, transparent 95%) 0 0/50% 100% no-repeat;
animation: spin 1s linear infinite;
}
#keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
<div class="loader"></div>
And this is the comparison of my answer with #Jordan Gray's answer by setting background to body:
#Jordan Gray's answer:
body {
background: pink;
}
#keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spinner {
animation: rotate 1s linear infinite;
background: cyan;
border-radius: 50%;
height: 200px;
width: 200px;
position: relative;
}
.spinner::before,
.spinner::after {
content: '';
position: absolute;
}
.spinner::before {
border-radius: 50%;
background:
linear-gradient(0deg, hsla(0, 0%, 100%, 1 ) 50%, hsla(0, 0%, 100%, 0.9) 100%) 0% 0%,
linear-gradient(90deg, hsla(0, 0%, 100%, 0.9) 0%, hsla(0, 0%, 100%, 0.6) 100%) 100% 0%,
linear-gradient(180deg, hsla(0, 0%, 100%, 0.6) 0%, hsla(0, 0%, 100%, 0.3) 100%) 100% 100%,
linear-gradient(360deg, hsla(0, 0%, 100%, 0.3) 0%, hsla(0, 0%, 100%, 0 ) 100%) 0% 100%
;
background-repeat: no-repeat;
background-size: 50% 50%;
top: -1px;
bottom: -1px;
left: -1px;
right: -1px;
}
.spinner::after {
background: white;
border-radius: 50%;
top: 3%;
bottom: 3%;
left: 3%;
right: 3%;
}
<div class="spinner"></div>
My answer:
body {
background: pink;
}
.loader {
--border-width: 10px;
height: 200px;
width: 200px;
border-radius: 50%;
/* 0.5px's are needed to avoid hard-stopping */
--mask: radial-gradient(
farthest-side,
transparent calc(100% - var(--border-width) - 0.5px),
#000 calc(100% - var(--border-width) + 0.5px)
);
-webkit-mask: var(--mask);
mask: var(--mask);
/* we're using two half linear-gradient which is masked by the radial-gradient */
background: linear-gradient(to top, rgba(0,255,226, 1), rgba(0,255,226, 0.5)) 100% 0/50% 100% no-repeat,
linear-gradient(rgba(0,255,226, 0.5) 50%, transparent 95%) 0 0/50% 100% no-repeat;
animation: spin 1s linear infinite;
}
#keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
<div class="loader"></div>

Using background-clip on single div:
div {
margin: 0 auto;
margin-top: 3rem;
}
.spnr {
height: 100px;
width: 100px;
border-radius: 50%;
border: 5px solid transparent;
animation: spin 1s linear infinite;
background: linear-gradient(white, white), conic-gradient(from 0.15turn, white, #00EBD3);
background-origin: border-box;
background-clip: content-box, border-box;
}
#keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
<div class="spnr"></div>
More variations:
:root {
--bg: white;
--wbg: linear-gradient(var(--bg), var(--bg));
}
.dark>div {
--bg: black;
--wbg: linear-gradient(var(--bg), var(--bg));
}
.dark {
background: black;
}
div {
display: inline-block;
margin: 0 1rem;
margin-top: 0.5rem;
height: 200px;
}
.one {
background: var(--wbg), conic-gradient(from 0.15turn, transparent, #00EBD3);
}
.two {
background: var(--wbg), conic-gradient(from 0.15turn, transparent, transparent, #00EBD3);
}
.three {
background: var(--wbg), conic-gradient(from 0.15turn, transparent 0.0turn, transparent .04turn, pink 0.49turn, pink 0.5turn, transparent 0.50turn, transparent 0.55turn, pink 0.99999turn);
}
.four {
background: var(--wbg), conic-gradient(from 0.25turn, transparent 0.0turn, darkgreen, transparent, darkgreen, transparent, darkgreen, transparent, darkgreen, transparent);
}
.five {
background: var(--wbg), conic-gradient(from 0.25turn, transparent 0.0turn, red 0.125turn, transparent 0.125turn, red .25turn, transparent .25turn, red 0.375turn, transparent .375turn, red 0.5turn, transparent .5turn, red 0.625turn, transparent .625turn, red 0.75turn, transparent .75turn, red 0.875turn, transparent .875turn, red 1turn, transparent 1turn);
animation-duration: 2s;
}
.six {
border-width: 15px;
background: var(--wbg), conic-gradient(from 0.25turn, transparent 0.0turn, transparent .125turn, orange 0.125turn, orange .25turn, transparent .25turn, transparent .375turn, orange 0.375turn, orange 0.5turn, transparent .5turn, transparent.625turn, orange .625turn, orange 0.75turn, transparent .75turn, transparent 0.875turn, orange .875turn, orange 1turn, transparent 1turn);
opacity: 0.7;
}
.spnr {
height: 60px;
width: 60px;
border-radius: 50%;
border: 5px solid transparent;
animation: spin 1s linear infinite;
background-origin: border-box;
background-clip: content-box, border-box;
}
.six {
animation: size 2s linear infinite alternate;
}
.five {
animation-duration: 2s;
}
#keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#keyframes size {
0% {
transform: rotate(0deg) scale(0.2);
border-width: 5px;
}
100% {
border-width: 10px;
transform: rotate(840deg) scale(1);
}
}
<div>
<div class="spnr one"></div>
<div class="spnr two"></div>
<div class="spnr three"></div>
<div class="spnr four"></div>
<div class="spnr five"></div>
<div class="spnr six"></div>
</div>
<div class="dark">
<div class="spnr one"></div>
<div class="spnr two"></div>
<div class="spnr three"></div>
<div class="spnr four"></div>
<div class="spnr five"></div>
<div class="spnr six"></div>
</div>

conic-gradient with mask and no complex values:
.ring {
width: 150px; /* the size */
padding: 8px; /* the border */
background: #07e8d6; /* the color */
aspect-ratio: 1;
border-radius: 50%;
-webkit-mask:
conic-gradient(#0000,#000),
linear-gradient(#000 0 0) content-box;
-webkit-mask-composite: source-out;
mask-composite: subtract;
box-sizing: border-box;
animation:r 2s linear infinite;
}
#keyframes r {to{transform:rotate(1turn)}}
body {
background:linear-gradient(90deg,pink,#fff);
}
<div class="ring"></div>

I was able to create a real, full circle, gradient spinner, with opacity, by using a linear gradient on two half circles, and aligning them. No JavaScript or CSS necessary.
<svg
version="1.1"
width="24" height="24"
viewBox="-1 -1 25 25"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<linearGradient x1="0%" y1="0%" x2="100%" y2="0" id="gradient-1">
<stop stop-color="red" offset="0%" />
<stop stop-color="red" offset="63.1%" stop-opacity=".631" />
<stop stop-color="red" offset="100%" stop-opacity=".5" />
</linearGradient>
<linearGradient x1="0%" y1="0%" x2="100%" y2="0" id="gradient-2">
<stop stop-color="red" offset="0%" stop-opacity=".5" />
<stop stop-color="red" offset="63.1%" stop-opacity=".12" />
<stop stop-color="red" offset="100%" stop-opacity="0" />
</linearGradient>
</defs>
<g fill="none">
<g transform="translate(1 1)">
<path
d="M 10.5 10.5 m -10.5 0a 10.5 10.5 0 1 0 21 0a"
stroke="url(#gradient-1)"
stroke-width="3"
/>
<animateTransform
attributeName="transform"
type="rotate"
from="0 10.5 10.5"
to="360 10.5 10.5"
dur="1s"
repeatCount="indefinite"
/>
</g>
<g transform="translate(1 1)">
<path
d="M 10.5 10.5 m -10.5 0a 10.5 10.5 0 1 0 21 0a"
stroke="url(#gradient-2)"
stroke-width="3"
/>
<animateTransform
attributeName="transform"
type="rotate"
from="-180 10.5 10.5"
to="180 10.5 10.5"
dur="1s"
repeatCount="indefinite"
/>
</g>
</g>
</svg>

Related

Converting clip-path: polygon() to Edge-compatible SVG

I'm trying to create a CSS illustration that works across browsers, including Edge.
Using this answer, I tried to convert the following (working) CSS polygon clip-path to an Edge-compatible, SVG-driven approach. The 1-to-1 method is rendering something, but it's not the desired result.
What am I misunderstanding here?
Currently working CSS clip-path: polygon()
.you-headshot {
position: relative;
max-width: 500px;
max-height: 500px;
background-color: #a3a3a1;
width: 100%;
height: 100%;
border-radius: 400px;
overflow: hidden;
}
.you-headshot div {
position: absolute;
height: 100%;
width: 100%;
}
.you-head-outline {
background-color: #000;
clip-path: polygon(48% 6%, 43% 7%, 38% 9%, 34% 12%, 29% 16%, 24% 22%, 22% 30%, 22% 44%, 23% 50%, 23% 65%, 25% 72%, 28% 77%, 32% 82%, 35% 86%, 40% 90%, 43% 92%, 50% 93%, 55% 91%, 62% 87%, 70% 76%, 74% 69%, 75% 64%, 75% 54%, 74% 49%, 74% 40%, 74% 32%, 71% 23%, 66% 15%, 59% 9%, 53% 6%);
}
.you-neck-outline {
background-color: #000;
clip-path: polygon(29% 77%, 28% 88%, 23% 100%, 24% 100%, 76% 100%, 68% 90%, 67% 87%, 65% 71%);
}
<div class="you-headshot">
<div class="you-neck-outline">
</div>
<div class="you-head-outline">
</div>
</div>
Not working SVG conversion
.you-headshot {
position: relative;
max-width: 500px;
max-height: 500px;
background-color: #a3a3a1;
width: 100%;
height: 100%;
border-radius: 400px;
overflow: hidden;
}
.you-headshot div {
position: absolute;
height: 100%;
width: 100%;
}
.you-head-outline {
background-color: #000;
clip-path: url(#you-head-outline)
}
.you-neck-outline {
background-color: #000;
clip-path: url(#you-neck-outline)
}
<div class="you-headshot">
<div class="you-head-outline">
</div>
<div class="you-neck-outline">
</div>
<svg width="0" height="0">
<clipPath id="you-head-outline" clipPathUnits="objectBoundingBox">
<polygon points=".48, .6, .43, .7, .38, .9, .34, .12, .29, .16, .24, .22, .22, .30, .22, .44, .23, .50, .23, .65, .25, .72, .28, .77, .32, .82, .35, .86, .40, .90, .43, .92, .50, .93, .55, .91, .62, .87, .70, .76, .74, .69, .75, .64, .75, .54, .74, .49, .74, .40, .74, .32, .71, .23, .66, .15, .59, .9, .53, .6"/>
</clipPath>
<clipPath id="you-neck-outline" clipPathUnits="objectBoundingBox">
<polygon points=".29, .77, .28, .88, .23, .100, .24, .100, .76, .100, .68, .90, .67, .87, .65, .71"/>
</clipPath>
</svg>
</div>
You have a few errors:
The clipPath elements have the same id as the divs. I've changed this by adding cp- to the clipping paths id
When you transformed from % to units you have 6% = .6 instead of 6% = .06. Also in your code 100% = .1 instead of 100% = 1
body{height:500px;}
.you-headshot {
position: relative;
max-width: 500px;
max-height: 500px;
background-color: #a3a3a1;
width: 100%;
height: 100%;
border-radius: 400px;
overflow: hidden;
}
.you-headshot div {
position: absolute;
height: 100%;
width: 100%;
}
.you-head-outline {
background-color: #000;
clip-path: url(#cp-you-head-outline)
}
.you-neck-outline {
background-color: #000;
clip-path: url(#cp-you-neck-outline)
}
<svg width="0" height="0">
<clipPath id="cp-you-head-outline" clipPathUnits="objectBoundingBox">
<polygon points=".48, .06, .43, .07, .38, .09, .34, .12, .29, .16, .24, .22, .22, .30, .22, .44, .23, .50, .23, .65, .25, .72, .28, .77, .32, .82, .35, .86, .40, .90, .43, .92, .50, .93, .55, .91, .62, .87, .70, .76, .74, .69, .75, .64, .75, .54, .74, .49, .74, .40, .74, .32, .71, .23, .66, .15, .59, .09, .53, .06"/>
</clipPath>
<clipPath id="cp-you-neck-outline" clipPathUnits="objectBoundingBox">
<polygon points=".29, .77, .28, .88, .23, 1, .24, 1, .76, 1, .68, .90, .67, .87, .65, .71"/>
</clipPath>
<polygon points=".29, .77, .28, .88, .23, 1, .24, 1, .76, 1, .68, .90, .67, .87, .65, .71"/>
</svg>
<div class="you-headshot">
<div class="you-neck-outline">
</div>
<div class="you-head-outline">
</div>
</div>

CSS Backdrop-filter not functioning on child element in slideout menu

I am trying to apply a backdrop-filter effect to a slideout menu that consists of two nested divs. I've managed to navigate the issue of how that changes the positioning of the elements, but for some reason, so long as the bottom part of the menu is nested inside the top "tab", the filters on the lower portion don't function, as seen on the left. It does work if applied only to the inner div (as seen on the right) but I would like to have the effect on both. If they aren't nested, I don't know how to make the slideout transition on hover work.
The closest I have found to a discussion of this issue is here but I'm not really sure how it applies.
Here's the demo:
body {
background: repeating-linear-gradient(
45deg,
#606dbc,
#606dbc 10px,
#465298 10px,
#465298 20px);
height: 100%;
width: 100%;
font-family: 'Work Sans', sans-serif;
font-size: 1rem;
text-align: center;
color: #F0F8FF;
}
/* -------------------GLASS EFFECT------------------- */
.glass {
backdrop-filter: contrast(130%) brightness(120%) blur(2px);
background:
radial-gradient(
ellipse at 16.7% -10%,
hsla(0, 0%, 100%, 0.45) 24%,
hsla(0, 0%, 100%, 0.4) 25%,
hsla(0, 0%, 100%, 0.2) 45%,
hsla(0, 0%, 100%, 0.1)
);
background-size: 300% 100%;
border-radius: 10px;
box-shadow:
0 2px 1px hsla(0, 0%, 100%, 0.5) inset, /* Highlight upper edge */
0 -2px 1px hsla(250, 70%, 5%, 0.3) inset, /* Shade lower edge */
0 -2px 6px hsla(0, 0%, 100%, 0.25); /* Imply light cast around the edges */
}
.frame {
padding: 20px;
}
.centered {
position: absolute;
top: 30%;
left: 50%;
transform: translate(-50%, -50%);
}
/* -------------------Menu version------------------- */
.glassUpper {
position: fixed;
bottom: 0px;
width: 35px;
height: 30px;
background:
radial-gradient(
ellipse at 16.7% -10%,
hsla(0, 0%, 100%, 0.4) 24%,
hsla(0, 0%, 100%, 0.3) 25%,
hsla(0, 0%, 100%, 0.1) 45%,
hsla(0, 0%, 100%, 0.05)
);
background-size: 300% 100%;
padding: 5px 2px 0 2px;
text-align: center;
border-radius: 8px 8px 0 0;
box-shadow:
0 2px 1px hsla(0, 0%, 100%, 0.5) inset, /* Highlight upper edge */
0 -2px 1px hsla(250, 70%, 5%, 0.3) inset, /* Shade lower edge */
0 -2px 6px hsla(0, 0%, 100%, 0.25); /* Imply light cast around the edges */
transition: bottom 1s;
transition-duration: 400ms ease-in;
}
#menuUpperRight {
right: 40px;
}
#menuUpperLeft {
left: 40px;
backdrop-filter: contrast(130%) brightness(120%) blur(2px);
}
.glassLower {
position: fixed;
bottom: -120px;
right: 40px;
width: 200px;
height: 110px;
padding: 5px 0px;
background:
radial-gradient(
ellipse at 16.7% -10%,
hsla(0, 0%, 100%, 0.4) 24%,
hsla(0, 0%, 100%, 0.3) 25%,
hsla(0, 0%, 100%, 0.1) 45%,
hsla(0, 0%, 100%, 0.05)
);
background-size: 300% 100%;
text-align: center;
opacity: 0.75;
box-shadow:
0 2px 1px hsla(0, 0%, 100%, 0.5) inset, /* Highlight upper edge */
0 -2px 1px hsla(250, 70%, 5%, 0.3) inset, /* Shade lower edge */
0 -2px 6px hsla(0, 0%, 100%, 0.25); /* Imply light cast around the edges */
transition: bottom 1s;
transition-duration: 400ms ease-in;
}
#menuLowerRight {
right: 40px;
border-radius: 10px 0 0 0;
backdrop-filter: contrast(130%) brightness(120%) blur(2px);
}
#menuLowerLeft {
left: 0px;
border-radius: 0 10px 0 0;
transform: translate3d(0, 0, 0);
backdrop-filter: contrast(130%) brightness(120%) blur(2px);
}
.glassUpper:hover {
bottom: 120px;
}
#menuUpperRight:hover #menuLowerRight {
bottom: 0;
}
<!-- First -->
<div class="glass frame centered">
<section class="content">
<p>A light glass-effect frame.</p>
</section>
</div>
<!-- Menus! -->
<div class="glassUpper" id="menuUpperRight">
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24px" height="24px" viewBox="0 0 24 24" fill="none" stroke="rgba(240, 248, 255, 0.75)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="17.291" x2="12" y2="10.708"/>
<line x1="12" y1="6.708" x2="12.01" y2="6.708"/>
</svg>
<div class="glassLower" id="menuLowerRight">
<p>Working when applied to child dev only!</p>
<!-- custom play and pause buttons -->
<button class="radioButton" id="play">Play</button>
<button class="radioButton" id="pause">Pause</button>
<!-- Credits -->
<button class="bigButton" id="credits">Credits</button>
</div>
</div>
<div class="glassUpper" id="menuUpperLeft">
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24px" height="24px" viewBox="0 0 24 24" fill="none" stroke="rgba(240, 248, 255, 0.75)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="17.291" x2="12" y2="10.708"/>
<line x1="12" y1="6.708" x2="12.01" y2="6.708"/>
</svg>
<div class="glassLower" id="menuLowerLeft">
<p>Backdrop Filters broken on child div???</p>
<!-- custom play and pause buttons -->
<button class="radioButton" id="play">Play</button>
<button class="radioButton" id="pause">Pause</button>
<!-- Credits -->
<button class="bigButton" id="credits">Credits</button>
</div>
</div>
Using an extra wrapper can solve this issue, you can then have your elements upper/lower as separate inside:
body {
background: repeating-linear-gradient(
45deg,
#606dbc,
#606dbc 10px,
#465298 10px,
#465298 20px);
height: 100%;
width: 100%;
font-family: 'Work Sans', sans-serif;
font-size: 1rem;
text-align: center;
color: #F0F8FF;
}
/* -------------------GLASS EFFECT------------------- */
.glass {
backdrop-filter: contrast(130%) brightness(120%) blur(2px);
background:
radial-gradient(
ellipse at 16.7% -10%,
hsla(0, 0%, 100%, 0.45) 24%,
hsla(0, 0%, 100%, 0.4) 25%,
hsla(0, 0%, 100%, 0.2) 45%,
hsla(0, 0%, 100%, 0.1)
);
background-size: 300% 100%;
border-radius: 10px;
box-shadow:
0 2px 1px hsla(0, 0%, 100%, 0.5) inset, /* Highlight upper edge */
0 -2px 1px hsla(250, 70%, 5%, 0.3) inset, /* Shade lower edge */
0 -2px 6px hsla(0, 0%, 100%, 0.25); /* Imply light cast around the edges */
}
.frame {
padding: 20px;
}
.centered {
position: absolute;
top: 30%;
left: 50%;
transform: translate(-50%, -50%);
}
/* -------------------Menu version------------------- */
.glassUpper {
width: 35px;
height: 30px;
background:
radial-gradient(
ellipse at 16.7% -10%,
hsla(0, 0%, 100%, 0.4) 24%,
hsla(0, 0%, 100%, 0.3) 25%,
hsla(0, 0%, 100%, 0.1) 45%,
hsla(0, 0%, 100%, 0.05)
);
background-size: 300% 100%;
padding: 5px 2px 0 2px;
text-align: center;
border-radius: 8px 8px 0 0;
box-shadow:
0 2px 1px hsla(0, 0%, 100%, 0.5) inset, /* Highlight upper edge */
0 -2px 1px hsla(250, 70%, 5%, 0.3) inset, /* Shade lower edge */
0 -2px 6px hsla(0, 0%, 100%, 0.25); /* Imply light cast around the edges */
transition: bottom 1s;
transition-duration: 400ms ease-in;
}
#menuUpperLeft {
backdrop-filter: contrast(130%) brightness(120%) blur(2px);
}
.glassLower {
width: 200px;
height: 110px;
padding: 5px 0px;
background:
radial-gradient(
ellipse at 16.7% -10%,
hsla(0, 0%, 100%, 0.4) 24%,
hsla(0, 0%, 100%, 0.3) 25%,
hsla(0, 0%, 100%, 0.1) 45%,
hsla(0, 0%, 100%, 0.05)
);
background-size: 300% 100%;
text-align: center;
opacity: 0.75;
box-shadow:
0 2px 1px hsla(0, 0%, 100%, 0.5) inset, /* Highlight upper edge */
0 -2px 1px hsla(250, 70%, 5%, 0.3) inset, /* Shade lower edge */
0 -2px 6px hsla(0, 0%, 100%, 0.25); /* Imply light cast around the edges */
transition: bottom 1s;
transition-duration: 400ms ease-in;
}
#menuLowerLeft {
border-radius: 0 10px 0 0;
transform: translate3d(0, 0, 0);
backdrop-filter: contrast(130%) brightness(120%) blur(2px);
}
.extra{
position: fixed;
top: calc(100% - 35px);
left: 40px;
transition:0.5s;
pointer-events:none;
}
.extra * {
pointer-events:initial;
}
.extra:hover {
top: calc(100% - 155px);
}
<!-- First -->
<div class="glass frame centered">
<section class="content">
<p>A light glass-effect frame.</p>
</section>
</div>
<!-- Menus! -->
<div class="extra">
<div class="glassUpper" id="menuUpperLeft">
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24px" height="24px" viewBox="0 0 24 24" fill="none" stroke="rgba(240, 248, 255, 0.75)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="17.291" x2="12" y2="10.708"/>
<line x1="12" y1="6.708" x2="12.01" y2="6.708"/>
</svg>
</div>
<div class="glassLower" id="menuLowerLeft">
<p>Backdrop Filters broken on child div???</p>
<!-- custom play and pause buttons -->
<button class="radioButton" id="play">Play</button>
<button class="radioButton" id="pause">Pause</button>
<!-- Credits -->
<button class="bigButton" id="credits">Credits</button>
</div>
</div>

How to create a right triangle which has linear-gradient background?

I want to create a right triangle with a linear-gradient background color. Is it possible using CSS?
Below is my code for a right triangle with a single background color.
The same code is also available here https://codepen.io/anon/pen/BMqVbL?editors=1100
<style>
body {
position: relative;
height: 100vh;
width: 100vw;
background: lightgrey;
}
.wrapper {
width: 760px;
height: 35px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.triangle {
border-right-width: 760px;
border-bottom-width: 35px;
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 0;
border-bottom-style: solid;
border-right-style: solid;
border-bottom-color: red;
border-right-color: transparent;
}
</style>
<body>
<div class="wrapper">
<div class="triangle"><!-- ### --></div>
</div>
</body>
I need my triangle to have a linear-gradient background transforming orange into red from left to right. The surroundings of my triangle have to be transparent.
I'd suggest to use the clip-path property instead, so you can reduce and clean the markup and easily use a linear-gradient as the background
Codepen demo
.triangle {
display: block;
max-width: 760px;
height: 35px;
background: linear-gradient(to right, orange, red);
clip-path: polygon(0 0, 100% 100%, 0 100%)
}
<span class="triangle"></span>
As a side note, I've used max-width instead of width, just to show you how you could make it responsive.
I think you are looking for border-image property:
border-image: linear-gradient(to top right, orange, red 50%, transparent 51%, transparent); should work
Demo solution:
.triangle {
border-right-width: 760px;
border-bottom-width: 35px;
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 0;
border-bottom-style: solid;
border-right-style: solid;
border-bottom-color: red;
border-right-color: transparent;
border-image: linear-gradient(to right top, orange, red 50%, transparent 51%, transparent);
}
<div class="wrapper">
<div class="triangle"><!-- ### --></div>
</div>
You can also consider multiple background to create the triangle but without transparency. The trick is to have a triangle on the top of the gradient having the same color as the main background:
.triangle {
max-width: 300px;
height: 50px;
background:
linear-gradient(to top right,transparent 49%,#fff 50%),
linear-gradient(to right, blue, red);
}
<div class="triangle"></div>
Another idea with skew transformation and overflow where you will have transparency:
.triangle {
max-width: 300px;
height: 50px;
overflow:hidden;
}
.triangle:before {
content:"";
display:block;
height:100%;
width:100%;
background: linear-gradient(to right, blue, red);
transform-origin:left;
transform:skewY(10deg);
}
<div class="triangle"></div>
You have also the SVG solution:
svg {
width:300px;
}
<svg viewBox="0 0 300 100">
<defs>
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="blue" />
<stop offset="100%" stop-color="red" />
</linearGradient>
</defs>
<polygon points='0,0 300,100 0,100' fill="url(#grad)" />
</svg>
https://codepen.io/vaneetthakur/pen/jdepLx
I have created the right triangle gradient background-color.
Please check below Code -
<div class="gradient-block"></div>
.gradient-block{
width:200px;
height:180px;
-webkit-clip-path: polygon(0 0, 0% 100%, 100% 59%);
clip-path: polygon(0 0, 0% 100%, 100% 59%);
display:inline-block;
background: #8c3310; /* Old browsers */
background: -moz-linear-gradient(top, #8c3310 0%, #bf6e4e 100%);
background: -webkit-linear-gradient(top, #8c3310 0%,#bf6e4e 100%);
background: linear-gradient(to bottom, #8c3310 0%,#bf6e4e 100%);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#8c3310', endColorstr='#bf6e4e',GradientType=0 );
}

How to change start point of svg line animation

I use stroke-dasharray to create line animation when hovering it, would I have any way to change the start point?
as this photo
/* layout */
body {
margin: 50px;
}
.container{
left : 50%;
}
/* main */
.svg-outer:hover {
stroke-width: 8px;
stroke: rgba(255, 0, 0, 1);
animation: MaplogoConatinerStroke .25s linear 1;
}
#keyframes MaplogoConatinerStroke {
0% {
stroke-dasharray: 0 549.7;
}
100% {
stroke-dasharray: 549.7 0;
}
}
<div class="container">
Hover the logo
<svg xmlns="http://www.w3.org/2000/svg" class="svg-outer" viewBox="0 0 166.5 186.49">
<defs>
<style>
.svg-outer {
width: 25%;
overflow: initial!important;
}
.circle {
fill: rgba(0, 0, 0, 0.6);
}
</style>
</defs>
<g>
<g>
<path class="circle" d="M166.5,83.25a83.25,83.25,0,1,0-100,81.56v19.1a2.76,2.76,0,0,0,3.25,2.5c.69.35,34.46-19.69,45.67-26.36A83.27,83.27,0,0,0,166.5,83.25Z" />
</g>
</g>
</svg>
</div>
you can use stroke-dashoffset to change the starting point. with a bit of fiddeling around i came up with offset of 153...
/* layout */
body {
margin: 50px;
}
.container{
left : 50%;
}
/* main */
.svg-outer:hover {
stroke-width: 8px;
stroke: rgba(255, 0, 0, 1);
animation: MaplogoConatinerStroke .25s linear 1;
stroke-dashoffset: 153;
}
#keyframes MaplogoConatinerStroke {
0% {
stroke-dasharray: 0 549.7;
}
100% {
stroke-dasharray: 549.7 0;
}
}
<div class="container">
Hover the logo
<svg xmlns="http://www.w3.org/2000/svg" class="svg-outer" viewBox="0 0 166.5 186.49">
<defs>
<style>
.svg-outer {
width: 25%;
overflow: initial!important;
}
.circle {
fill: rgba(0, 0, 0, 0.6);
}
</style>
</defs>
<g>
<g>
<path class="circle" d="M166.5,83.25a83.25,83.25,0,1,0-100,81.56v19.1a2.76,2.76,0,0,0,3.25,2.5c.69.35,34.46-19.69,45.67-26.36A83.27,83.27,0,0,0,166.5,83.25Z" />
</g>
</g>
</svg>
</div>

Ring-shaped process spinner with fading gradient effect around the ring

I want to create a ring-shaped process spinner with CSS3 or JavaScript, similar to the loading progress spinner in Android.
The spinner should rotate continuously and be filled with a solid colour that fades out along the rim (i.e. a conical gradient) as in this picture:
How can I achieve this?
This would be trivially easy if only CSS or SVG had conical gradients! Until the conic-gradient() notation matures and gains support, we can approximate the effect by slicing up the gradient and covering the seams somehow.
Below you will find two solutions. The first solution uses an embedded SVG image; the second uses multiple CSS gradients and pseudo-elements.
Both start with a single div with a keyframe animation applied to make it rotate:
HTML:
<div class="spinner"></div>
CSS:
#keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spinner {
animation: rotate 1s linear infinite;
height: 200px;
width: 200px;
}
You can use a progress element if you prefer, but you will find it a pain to style. Also note that unless you're using something like prefixfree.js, you'll need to add the vendor-prefixed versions of the #keyframes at-rule and the transform and animation properties.
The SVG solution
#keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spinner {
animation: rotate 1s linear infinite;
background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwLDAgMjAwLDIwMCI+PGRlZnM+PGNsaXBQYXRoIGlkPSJyaW5nIj48cGF0aCBkPSJNMjAwLDEwMEExMDAsMTAwLDAsMSwxLDE5Ny44MSw3OS4yMUwxODguMDMsODEuMjlBOTAsOTAsMCwxLDAsMTkwLDEwMHoiLz48L2NsaXBQYXRoPjxmaWx0ZXIgaWQ9ImJsdXIiIHg9IjAiIHk9IjAiPjxmZUdhdXNzaWFuQmx1ciBpbj0iU291cmNlR3JhcGhpYyIgc3RkRGV2aWF0aW9uPSIzIiAvPjwvZmlsdGVyPjxwYXRoIGlkPSJwIiBkPSJNMjUwLDEwMEExNTAsMTUwLDAsMCwxLDI0Ni43MiwxMzEuMTlMMTAwLDEwMEEwLDAsMCwwLDAsMTAwLDEwMHoiIGZpbGw9ImN5YW4iLz48L2RlZnM+PGcgY2xpcC1wYXRoPSJ1cmwoI3JpbmcpIj48ZyBmaWx0ZXI9InVybCgjYmx1cikiIHRyYW5zZm9ybT0icm90YXRlKC02IDEwMCAxMDApIj48dXNlIHhsaW5rOmhyZWY9IiNwIiBmaWxsLW9wYWNpdHk9IjAiIHRyYW5zZm9ybT0icm90YXRlKDAgMTAwIDEwMCkiLz48dXNlIHhsaW5rOmhyZWY9IiNwIiBmaWxsLW9wYWNpdHk9Ii4wMyIgdHJhbnNmb3JtPSJyb3RhdGUoMTIgMTAwIDEwMCkiLz48dXNlIHhsaW5rOmhyZWY9IiNwIiBmaWxsLW9wYWNpdHk9Ii4wNyIgdHJhbnNmb3JtPSJyb3RhdGUoMjQgMTAwIDEwMCkiLz48dXNlIHhsaW5rOmhyZWY9IiNwIiBmaWxsLW9wYWNpdHk9Ii4xIiB0cmFuc2Zvcm09InJvdGF0ZSgzNiAxMDAgMTAwKSIvPjx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGwtb3BhY2l0eT0iLjE0IiB0cmFuc2Zvcm09InJvdGF0ZSg0OCAxMDAgMTAwKSIvPjx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGwtb3BhY2l0eT0iLjE3IiB0cmFuc2Zvcm09InJvdGF0ZSg2MCAxMDAgMTAwKSIvPjx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGwtb3BhY2l0eT0iLjIiIHRyYW5zZm9ybT0icm90YXRlKDcyIDEwMCAxMDApIi8+PHVzZSB4bGluazpocmVmPSIjcCIgZmlsbC1vcGFjaXR5PSIuMjQiIHRyYW5zZm9ybT0icm90YXRlKDg0IDEwMCAxMDApIi8+PHVzZSB4bGluazpocmVmPSIjcCIgZmlsbC1vcGFjaXR5PSIuMjgiIHRyYW5zZm9ybT0icm90YXRlKDk2IDEwMCAxMDApIi8+PHVzZSB4bGluazpocmVmPSIjcCIgZmlsbC1vcGFjaXR5PSIuMzEiIHRyYW5zZm9ybT0icm90YXRlKDEwOCAxMDAgMTAwKSIvPjx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGwtb3BhY2l0eT0iLjM0IiB0cmFuc2Zvcm09InJvdGF0ZSgxMjAgMTAwIDEwMCkiLz48dXNlIHhsaW5rOmhyZWY9IiNwIiBmaWxsLW9wYWNpdHk9Ii4zOCIgdHJhbnNmb3JtPSJyb3RhdGUoMTMyIDEwMCAxMDApIi8+PHVzZSB4bGluazpocmVmPSIjcCIgZmlsbC1vcGFjaXR5PSIuNDEiIHRyYW5zZm9ybT0icm90YXRlKDE0NCAxMDAgMTAwKSIvPjx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGwtb3BhY2l0eT0iLjQ1IiB0cmFuc2Zvcm09InJvdGF0ZSgxNTYgMTAwIDEwMCkiLz48dXNlIHhsaW5rOmhyZWY9IiNwIiBmaWxsLW9wYWNpdHk9Ii40OCIgdHJhbnNmb3JtPSJyb3RhdGUoMTY4IDEwMCAxMDApIi8+PHVzZSB4bGluazpocmVmPSIjcCIgZmlsbC1vcGFjaXR5PSIuNTIiIHRyYW5zZm9ybT0icm90YXRlKDE4MCAxMDAgMTAwKSIvPjx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGwtb3BhY2l0eT0iLjU1IiB0cmFuc2Zvcm09InJvdGF0ZSgxOTIgMTAwIDEwMCkiLz48dXNlIHhsaW5rOmhyZWY9IiNwIiBmaWxsLW9wYWNpdHk9Ii41OSIgdHJhbnNmb3JtPSJyb3RhdGUoMjA0IDEwMCAxMDApIi8+PHVzZSB4bGluazpocmVmPSIjcCIgZmlsbC1vcGFjaXR5PSIuNjIiIHRyYW5zZm9ybT0icm90YXRlKDIxNiAxMDAgMTAwKSIvPjx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGwtb3BhY2l0eT0iLjY2IiB0cmFuc2Zvcm09InJvdGF0ZSgyMjggMTAwIDEwMCkiLz48dXNlIHhsaW5rOmhyZWY9IiNwIiBmaWxsLW9wYWNpdHk9Ii42OSIgdHJhbnNmb3JtPSJyb3RhdGUoMjQwIDEwMCAxMDApIi8+PHVzZSB4bGluazpocmVmPSIjcCIgZmlsbC1vcGFjaXR5PSIuNyIgdHJhbnNmb3JtPSJyb3RhdGUoMjUyIDEwMCAxMDApIi8+PHVzZSB4bGluazpocmVmPSIjcCIgZmlsbC1vcGFjaXR5PSIuNzIiIHRyYW5zZm9ybT0icm90YXRlKDI2NCAxMDAgMTAwKSIvPjx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGwtb3BhY2l0eT0iLjc2IiB0cmFuc2Zvcm09InJvdGF0ZSgyNzYgMTAwIDEwMCkiLz48dXNlIHhsaW5rOmhyZWY9IiNwIiBmaWxsLW9wYWNpdHk9Ii43OSIgdHJhbnNmb3JtPSJyb3RhdGUoMjg4IDEwMCAxMDApIi8+PHVzZSB4bGluazpocmVmPSIjcCIgZmlsbC1vcGFjaXR5PSIuODMiIHRyYW5zZm9ybT0icm90YXRlKDMwMCAxMDAgMTAwKSIvPjx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGwtb3BhY2l0eT0iLjg2IiB0cmFuc2Zvcm09InJvdGF0ZSgzMTIgMTAwIDEwMCkiLz48dXNlIHhsaW5rOmhyZWY9IiNwIiBmaWxsLW9wYWNpdHk9Ii45MyIgdHJhbnNmb3JtPSJyb3RhdGUoMzI0IDEwMCAxMDApIi8+PHVzZSB4bGluazpocmVmPSIjcCIgZmlsbC1vcGFjaXR5PSIuOTciIHRyYW5zZm9ybT0icm90YXRlKDMzNiAxMDAgMTAwKSIvPjx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGwtb3BhY2l0eT0iMSIgdHJhbnNmb3JtPSJyb3RhdGUoMzQ4IDEwMCAxMDApIi8+PC9nPjwvZz48L3N2Zz4=') no-repeat;
height: 200px;
width: 200px;
}
<div class="spinner"></div>
Tested and working in IE 10, Chrome and Firefox.
Caveats
Changing the inner or outer radius of the ring is more painful than you might imagine, as it would require editing the clip path values. It's outside the scope of this answer to explain how to calculate it, but suffice to say it took a bit of geometry. I'll try to put a generator on GitHub if I get time.
How the SVG version works
That big blob of gibberish is just a Base64 encoded SVG image. Run it through a Base64 decoder and you'll see the original SVG image.
Here's the full image nicely indented and commented so you can see exactly how it works:
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0,0 200,200">
<defs>
<!-- Ring shape centred on 100, 100 with inner radius 90px, outer
radius 100px and a 12 degree gap at 348. -->
<clipPath id="ring">
<path d="M 200, 100
A 100, 100, 0, 1, 1, 197.81, 79.21
L 188.03, 81.29
A 90, 90, 0, 1, 0, 190, 100 z"/>
</clipPath>
<!-- Very simple Gaussian blur, used to visually merge sectors. -->
<filter id="blur" x="0" y="0">
<feGaussianBlur in="SourceGraphic" stdDeviation="3" />
</filter>
<!-- A 12 degree sector extending to 150px. -->
<path id="p" d="M 250, 100
A 150, 150, 0, 0, 1, 246.72, 131.19
L 100, 100
A 0, 0, 0, 0, 0, 100, 100 z" fill="cyan"/>
</defs>
<!-- Clip the blurred sectors to the ring shape. -->
<g clip-path="url(#ring)">
<!-- Blur the sectors together to make a smooth shape and rotate
them anti-clockwise by 6 degrees to hide the seam where the
fully opaque sector blurs with the fully transparent one. -->
<g filter="url(#blur)" transform="rotate(-6 100 100)">
<!-- Each successive sector increases in opacity and is rotated
by a further 12 degrees. -->
<use xlink:href="#p" fill-opacity="0" transform="rotate( 0 100 100)"/>
<use xlink:href="#p" fill-opacity="0.03" transform="rotate( 12 100 100)"/>
<use xlink:href="#p" fill-opacity="0.07" transform="rotate( 24 100 100)"/>
<use xlink:href="#p" fill-opacity="0.1" transform="rotate( 36 100 100)"/>
<use xlink:href="#p" fill-opacity="0.14" transform="rotate( 48 100 100)"/>
<use xlink:href="#p" fill-opacity="0.17" transform="rotate( 60 100 100)"/>
<use xlink:href="#p" fill-opacity="0.2" transform="rotate( 72 100 100)"/>
<use xlink:href="#p" fill-opacity="0.24" transform="rotate( 84 100 100)"/>
<use xlink:href="#p" fill-opacity="0.28" transform="rotate( 96 100 100)"/>
<use xlink:href="#p" fill-opacity="0.31" transform="rotate(108 100 100)"/>
<use xlink:href="#p" fill-opacity="0.34" transform="rotate(120 100 100)"/>
<use xlink:href="#p" fill-opacity="0.38" transform="rotate(132 100 100)"/>
<use xlink:href="#p" fill-opacity="0.41" transform="rotate(144 100 100)"/>
<use xlink:href="#p" fill-opacity="0.45" transform="rotate(156 100 100)"/>
<use xlink:href="#p" fill-opacity="0.48" transform="rotate(168 100 100)"/>
<use xlink:href="#p" fill-opacity="0.52" transform="rotate(180 100 100)"/>
<use xlink:href="#p" fill-opacity="0.55" transform="rotate(192 100 100)"/>
<use xlink:href="#p" fill-opacity="0.59" transform="rotate(204 100 100)"/>
<use xlink:href="#p" fill-opacity="0.62" transform="rotate(216 100 100)"/>
<use xlink:href="#p" fill-opacity="0.66" transform="rotate(228 100 100)"/>
<use xlink:href="#p" fill-opacity="0.69" transform="rotate(240 100 100)"/>
<use xlink:href="#p" fill-opacity="0.7" transform="rotate(252 100 100)"/>
<use xlink:href="#p" fill-opacity="0.72" transform="rotate(264 100 100)"/>
<use xlink:href="#p" fill-opacity="0.76" transform="rotate(276 100 100)"/>
<use xlink:href="#p" fill-opacity="0.79" transform="rotate(288 100 100)"/>
<use xlink:href="#p" fill-opacity="0.83" transform="rotate(300 100 100)"/>
<use xlink:href="#p" fill-opacity="0.86" transform="rotate(312 100 100)"/>
<use xlink:href="#p" fill-opacity="0.93" transform="rotate(324 100 100)"/>
<use xlink:href="#p" fill-opacity="0.97" transform="rotate(336 100 100)"/>
<use xlink:href="#p" fill-opacity="1" transform="rotate(348 100 100)"/>
</g>
</g>
</svg>
This is minified, Base64 encoded and used as an inline CSS background image. You may also serve it as a separate file if you prefer. Technically, it should be possible to embed the image without the Base64 encoding, but right now that only works in Chrome.
The pure CSS solution
This solution uses separate linear gradients in each quadrant and relies on visual similarity to cover up the seams. The ring shape is formed using pseudo-elements.
#keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spinner {
animation: rotate 1s linear infinite;
background: cyan;
border-radius: 50%;
height: 200px;
width: 200px;
position: relative;
}
.spinner::before,
.spinner::after {
content: '';
position: absolute;
}
.spinner::before {
border-radius: 50%;
background:
linear-gradient(0deg, hsla(0, 0%, 100%, 1 ) 50%, hsla(0, 0%, 100%, 0.9) 100%) 0% 0%,
linear-gradient(90deg, hsla(0, 0%, 100%, 0.9) 0%, hsla(0, 0%, 100%, 0.6) 100%) 100% 0%,
linear-gradient(180deg, hsla(0, 0%, 100%, 0.6) 0%, hsla(0, 0%, 100%, 0.3) 100%) 100% 100%,
linear-gradient(360deg, hsla(0, 0%, 100%, 0.3) 0%, hsla(0, 0%, 100%, 0 ) 100%) 0% 100%
;
background-repeat: no-repeat;
background-size: 50% 50%;
top: -1px;
bottom: -1px;
left: -1px;
right: -1px;
}
.spinner::after {
background: white;
border-radius: 50%;
top: 3%;
bottom: 3%;
left: 3%;
right: 3%;
}
<div class="spinner"></div>
Tested and working in IE 10, Chrome and Firefox.
Caveats
Unlike the SVG solution, this only works against a solid background colour. It also requires modification in several places if you want to change that colour, which is a pain.
How the pure CSS version works
To start with, the spinner is styled as a circle with a uniform background colour. This will be the colour of the spinning gradient.
.spinner {
background: cyan;
border-radius: 50%;
/* ... */
}
Set things up so that we can overlay the pseudo-elements on top of the spinner:
.spinner {
/* ... */
position: relative;
}
.spinner::before,
.spinner::after {
content: '';
position: absolute;
}
This is the tricky bit. Each quadrant of the :before pseudo-element is set to a different linear gradient starting with opaque white and progressively becoming more and more transparent. Towards the centre it's easy to see where the gradients join up, but notice how around the outside the colours are close enough together that they appear to join up smoothly.
.spinner::before {
border-radius: 50%;
background:
linear-gradient(0deg, hsla(0, 0%, 100%, 1 ) 50%, hsla(0, 0%, 100%, 0.9) 100%) 0% 0%,
linear-gradient(90deg, hsla(0, 0%, 100%, 0.9) 0%, hsla(0, 0%, 100%, 0.6) 100%) 100% 0%,
linear-gradient(180deg, hsla(0, 0%, 100%, 0.6) 0%, hsla(0, 0%, 100%, 0.3) 100%) 100% 100%,
linear-gradient(360deg, hsla(0, 0%, 100%, 0.3) 0%, hsla(0, 0%, 100%, 0 ) 100%) 0% 100%
;
background-repeat: no-repeat;
background-size: 50% 50%;
top: -1px;
bottom: -1px;
left: -1px;
right: -1px;
}
This is positioned so that it goes slightly over the edge of the spinner because if we position it right to the edge a faint rim of the background colour is visible.
Finally, hide the middle bit using the ::after pseudo-element to make a ring shape:
.spinner::after {
background: white;
border-radius: 50%;
top: 3%;
bottom: 3%;
left: 3%;
right: 3%;
}
Et voilá!
We can easily create this with only single div.
.loader {
--border-width: 10px;
height: 200px;
width: 200px;
border-radius: 50%;
/* 0.5px's are needed to avoid hard-stopping */
--mask: radial-gradient(
farthest-side,
transparent calc(100% - var(--border-width) - 0.5px),
#000 calc(100% - var(--border-width) + 0.5px)
);
-webkit-mask: var(--mask);
mask: var(--mask);
/* we're using two half linear-gradient which is masked by the radial-gradient */
background: linear-gradient(to top, rgba(0,255,226, 1), rgba(0,255,226, 0.5)) 100% 0/50% 100% no-repeat,
linear-gradient(rgba(0,255,226, 0.5) 50%, transparent 95%) 0 0/50% 100% no-repeat;
animation: spin 1s linear infinite;
}
#keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
<div class="loader"></div>
And this is the comparison of my answer with #Jordan Gray's answer by setting background to body:
#Jordan Gray's answer:
body {
background: pink;
}
#keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spinner {
animation: rotate 1s linear infinite;
background: cyan;
border-radius: 50%;
height: 200px;
width: 200px;
position: relative;
}
.spinner::before,
.spinner::after {
content: '';
position: absolute;
}
.spinner::before {
border-radius: 50%;
background:
linear-gradient(0deg, hsla(0, 0%, 100%, 1 ) 50%, hsla(0, 0%, 100%, 0.9) 100%) 0% 0%,
linear-gradient(90deg, hsla(0, 0%, 100%, 0.9) 0%, hsla(0, 0%, 100%, 0.6) 100%) 100% 0%,
linear-gradient(180deg, hsla(0, 0%, 100%, 0.6) 0%, hsla(0, 0%, 100%, 0.3) 100%) 100% 100%,
linear-gradient(360deg, hsla(0, 0%, 100%, 0.3) 0%, hsla(0, 0%, 100%, 0 ) 100%) 0% 100%
;
background-repeat: no-repeat;
background-size: 50% 50%;
top: -1px;
bottom: -1px;
left: -1px;
right: -1px;
}
.spinner::after {
background: white;
border-radius: 50%;
top: 3%;
bottom: 3%;
left: 3%;
right: 3%;
}
<div class="spinner"></div>
My answer:
body {
background: pink;
}
.loader {
--border-width: 10px;
height: 200px;
width: 200px;
border-radius: 50%;
/* 0.5px's are needed to avoid hard-stopping */
--mask: radial-gradient(
farthest-side,
transparent calc(100% - var(--border-width) - 0.5px),
#000 calc(100% - var(--border-width) + 0.5px)
);
-webkit-mask: var(--mask);
mask: var(--mask);
/* we're using two half linear-gradient which is masked by the radial-gradient */
background: linear-gradient(to top, rgba(0,255,226, 1), rgba(0,255,226, 0.5)) 100% 0/50% 100% no-repeat,
linear-gradient(rgba(0,255,226, 0.5) 50%, transparent 95%) 0 0/50% 100% no-repeat;
animation: spin 1s linear infinite;
}
#keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
<div class="loader"></div>
Using background-clip on single div:
div {
margin: 0 auto;
margin-top: 3rem;
}
.spnr {
height: 100px;
width: 100px;
border-radius: 50%;
border: 5px solid transparent;
animation: spin 1s linear infinite;
background: linear-gradient(white, white), conic-gradient(from 0.15turn, white, #00EBD3);
background-origin: border-box;
background-clip: content-box, border-box;
}
#keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
<div class="spnr"></div>
More variations:
:root {
--bg: white;
--wbg: linear-gradient(var(--bg), var(--bg));
}
.dark>div {
--bg: black;
--wbg: linear-gradient(var(--bg), var(--bg));
}
.dark {
background: black;
}
div {
display: inline-block;
margin: 0 1rem;
margin-top: 0.5rem;
height: 200px;
}
.one {
background: var(--wbg), conic-gradient(from 0.15turn, transparent, #00EBD3);
}
.two {
background: var(--wbg), conic-gradient(from 0.15turn, transparent, transparent, #00EBD3);
}
.three {
background: var(--wbg), conic-gradient(from 0.15turn, transparent 0.0turn, transparent .04turn, pink 0.49turn, pink 0.5turn, transparent 0.50turn, transparent 0.55turn, pink 0.99999turn);
}
.four {
background: var(--wbg), conic-gradient(from 0.25turn, transparent 0.0turn, darkgreen, transparent, darkgreen, transparent, darkgreen, transparent, darkgreen, transparent);
}
.five {
background: var(--wbg), conic-gradient(from 0.25turn, transparent 0.0turn, red 0.125turn, transparent 0.125turn, red .25turn, transparent .25turn, red 0.375turn, transparent .375turn, red 0.5turn, transparent .5turn, red 0.625turn, transparent .625turn, red 0.75turn, transparent .75turn, red 0.875turn, transparent .875turn, red 1turn, transparent 1turn);
animation-duration: 2s;
}
.six {
border-width: 15px;
background: var(--wbg), conic-gradient(from 0.25turn, transparent 0.0turn, transparent .125turn, orange 0.125turn, orange .25turn, transparent .25turn, transparent .375turn, orange 0.375turn, orange 0.5turn, transparent .5turn, transparent.625turn, orange .625turn, orange 0.75turn, transparent .75turn, transparent 0.875turn, orange .875turn, orange 1turn, transparent 1turn);
opacity: 0.7;
}
.spnr {
height: 60px;
width: 60px;
border-radius: 50%;
border: 5px solid transparent;
animation: spin 1s linear infinite;
background-origin: border-box;
background-clip: content-box, border-box;
}
.six {
animation: size 2s linear infinite alternate;
}
.five {
animation-duration: 2s;
}
#keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#keyframes size {
0% {
transform: rotate(0deg) scale(0.2);
border-width: 5px;
}
100% {
border-width: 10px;
transform: rotate(840deg) scale(1);
}
}
<div>
<div class="spnr one"></div>
<div class="spnr two"></div>
<div class="spnr three"></div>
<div class="spnr four"></div>
<div class="spnr five"></div>
<div class="spnr six"></div>
</div>
<div class="dark">
<div class="spnr one"></div>
<div class="spnr two"></div>
<div class="spnr three"></div>
<div class="spnr four"></div>
<div class="spnr five"></div>
<div class="spnr six"></div>
</div>
conic-gradient with mask and no complex values:
.ring {
width: 150px; /* the size */
padding: 8px; /* the border */
background: #07e8d6; /* the color */
aspect-ratio: 1;
border-radius: 50%;
-webkit-mask:
conic-gradient(#0000,#000),
linear-gradient(#000 0 0) content-box;
-webkit-mask-composite: source-out;
mask-composite: subtract;
box-sizing: border-box;
animation:r 2s linear infinite;
}
#keyframes r {to{transform:rotate(1turn)}}
body {
background:linear-gradient(90deg,pink,#fff);
}
<div class="ring"></div>
I was able to create a real, full circle, gradient spinner, with opacity, by using a linear gradient on two half circles, and aligning them. No JavaScript or CSS necessary.
<svg
version="1.1"
width="24" height="24"
viewBox="-1 -1 25 25"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<linearGradient x1="0%" y1="0%" x2="100%" y2="0" id="gradient-1">
<stop stop-color="red" offset="0%" />
<stop stop-color="red" offset="63.1%" stop-opacity=".631" />
<stop stop-color="red" offset="100%" stop-opacity=".5" />
</linearGradient>
<linearGradient x1="0%" y1="0%" x2="100%" y2="0" id="gradient-2">
<stop stop-color="red" offset="0%" stop-opacity=".5" />
<stop stop-color="red" offset="63.1%" stop-opacity=".12" />
<stop stop-color="red" offset="100%" stop-opacity="0" />
</linearGradient>
</defs>
<g fill="none">
<g transform="translate(1 1)">
<path
d="M 10.5 10.5 m -10.5 0a 10.5 10.5 0 1 0 21 0a"
stroke="url(#gradient-1)"
stroke-width="3"
/>
<animateTransform
attributeName="transform"
type="rotate"
from="0 10.5 10.5"
to="360 10.5 10.5"
dur="1s"
repeatCount="indefinite"
/>
</g>
<g transform="translate(1 1)">
<path
d="M 10.5 10.5 m -10.5 0a 10.5 10.5 0 1 0 21 0a"
stroke="url(#gradient-2)"
stroke-width="3"
/>
<animateTransform
attributeName="transform"
type="rotate"
from="-180 10.5 10.5"
to="180 10.5 10.5"
dur="1s"
repeatCount="indefinite"
/>
</g>
</g>
</svg>