Saving status
Edits save as you make them. A small dot says a change is still on its way; words appear only when a save takes long or fails.
Playground
Change the controls to try each prop. The code under the preview updates as you go and lists only the props you changed; copy it into your page.
import { PendingDot } from '@/ui/saving-status';<PendingDot label="Saving" />Pending dot
The 6px dot pulses while a change is in flight. muted sits in the selection column of a new row, faint beside a cell's value, warning where the wait is worth watching.
import { PendingDot } from '@/ui/saving-status';
export default function SavingStatusDot() {
return (
<div className="flex flex-wrap items-center gap-400 text-body leading-body text-text">
<span className="flex items-center gap-100">
<PendingDot />
Maya Chen
</span>
<span className="flex items-center gap-100">
Halcyon Bank
<PendingDot tone="faint" />
</span>
<span className="flex items-center gap-100">
Sourcing
<PendingDot tone="warning" label="Finding people" />
</span>
</div>
);
}The dot goes when the save lands
Press Replay. No "Saved" text and no toast: the dot going is the answer.
import { useState } from 'react';
import { Button } from '@/ui/button';
import { PendingDot } from '@/ui/saving-status';
// Press Replay: the dot shows while the change is on its way and goes when it lands.
export default function SavingStatusCell() {
const [saving, setSaving] = useState(false);
const save = () => {
setSaving(true);
setTimeout(() => setSaving(false), 2400);
};
return (
<div className="flex items-center gap-400">
<span className="flex w-40 items-center gap-100 text-body leading-body text-text">
Orbital Freight
{saving ? <PendingDot tone="faint" /> : null}
</span>
<Button size="sm" onClick={save} disabled={saving}>
Replay
</Button>
</div>
);
}Words in the toolbar
A shared View says it is saving in words, in a 160px slot at the toolbar's end so the toolbar doesn't move. Press Replay: the words fade in after 400ms, so a quick save shows nothing.
import { useState } from 'react';
import { Button } from '@/ui/button';
import { SavingStatus } from '@/ui/saving-status';
// Press Replay: a View change goes out a second later, its words fade in once the save has run 400ms, and clear when it
// lands.
export default function SavingStatusToolbar() {
const [saving, setSaving] = useState(true);
const replay = () => {
setSaving(false);
setTimeout(() => setSaving(true), 1200);
setTimeout(() => setSaving(false), 3600);
};
return (
<div className="flex w-full flex-col items-start gap-200">
<div className="flex h-11 w-full items-center gap-100 rounded-xlarge px-150 shadow-ring">
<Button variant="ghost">Filter</Button>
<Button variant="ghost">Sort</Button>
<div className="grow" />
<SavingStatus>{saving ? 'Saving shared View…' : null}</SavingStatus>
</div>
<Button size="sm" onClick={replay}>
Replay
</Button>
</div>
);
}No answer
The answer never came. The failure stays on screen with the exact fix until the person picks it.
import { Button } from '@/ui/button';
import { SaveFailure } from '@/ui/saving-status';
export default function SavingStatusFailure() {
return <SaveFailure actions={<Button>Retry same request</Button>}>Ascern didn’t hear back, so this View may not have saved.</SaveFailure>;
}Conflict
Someone else saved first. Offer both ways out.
import { Button } from '@/ui/button';
import { SaveFailure } from '@/ui/saving-status';
export default function SavingStatusConflict() {
return (
<SaveFailure
actions={
<>
<Button>Use shared View</Button>
<Button>Keep my changes for review</Button>
</>
}
>
Tom changed this View while you were editing it.
</SaveFailure>
);
}Couldn't save
A save that was refused or never answered. Say what didn't save and that the edit is kept, then the fix. It stays until the person acts; never a toast that closes on its own.
import { Button } from '@/ui/button';
import { SaveFailure } from '@/ui/saving-status';
// A cell's save was refused or never answered. The edit stays in the cell.
export default function SavingStatusFailed() {
return <SaveFailure actions={<Button>Try again</Button>}>Couldn’t save the salary. Your edit is kept; try again.</SaveFailure>;
}Offline
The connection dropped mid-save. The dot turns to the warning tone and the line says the edit is kept. Press Try again, then Replay.
import { useState } from 'react';
import { Button } from '@/ui/button';
import { PendingDot, SaveFailure, SavingStatus } from '@/ui/saving-status';
// The connection dropped mid-save. Press Try again: the save goes when the browser is back online.
export default function SavingStatusOffline() {
const [state, setState] = useState<'offline' | 'saving' | 'saved'>('offline');
const retry = () => {
setState('saving');
setTimeout(() => setState('saved'), 1500);
};
return (
<div className="flex w-full flex-col items-start gap-200">
<span className="flex items-center gap-100 text-body leading-body text-text">
Halcyon Bank
{state === 'saved' ? null : <PendingDot tone="warning" label={state === 'offline' ? 'Not saved' : 'Saving'} />}
</span>
{state === 'offline' ? (
<SaveFailure actions={<Button onClick={retry}>Try again</Button>}>You’re offline, so this change didn’t save. Your edit is kept.</SaveFailure>
) : (
<SavingStatus>{state === 'saving' ? 'Saving…' : null}</SavingStatus>
)}
{state === 'saved' ? (
<Button size="sm" onClick={() => setState('offline')}>
Replay
</Button>
) : null}
</div>
);
}Progress bar
A thin bar for work that takes longer than a moment and runs while you keep working: reading a mailbox, finding people, importing a file.
Section message
A message about one part of a page, placed at the top of that part. Middle of three levels: a banner speaks for the whole app, an inline message for one control.