Spinner
Also called loading spinner, loading indicator, or throbber
A small looping animation, usually a rotating ring, that shows the app is busy with a short task of unknown length.
import { useState } from 'react';
const SPINNER_DELAY_MS = 300; // quick saves never flash a spinner
const SAVED_FOR_MS = 1500;
type Status = 'idle' | 'pending' | 'saving' | 'saved';
const LABELS: Record<Status, string> = {
idle: 'Save changes',
pending: 'Save changes',
saving: 'Saving…',
saved: 'Saved'
};
export function SaveButton({ onSave }: { onSave: () => Promise<void> }) {
const [status, setStatus] = useState<Status>('idle');
async function save() {
setStatus('pending');
const timer = setTimeout(() => setStatus('saving'), SPINNER_DELAY_MS);
try {
await onSave();
setStatus('saved');
setTimeout(() => setStatus('idle'), SAVED_FOR_MS);
} catch {
setStatus('idle'); // onSave reports its own error
} finally {
clearTimeout(timer);
}
}
return (
<>
{/* min-w keeps the width steady as the label changes. */}
<button
type='button'
onClick={save}
disabled={status !== 'idle'}
className='inline-flex h-10 min-w-36 items-center justify-center gap-2 rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground disabled:opacity-80'
>
{status === 'saving' && (
// One quarter of the ring in the text color, the rest at 20%, one turn per 700ms.
<span className='size-4 animate-spin rounded-full border-2 border-current/20 border-t-current [animation-duration:700ms] motion-reduce:[animation-duration:2s]' />
)}
{status === 'saved' && '✓'}
{LABELS[status]}
</button>
<span role='status' className='sr-only'>
{status === 'saving' ? 'Saving' : status === 'saved' ? 'Saved' : ''}
</span>
</>
);
}When the user clicks Save changes, disable the button, keep its width, and show a 16px spinning ring with the label "Saving…". The ring is a circle with a 2px stroke, one quarter of it in the text color and the rest at 20% opacity, rotating once every 700ms with linear easing. Only show the spinner if saving takes longer than 300ms, then show "Saved" with a checkmark for 1.5s.
Paste into Layerwise or any AI coding tool.
What is Spinner?
A spinner is the simplest loading indicator: a ring, a set of dots, or a few bars that animate in a loop. It tells people the app heard them and is working, without saying how long the wait will be.
Use it for short waits of about one to a few seconds where the layout won’t change much: saving a form, sending a message, refreshing a small panel. Inside a button, show the spinner next to a label like “Saving…”, keep the button the same width, and disable it so people don’t submit twice.
For a page or panel of content that is loading, a skeleton screen previews the layout instead. For long tasks you can measure, such as an upload, a progress bar shows how far along it is. Wait about 300ms before showing a spinner so quick actions don’t flash, and give it an accessible label, for example role="status" with the text “Saving”.
Often confused with
- Skeleton screen
- A skeleton screen previews the layout of content that is loading. A spinner only says the app is busy, so it suits short actions like saving.
- Progress bar
- A progress bar shows how much of a task is done. A spinner suits short waits whose length the app cannot measure.
Related terms
All termsProgress bar
A horizontal bar that fills from left to right to show how much of a task is done, or slides in a loop while the total is still unknown.
Skeleton screen
A loading placeholder made of gray blocks shaped like the content on its way, often with a soft shimmer, that the real content replaces.
Loading state
The view a screen shows while its data is on the way, designed together with the outcomes that follow: loaded, empty, or error.
Looping animation
An animation that plays more than once, either a set number of times or forever, set in CSS with animation-iteration-count.
Build it with Layerwise
Describe your app, watch it run in a live preview, and publish it with a database, storage, and sign-in built in.