Autosave status
StableA quiet line under an editor: “Unsaved changes”, “Saving…”, “Saved · 2 min ago”, and what happens next when a save can’t go through.
The note editor’s save indicator. It walks from Unsaved changes to Saving… to Saved and then stays, counting how long ago, so it never flashes and disappears. Failures are worded as what happens next: “Couldn’t save · Retrying”, “Offline · Saves when you reconnect”.
Updated View source
In an editor
import { useEffect, useRef, useState } from 'react'import { useNow } from '@rchursin/product/calendar/hooks'import { AutosaveStatus, type SaveState } from '@rchursin/product/notes'import { Field, Switch, Textarea } from '@rchursin/product/ui'
const DEBOUNCE_MS = 800 // save this long after the last keyconst ROUND_TRIP_MS = 450 // stands in for the network
/** * Type: “Unsaved changes”, then “Saving…” 800 ms after you stop, then “Saved · just now”, which * stays and keeps counting. Turn on the offline switch to see how a save waits for the connection. * Nothing leaves this page; the save is simulated. */export default function AutosaveEditor() { const now = useNow().getTime() const [text, setText] = useState('Split the cost report by model before Friday.\nFlag every run over a dollar.') const [state, setState] = useState<SaveState>('saved') const [savedAt, setSavedAt] = useState(() => Date.now()) const [offline, setOffline] = useState(false) const timer = useRef<number | undefined>(undefined)Usage
import { AutosaveStatus, type SaveState } from '@rchursin/product/notes'<AutosaveStatus state={autosave.state} savedAt={note.updatedAt} now={now} />Examples
Every state
The two that need attention are set heavier, not in a colour.
import { AutosaveStatus, type SaveState } from '@rchursin/product/notes'
const now = new Date(2026, 9, 21, 10, 30).getTime() // a fixed sample "now"
const rows: [state: SaveState, savedAt: number, when: string][] = [ ['dirty', now, 'While typing'], ['saving', now, '800 ms after the last key'], ['saved', now - 5_000, 'Stays, and keeps counting'], ['saved', now - 4 * 60_000, 'Minutes later'], ['error', now, 'Retries by itself'], ['offline', now, 'Saves on reconnect'],]
/** Every state. The two that need attention are set in a heavier weight, not a colour. */export default function AutosaveStates() { return ( <ul className="flex w-full max-w-md flex-col divide-y divide-line"> {rows.map(([state, savedAt, when], i) => ( <li key={i} className="flex items-center justify-between gap-4 py-2.5"> <AutosaveStatus state={state} savedAt={savedAt} now={now} /> <span className="text-right text-[11px] text-fg-3">{when}</span> </li> ))} </ul> )}API
AutosaveStatus props
| Prop | Type | Default | Description |
|---|---|---|---|
staterequired | SaveState | – | 'saved' | 'dirty' | 'saving' | 'error' | 'offline'. |
savedAtrequired | number | – | When it last saved, in epoch ms. Read as “just now” (under 45 seconds), “4 min ago” (under an hour), a time today, or a date. |
nowrequired | number | – | The current time. Re-render on an interval so “ago” keeps counting. |
className | string | – | Classes on the line. |
Accessibility
- Plain text, not a live region, on purpose: it changes at every pause in typing and would talk over the person writing.
- Each state has its own icon and words. Error and offline are set in a heavier weight and the full text colour, never a colour of their own.
- Saving uses the Spinner, which turns only when motion is allowed.
- Saved stays on screen and says when, so there is never a moment where it is unclear whether the work is safe.
Guidelines
Do
- Save on a short pause (the note editor waits 800 ms) and right away when the page is hidden.
- Word failures as what happens next.
- Keep now ticking, every 15 to 60 seconds.
Don’t
- Don’t flash “Saved” and then hide it.
- Don’t announce every save to screen readers.
- Don’t block typing while a save is in flight.
In use
Related
- Note editorA note’s title, a Markdown block body, tags, word count and an honest save status; it saves 800 ms after typing stops.
- SpinnerThe loader icon turning once every 900 ms, only when motion is allowed. Decorative next to words; give it a label when it stands alone.
- ToastShort past-tense results with at most one action, like “Run archived · Undo”. Read out politely; they wait while hovered or focused.
- useMediaQuery & useNowTwo small hooks from the calendar: a media query as a boolean, and the current time that re-renders on each whole minute or second.
Source
The component’s own files, exactly as they run on this site.
/** * Note editor: title, block body and a footer with tags, counts and the save status. * * Autosave: a save runs 800 ms after typing stops, and at once when the note closes or the page * is hidden. The status walks "Unsaved changes" → "Saving…" → "Saved · just now" and stays there * (the relative time keeps counting); it never flashes and disappears. It is plain text, not a * live region, so it doesn't talk over typing. */import { useCallback, useEffect, useId, useRef, useState, type Ref } from "react"import { cx } from "../../components/cx"import { Icon } from "../icons/Icon"import { formatTime, relativeDay } from "../lib/dates"import type { Note } from "../data/sample"import { useHydrated } from "../lib/hydration"import { IconButton, Spinner } from "../ui/button"import { Menu } from "../ui/menu"import { Chip } from "../ui/misc"import { PickerButton } from "../ui/picker"import { Popover, usePopover } from "../ui/popover"import { BlockEditor, growTextarea, type BlockEditorHandle } from "./BlockEditor"