A frosted image preview clears as a big percentage counts to 100 inside a progress ring, then a Ready chip pops in. Counter, ring and blur all read one animated integer @property.
Also known as: image generation progress animation css, frosted glass loading, percentage loader over image, ai image loading complete state.
<div class="aia-frosted-progress" role="status" aria-label="Generating image">
<span class="aia-frosted-progress-image"></span>
<span class="aia-frosted-progress-frost"></span>
<span class="aia-frosted-progress-ring"></span>
<span class="aia-frosted-progress-num"></span>
<span class="aia-frosted-progress-ready">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5.5 12.5l4 4 9-9.5"/></svg>Ready
</span>
<span class="aia-frosted-progress-prompt">Glass koi in morning fog</span>
</div>@property --aia-frosted-progress-p {
syntax: "<integer>";
inherits: true;
initial-value: 0;
}
.aia-frosted-progress {
--c: var(--aia-color, #7c5cff);
font-size: var(--aia-size, 16px);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
line-height: 1.4;
--t: 6.5s;
position: relative;
display: grid;
place-items: center;
width: 10.5em;
height: 12.6em;
border-radius: 1em;
overflow: hidden;
color: #fff;
box-shadow: 0 0 0 1px color-mix(in srgb, currentColor 12%, transparent), 0 1em 2em -1.1em rgba(0, 0, 0, 0.5);
animation: aia-frosted-progress-p var(--t) cubic-bezier(0.2, 0.6, 0.35, 1) infinite;
}
.aia-frosted-progress > span { grid-area: 1 / 1; }
.aia-frosted-progress-image {
position: absolute;
inset: -10%;
background:
radial-gradient(ellipse 30% 16% at 40% 46%, #fff6e8 0 30%, color-mix(in oklab, var(--c) 15%, #ffb35c) 55%, transparent 72%),
radial-gradient(ellipse 12% 9% at 64% 40%, color-mix(in oklab, var(--c) 30%, #ffb35c), transparent 70%),
radial-gradient(circle at 75% 78%, color-mix(in oklab, var(--c) 40%, #3ddcff), transparent 45%),
radial-gradient(circle at 20% 20%, color-mix(in srgb, var(--c) 55%, #fff), transparent 50%),
linear-gradient(160deg, color-mix(in srgb, var(--c) 35%, #fff), var(--c) 55%, color-mix(in srgb, var(--c) 55%, #000));
filter: blur(calc((100 - var(--aia-frosted-progress-p)) * 0.016em)) saturate(calc(0.5 + var(--aia-frosted-progress-p) * 0.005));
}
.aia-frosted-progress-frost {
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.18);
opacity: calc(1 - var(--aia-frosted-progress-p) / 100);
}
.aia-frosted-progress-ring {
width: 5.4em;
height: 5.4em;
border-radius: 50%;
background: conic-gradient(#fff calc(var(--aia-frosted-progress-p) * 1%), rgba(255, 255, 255, 0.25) 0);
-webkit-mask: radial-gradient(closest-side, transparent calc(100% - 0.22em), #000 calc(100% - 0.2em));
mask: radial-gradient(closest-side, transparent calc(100% - 0.22em), #000 calc(100% - 0.2em));
animation: aia-frosted-progress-busy var(--t) ease infinite;
}
.aia-frosted-progress-num {
font-size: 1.7em;
font-weight: 600;
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
text-shadow: 0 0.05em 0.4em rgba(0, 0, 0, 0.25);
counter-reset: aia-frosted-progress-p var(--aia-frosted-progress-p);
animation: aia-frosted-progress-busy var(--t) ease infinite;
}
.aia-frosted-progress-num::after { content: counter(aia-frosted-progress-p) "%"; }
.aia-frosted-progress-ready {
display: flex;
align-items: center;
gap: 0.3em;
padding: 0.35em 0.75em 0.35em 0.55em;
border-radius: 99em;
font-size: 0.85em;
font-weight: 600;
color: #111;
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 0.4em 1em -0.3em rgba(0, 0, 0, 0.35);
opacity: 0;
animation: aia-frosted-progress-ready var(--t) cubic-bezier(0.34, 1.56, 0.64, 1) infinite;
}
.aia-frosted-progress-ready svg {
width: 1.1em;
fill: none;
stroke: var(--c);
stroke-width: 2.6;
stroke-linecap: round;
stroke-linejoin: round;
}
.aia-frosted-progress-prompt {
position: absolute;
left: 0.7em;
right: 0.7em;
bottom: 0.7em;
padding: 0.35em 0.6em;
border-radius: 0.5em;
font-size: 0.72em;
font-weight: 500;
background: rgba(0, 0, 0, 0.28);
-webkit-backdrop-filter: blur(0.5em);
backdrop-filter: blur(0.5em);
}
@keyframes aia-frosted-progress-p {
0% { --aia-frosted-progress-p: 0; }
62%, 86% { --aia-frosted-progress-p: 100; }
100% { --aia-frosted-progress-p: 0; }
}
@keyframes aia-frosted-progress-busy {
0%, 60% { opacity: 1; scale: 1; }
66%, 96% { opacity: 0; scale: 1.15; }
100% { opacity: 1; scale: 1; }
}
@keyframes aia-frosted-progress-ready {
0%, 63% { opacity: 0; scale: 0.6; }
68%, 84% { opacity: 1; scale: 1; }
90%, 100% { opacity: 0; scale: 0.9; }
}
@media (prefers-reduced-motion: reduce) {
.aia-frosted-progress { animation: none; --aia-frosted-progress-p: 100; }
.aia-frosted-progress-ring,
.aia-frosted-progress-num { animation: none; opacity: 0; }
.aia-frosted-progress-ready { animation: none; opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
.aia-frosted-progress, .aia-frosted-progress *, .aia-frosted-progress::before, .aia-frosted-progress::after, .aia-frosted-progress *::before, .aia-frosted-progress *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}--aia-color on the element or a parent.
Currently previewing #3b82f6.--aia-size. Every other dimension is in
em, so the whole thing scales together. Currently previewing 16px.transition or animation
shorthand. Staggered delays are relative, so they keep their rhythm.