Auto-Save vs Explicit Save in Options

Decide between auto-saving extension settings and a Save button: how each feels, chrome.storage.sync write limits, saved-state feedback, validation, unsaved-changes warnings, and a hybrid that suits most extensions.

Published October 2, 2026 Updated October 2, 2026 8 min read
Table of Contents

The user toggles “Highlight prices” on the options page, closes the tab, and is annoyed to discover the change did not stick because there was a Save button at the bottom they never scrolled to. Another extension auto-saves everything, and a user who was halfway through typing a regular expression into a filter field watches the extension apply the broken pattern to every page they visit. Neither model is right for every setting. Browser settings pages auto-save, which shapes user expectations, but some settings need a moment of composition before they take effect. This guide compares the models, deals with chrome.storage.sync limits, and lays out a hybrid. It belongs to options page layouts.

What each model promises

Auto-save promises that what you see is what is in effect: every change is persisted as soon as it is made. It matches chrome://settings and most modern apps, needs no Save button, and works well for independent toggles and choices. Its risks are transient invalid states (a half-typed URL pattern), bursts of writes that exceed storage rate limits, and no “cancel”. Explicit save promises that nothing changes until you confirm: edits are a draft, Save applies them together, and you can discard. It suits groups of related fields that must be valid together, but users forget to press Save, and it needs an unsaved-changes warning. A hybrid auto-saves simple controls and uses explicit apply for composed inputs.

Auto-save, explicit save and hybrid comparedThe three save models compared on matching browser conventions, handling invalid intermediate input, write volume, and risk of lost changes.ModelMatches browserInvalid draftsLost changes riskAuto-save everythingYesApplied mid-typingNoneExplicit Save buttonNoHeld as draftForgot to saveHybridMostlyValidated before applyLow (per-field apply)
Hybrid: instant for toggles, deliberate for composed inputs.

Step-by-step: a hybrid save model

1. Classify each setting

1settings-inventory.md
2Auto-save immediately:  enabled, theme, showBadge, highlightColor, language
3Debounced auto-save:    displayName (free text, always valid)
4Apply with validation:  blockedPatterns (regex list), apiEndpoint (URL), syncInterval (number range)
5Explicit group save:    account connection (endpoint + token tested together)

Execution context: a design inventory. Toggles, selects, radios and colour pickers produce a valid value on every change — auto-save them. Free text that is always valid can auto-save after a pause. Inputs that can be invalid while typing should validate before applying. Groups whose fields only make sense together get an explicit Save.

2. Auto-save simple controls with feedback

 1// options.js
 2for (const el of document.querySelectorAll("[data-autosave]")) {
 3  el.addEventListener("change", async () => {
 4    const value = el.type === "checkbox" ? el.checked : el.value;
 5    await chrome.storage.sync.set({ [el.name]: value });
 6    flashSaved(el);
 7  });
 8}
 9
10function flashSaved(el) {
11  const status = el.closest(".field").querySelector(".status");
12  status.textContent = chrome.i18n.getMessage("saved");        // "Saved"
13  clearTimeout(status._t);
14  status._t = setTimeout(() => (status.textContent = ""), 2000);
15}

Execution context: the options page. A brief “Saved” next to the changed control confirms auto-save without a modal or toast. Make the status element a polite live region (role="status") so screen reader users hear it.

Three save paths in one pageToggles write immediately on change; free text writes after a 500 ms pause; validated fields write on blur or Enter only when valid, otherwise show an error; an account group writes on Save after a connection test.Toggle / selectchange → set()Free textinput → debounce 500 msValidated fieldblur/Enter → validatestorage.sync"Saved" statuslive regionInline errorvalue not appliedGroup Savetest, then write
The path depends on whether the input can be invalid mid-edit.

3. Debounce free text

1function debounce(fn, ms) { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; }
2
3const saveName = debounce(async (value) => {
4  await chrome.storage.sync.set({ displayName: value.trim() });
5  flashSaved(nameInput);
6}, 500);
7nameInput.addEventListener("input", (e) => saveName(e.target.value));
8nameInput.addEventListener("blur", (e) => saveName.flush?.() ?? chrome.storage.sync.set({ displayName: e.target.value.trim() }));

Execution context: the options page. chrome.storage.sync allows 120 write operations per minute and 1,800 per hour; saving every keystroke in a few fields can exceed that and fail with a quota error. A 500 ms debounce cuts writes by an order of magnitude. Saving on blur ensures the last edit is kept if the user leaves the field quickly.

