Autosave Indicator

Shows content is being saved automatically.

All changes saved 2 minutes ago

html

<div class="autosave autosave--saved" data-state="saved" role="status" aria-live="polite" style="--accent:#A8E6C1;">
  <span class="autosave__dot" aria-hidden="true">
    <svg class="autosave__icon autosave__icon--saved" viewBox="0 0 20 20" width="14" height="14"><path d="M4 10.5l4 4 8-9" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
    <svg class="autosave__icon autosave__icon--saving" viewBox="0 0 20 20" width="14" height="14"><circle cx="10" cy="10" r="7" fill="none" stroke="currentColor" stroke-width="2.2" stroke-dasharray="24 12"/></svg>
    <svg class="autosave__icon autosave__icon--error" viewBox="0 0 20 20" width="14" height="14"><path d="M10 3l8.5 14.5H1.5L10 3z" fill="none" stroke="currentColor" stroke-width="2"/><path d="M10 8v4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><circle cx="10" cy="14.5" r="1" fill="currentColor"/></svg>
  </span>
  
  <span class="autosave__text">
    <span class="autosave__message">All changes saved</span>
    <span class="autosave__time">2 minutes ago</span>
  </span>
  
</div>
<style>
.autosave { font: 14px/1.4 system-ui, sans-serif; display: inline-flex; align-items: center; gap: 8px; padding: 8px 14px; border-radius: 999px; background: #FAF7F2; color: #241430; border: 1px solid #24143022; }
.autosave__dot { position: relative; width: 16px; height: 16px; display: inline-flex; align-items: center; justify-content: center; color: var(--accent); }
.autosave__icon { position: absolute; display: none; }
.autosave--saved .autosave__icon--saved { display: block; }
.autosave--saving .autosave__icon--saving { display: block; }
@media (prefers-reduced-motion: no-preference) {
.autosave--saving .autosave__icon--saving { animation: autosave-spin .2s linear infinite; }
}
.autosave--error .autosave__icon--error { display: block; color: #b3261e; }
.autosave__text { display: flex; flex-direction: column; line-height: 1.25; }
.autosave__message { font-weight: 600; }
.autosave__time { font-size: 12px; color: #24143099; }
@keyframes autosave-spin { to { transform: rotate(360deg); } }
</style>

tailwind

<div role="status" aria-live="polite" data-state="saved" style="--accent:#A8E6C1;" class="autosave-tw autosave-tw--saved inline-flex items-center gap-2 px-3.5 py-2 rounded-full bg-[#FAF7F2] text-[#241430] border border-[#24143022] font-sans text-sm">
  <span aria-hidden="true" class="relative w-4 h-4 inline-flex items-center justify-center" style="color:#A8E6C1">
    <svg class="autosave-tw__icon autosave-tw__icon--saved absolute" viewBox="0 0 20 20" width="14" height="14"><path d="M4 10.5l4 4 8-9" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
    <svg class="autosave-tw__icon autosave-tw__icon--saving absolute" viewBox="0 0 20 20" width="14" height="14"><circle cx="10" cy="10" r="7" fill="none" stroke="currentColor" stroke-width="2.2" stroke-dasharray="24 12"/></svg>
    <svg class="autosave-tw__icon autosave-tw__icon--error absolute" viewBox="0 0 20 20" width="14" height="14"><path d="M10 3l8.5 14.5H1.5L10 3z" fill="none" stroke="currentColor" stroke-width="2"/><path d="M10 8v4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><circle cx="10" cy="14.5" r="1" fill="currentColor"/></svg>
  </span>
  
  <span class="flex flex-col leading-tight">
    <span class="font-semibold">All changes saved</span>
    <span class="text-xs text-[#241430]/60">2 minutes ago</span>
  </span>
  
</div>
<style>
.autosave-tw__icon { display: none; }
.autosave-tw--saved .autosave-tw__icon--saved { display: block; }
.autosave-tw--saving .autosave-tw__icon--saving { display: block; }
@media (prefers-reduced-motion: no-preference) {
.autosave-tw--saving .autosave-tw__icon--saving { animation: autosave-tw-spin .2s linear infinite; }
}
.autosave-tw--error .autosave-tw__icon--error { display: block; color: #b3261e; }
@keyframes autosave-tw-spin { to { transform: rotate(360deg); } }
</style>

react

export function AutosaveIndicator({
  message = "All changes saved",
  timestamp = "2 minutes ago",
  state = "saved",
  accent = "#A8E6C1",
  compact = false,
}) {
  const icons = {
    saved: <path d="M4 10.5l4 4 8-9" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />,
    saving: <circle cx="10" cy="10" r="7" fill="none" stroke="currentColor" strokeWidth="2.2" strokeDasharray="24 12" />,
    error: <path d="M10 3l8.5 14.5H1.5L10 3z" fill="none" stroke="currentColor" strokeWidth="2" />,
  };
  return (
    <div role="status" aria-live="polite" style={{ display: "inline-flex", alignItems: "center", gap: 8,
      padding: "8px 14px", borderRadius: 999, background: "#FAF7F2", color: "#241430",
      border: "1px solid #24143022", fontFamily: "system-ui, sans-serif", fontSize: 14 }}>
      <span aria-hidden="true" className={state === "saving" ? "as-spin" : undefined} style={{ color: accent, width: 16, height: 16, display: "inline-flex",
        alignItems: "center", justifyContent: "center" }}>
        <svg viewBox="0 0 20 20" width="14" height="14">{icons[state] ?? icons.saved}</svg>
      </span>
      {!compact && (
        <span style={{ display: "flex", flexDirection: "column", lineHeight: 1.25 }}>
          <span style={{ fontWeight: 600 }}>{message}</span>
          <span style={{ fontSize: 12, color: "#24143099" }}>{timestamp}</span>
        </span>
      )}
      <style>{"@media (prefers-reduced-motion: no-preference) { .as-spin { animation: autosave-spin .2s linear infinite; } } @keyframes autosave-spin { to { transform: rotate(360deg); } }"}</style>
    </div>
  );
}