An inline 'Saving' status whose spinner turns into a check as the text rolls over to 'Saved', then back again. Built for editors and settings pages that save as you type.
Also known as: saving indicator, saving to saved animation, autosave status.
<div class="ldr-autosave-indicator" role="status" aria-label="Saving"> <span class="ldr-autosave-indicator-icon" aria-hidden="true"><i></i></span> <span class="ldr-autosave-indicator-text" aria-hidden="true"><span>Saving…</span><span>Saved</span></span> </div>
.ldr-autosave-indicator {
--s: var(--ldr-size, 48px);
--c: var(--ldr-color, #3b82f6);
display: inline-flex;
align-items: center;
gap: 0.5em;
font: 500 calc(var(--s) / 2.8) / 1.3 system-ui, sans-serif;
color: color-mix(in srgb, currentColor 70%, transparent);
}
.ldr-autosave-indicator-icon {
position: relative;
width: 1.1em;
height: 1.1em;
}
.ldr-autosave-indicator-icon::before {
content: "";
position: absolute;
inset: 0;
box-sizing: border-box;
border: 0.14em solid color-mix(in srgb, var(--ldr-color, #3b82f6) 25%, transparent);
border-top-color: var(--c);
border-radius: 50%;
animation:
ldr-autosave-indicator-spin 0.7s linear infinite,
ldr-autosave-indicator-ring 3.2s cubic-bezier(0.65, 0, 0.35, 1) infinite;
}
.ldr-autosave-indicator-icon i {
position: absolute;
inset: 0;
border-radius: 50%;
background: var(--c);
scale: 0;
animation: ldr-autosave-indicator-check 3.2s infinite;
}
.ldr-autosave-indicator-icon i::after {
content: "";
position: absolute;
left: 38%;
top: 22%;
width: 22%;
height: 42%;
border: solid #fff;
border-width: 0 0.12em 0.12em 0;
transform: rotate(45deg);
}
.ldr-autosave-indicator-text {
display: grid;
height: 1.3em;
overflow: hidden;
}
.ldr-autosave-indicator-text span {
grid-area: 1 / 1;
animation: ldr-autosave-indicator-saving 3.2s cubic-bezier(0.65, 0, 0.35, 1) infinite;
}
.ldr-autosave-indicator-text span + span {
color: var(--c);
animation-name: ldr-autosave-indicator-saved;
}
@keyframes ldr-autosave-indicator-spin {
to { transform: rotate(360deg); }
}
@keyframes ldr-autosave-indicator-ring {
0%, 52% { opacity: 1; scale: 1; }
60%, 92% { opacity: 0; scale: 0.5; }
100% { opacity: 1; scale: 1; }
}
@keyframes ldr-autosave-indicator-check {
0%, 54% { scale: 0; animation-timing-function: cubic-bezier(0.34, 1.7, 0.64, 1); }
64%, 88% { scale: 1; animation-timing-function: cubic-bezier(0.65, 0, 0.35, 1); }
95%, 100% { scale: 0; }
}
@keyframes ldr-autosave-indicator-saving {
0%, 52% { transform: translateY(0); opacity: 1; }
60% { transform: translateY(-100%); opacity: 0; }
61%, 90% { transform: translateY(100%); opacity: 0; }
98%, 100% { transform: translateY(0); opacity: 1; }
}
@keyframes ldr-autosave-indicator-saved {
0%, 54% { transform: translateY(100%); opacity: 0; }
62%, 88% { transform: translateY(0); opacity: 1; }
96%, 100% { transform: translateY(-100%); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.ldr-autosave-indicator, .ldr-autosave-indicator *, .ldr-autosave-indicator::before, .ldr-autosave-indicator::after, .ldr-autosave-indicator *::before, .ldr-autosave-indicator *::after {
animation-duration: 3s;
}
}--ldr-color on the element or a parent.
Currently previewing #3b82f6.--ldr-size. Every dimension in the snippet is
derived from it. Currently previewing 64px.animation shorthand.
Delays on the children are relative, so they keep their rhythm.role="status" and an
aria-label so screen readers announce the wait. The reduced-motion rule
at the end slows the animation for users who asked for less motion.