Draft Restore

Autosaves a form locally and offers the draft back on reopen, keeping any value the server changed since, so work isn't lost or stale data restored.

StopsLost form after accidental refresh

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/draft-restore.json

The files land in your project. They're yours to edit.

Usage

draft-restore-usage.tsx
import { DraftRestore, type FormValues, type StoredDraft } from "@/components/ui/draft-restore"

<DraftRestore
  key={opened}
  storageKey={KEY}
  values={values}
  serverValues={server}
  labels={labels}
  onRestore={setValues}
  defaultExpanded
/>

What it catches

  • Form work lost to a refresh, crash or closed tab
  • Stale draft values that would overwrite newer server values, kept server-side unless chosen
  • Old drafts, via an expiry (ttl)

Props

DraftRestore props
PropTypeDefault
storageKey* (required)

localStorage key, unique per record, e.g. `draft:invoice:1042`.

string—
values* (required)

The form's current values. Saved (debounced) whenever they differ from `serverValues`.

FormValues—
serverValues* (required)

The record as the server has it now.

FormValues—
onRestore* (required)

Receives the server values with your draft merged in. Conflicting fields keep the server value unless ticked.

(values: FormValues) => void—
onDiscard

Called after the draft is thrown away.

() => void—
labels

Readable field names for the preview.

Record<string, string>{}
exclude

Fields never written to storage (passwords, card numbers, one-time codes).

string[]NONE
delay

Debounce before saving, in ms.

number800
ttl

Drafts older than this (ms) are deleted instead of offered. Defaults to 7 days.

number7 * 86_400_000
message

Templates: `{when}` ("5 minutes ago") and `{count}` are filled in.

string"Restore your draft from {when}?"
conflictMessage

Message when the server changed fields since the draft.

string"Your draft from {when} clashes with {count} on the server."
expiresText

Text saying when the draft expires.

string"Expires in {days}"
previewLabel

Text for the preview toggle.

string"What differs"
restoreLabel

Restore button text.

string"Restore"
discardLabel

Discard button text.

string"Discard"
useMineLabel

Label for keeping the draft's value over the server's.

string"Use my draft"
conflictHint

Shown under a conflicting field.

string"Changed on the server after your draft. Kept unless you …
defaultExpanded

Show the field preview expanded from the start.

booleanfalse

* required. Also accepts the native props of <div> (className, aria-*, data-*…), passed through to it.

Accessibility

  • A role="region" labelled "Unsaved draft"
  • Message is aria-live="polite"
  • Preview toggle uses aria-expanded and aria-controls

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

  • Drafts live in this browser's localStorage: not on other devices, and cleared with site data
  • Don't store sensitive fields; use `exclude` for passwords, card numbers and similar
  • Anyone with access to the device can read localStorage

What it stops

Lost form after accidental refresh

Also known as

draft · autosave · localstorage · restore · form persistence · resume application · refresh lost data

Dependencies

Pairs well with