A disabled button with a small spinner beside its label, for form submits and 'Save' actions. The spinner inherits the button's text color so it works on any brand color.
Also known as: submit button loader, loading button.
<button class="ldr-button-spinner" disabled aria-busy="true"> <span aria-hidden="true"></span> Saving… </button>
.ldr-button-spinner {
--s: var(--ldr-size, 48px);
--c: var(--ldr-color, #3b82f6);
display: inline-flex;
align-items: center;
gap: calc(var(--s) / 5);
padding: calc(var(--s) / 4) calc(var(--s) / 2);
border: 0;
border-radius: calc(var(--s) / 6);
background: var(--c);
color: #fff;
font: 600 calc(var(--s) / 3) system-ui, sans-serif;
cursor: progress;
opacity: 0.85;
}
.ldr-button-spinner span {
width: calc(var(--s) / 3);
height: calc(var(--s) / 3);
border: 2px solid rgba(255, 255, 255, 0.35);
border-top-color: currentColor;
border-radius: 50%;
animation: ldr-button-spinner-spin 0.7s linear infinite;
}
@keyframes ldr-button-spinner-spin {
to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
.ldr-button-spinner, .ldr-button-spinner *, .ldr-button-spinner::before, .ldr-button-spinner::after {
animation-duration: 3s;
}
}The same loader using only Tailwind's built-in animation utilities. No custom keyframes or config needed.
<button disabled aria-busy="true" class="inline-flex items-center gap-2 rounded-md bg-blue-500 px-4 py-2 font-semibold text-white opacity-85 cursor-progress"> <span class="h-4 w-4 animate-spin rounded-full border-2 border-white/40 border-t-white"></span> Saving… </button>
--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.