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.
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.
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.
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.
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.synclimits: 120 writes/minute, 1,800/hour, 8 KB per item, 100 KB total. - Firefox:
storage.synchas similar quotas when Firefox Sync is enabled; without it, data is stored locally. - Safari:
storage.syncmay not sync across devices; the save model is unaffected.
Verification
- Toggle a setting and close the tab immediately; reopen and confirm it persisted.
- Type quickly in a debounced field and confirm writes are batched.
- Enter an invalid regex and confirm it is not saved and the previous value still applies.
- 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.
Related
- Validating and resetting options forms — validation patterns.
- Syncing options form state with chrome.storage — reading and writing.
- Building an options page with React — the same models in React.
- Options page layouts — the parent topic.