4. Validate before applying composed inputs

 1patternInput.addEventListener("change", async () => {
 2  const lines = patternInput.value.split("\n").map((l) => l.trim()).filter(Boolean);
 3  const bad = lines.find((l) => { try { new RegExp(l); return false; } catch { return true; } });
 4  const error = patternInput.closest(".field").querySelector(".error");
 5  if (bad) {
 6    error.textContent = chrome.i18n.getMessage("invalidPattern", [bad]);
 7    patternInput.setAttribute("aria-invalid", "true");
 8    return;                                                        // keep the last valid value in storage
 9  }
10  error.textContent = "";
11  patternInput.removeAttribute("aria-invalid");
12  await chrome.storage.sync.set({ blockedPatterns: lines });
13  flashSaved(patternInput);
14});

Execution context: the options page. Using change (fired on blur for text areas) rather than input means validation runs when the user finishes, not mid-typing. Invalid input is never written, so the extension keeps running with the last valid configuration. See validating and resetting options forms.

An explicit group saveThe user edits the API endpoint and token as a draft; the page warns if they try to leave; on Save the page tests the connection with the draft values, writes both to storage on success, and shows an error without saving on failure.UserOptions pageBackend / storageedit endpoint + token (draft)Savetest connection(draft)200 OKstorage.set({endpoint, token})
Groups that must be valid together get a draft and a Save.

5. Use explicit save for coupled groups

 1const form = document.querySelector("#account-form");
 2let dirty = false;
 3form.addEventListener("input", () => { dirty = true; saveBtn.disabled = false; });
 4form.addEventListener("submit", async (e) => {
 5  e.preventDefault();
 6  const { endpoint, token } = Object.fromEntries(new FormData(form));
 7  const ok = await testConnection(endpoint, token);
 8  if (!ok) return showFormError(chrome.i18n.getMessage("connectionFailed"));
 9  await chrome.storage.local.set({ endpoint, token });           // token stays local, never synced
10  dirty = false; saveBtn.disabled = true; flashSaved(saveBtn);
11});
12addEventListener("beforeunload", (e) => { if (dirty) e.preventDefault(); });

Execution context: the options page. The Save button is enabled only when there are changes. beforeunload warns before closing the tab with unsaved edits. Secrets such as tokens belong in storage.local, not sync.

6. Make the model visible

Explain the model once at the top of the page — “Changes are saved automatically” — and give the explicit section its own heading and Save button so it is obviously different. Mixed models without signposting confuse users more than either model alone.

7. Offer undo for risky auto-saves

For auto-saved changes with large effects (disabling the extension, clearing a list), show an inline “Undo” for a few seconds after saving, restoring the previous value from the onChanged oldValue. This gives auto-save a safety net without a confirmation dialog.

8. Handle conflicts from other devices

 1chrome.storage.onChanged.addListener((changes, area) => {
 2  if (area !== "sync") return;
 3  for (const [key, { newValue }] of Object.entries(changes)) {
 4    const el = form.elements.namedItem(key);
 5    if (!el) continue;
 6    if (document.activeElement === el && el.dataset.dirty) {
 7      showConflict(el, newValue);           // "Changed on another device — use theirs / keep yours"
 8    } else {
 9      el.type === "checkbox" ? (el.checked = newValue) : (el.value = newValue ?? "");
10    }
11  }
12});

Execution context: the options page. With storage.sync, a change made on another device can arrive while the user is editing the same field here. Overwriting the field under the user’s cursor is jarring; ignoring the change leaves the page out of date. Updating untouched fields silently and asking only for the field being edited resolves both. Mark a field data-dirty on input and clear it after a successful save.

Common mistakes

  • Save button for simple toggles. Users close the tab without saving.
  • Auto-saving invalid drafts. Half-typed patterns take effect.
  • Saving every keystroke to sync. Hits MAX_WRITE_OPERATIONS_PER_MINUTE.
  • No saved feedback. Users don’t trust auto-save.
  • Tokens in storage.sync. Synced secrets; keep them local.

Cross-browser variation

  • Chrome / Edge: storage.sync limits: 120 writes/minute, 1,800/hour, 8 KB per item, 100 KB total.
  • Firefox: storage.sync has similar quotas when Firefox Sync is enabled; without it, data is stored locally.
  • Safari: storage.sync may not sync across devices; the save model is unaffected.

Verification

  1. Toggle a setting and close the tab immediately; reopen and confirm it persisted.
  2. Type quickly in a debounced field and confirm writes are batched.
  3. Enter an invalid regex and confirm it is not saved and the previous value still applies.
  4. Edit the account group and try to close the tab — confirm the warning.

FAQ

Which model do users expect?

Browser settings auto-save, so most users expect it for simple controls.

Should I show a toast for every auto-save?

No. A brief inline status by the control is enough and less distracting.

Can I have a Cancel for auto-save?

Use per-change Undo instead; a global Cancel contradicts auto-save.

What if a write fails because of the quota?

storage.sync.set rejects with a quota error. Catch it, show the field as not saved, and retry after a short delay; persistent failures usually mean too many writes or an item over 8 KB, which should move to storage.local.

Other UI/UX Patterns & Interactive Components Resources