Persisting form drafts with HTML5 localStorage: limits and safer defaults
Use HTML5 localStorage to save form drafts across reloads, but respect its 5MB limit, synchronous access, and lack of encryption.
15 Aug 2025, 14:30 UTC

The problem: a form that forgets
You spend ten minutes filling out a multi-field request form, click an external link, and lose every entry when the page reloads. Users expect drafts to survive navigation, but rebuilding that behavior from scratch can feel like overkill for a simple website.
The useful takeaway: HTML5 localStorage is a reasonable first choice for small, non-sensitive form drafts. It stores key-value text per origin, survives page reloads, and requires no server round-trip. It is not a database, so you should respect its size limit, avoid sensitive data, and handle storage errors explicitly.
How localStorage works
localStorage is part of the Web Storage API. Each origin—scheme, host, and port combined—gets its own key-value store. Data persists until your code or the user clears it, and every tab from the same origin shares the same storage. Reads and writes are synchronous, meaning they run on the main thread and can block rendering if the payload is large.
The practical limit is usually around five megabytes per origin. Exceeding it throws a QuotaExceededError. Because values are stored as strings, objects must be serialized with JSON.stringify and parsed with JSON.parse.
Worked example: autosave a contact form
Add this markup to your page. The script saves the form contents on every input event and restores them when the page loads.
<form id='draft-form'>
<label>Name <input name='name'></label>
<label>Email <input name='email' type='email'></label>
<label>Message <textarea name='message'></textarea></label>
<button type='submit'>Send</button>
</form>
<script>
const FORM = document.getElementById('draft-form');
const KEY = 'contact-form-draft-v1';
function saveDraft() {
const data = {};
for (const field of FORM.elements) {
if (field.name) data[field.name] = field.value;
}
try {
localStorage.setItem(KEY, JSON.stringify(data));
} catch (err) {
if (err.name === 'QuotaExceededError') {
console.warn('Draft too large for localStorage');
}
}
}
function restoreDraft() {
const raw = localStorage.getItem(KEY);
if (!raw) return;
const data = JSON.parse(raw);
for (const [name, value] of Object.entries(data)) {
const field = FORM.elements[name];
if (field) field.value = value;
}
}
FORM.addEventListener('input', saveDraft);
FORM.addEventListener('submit', () => localStorage.removeItem(KEY));
restoreDraft();
</script>
Run this in the page where the form lives, not in a standalone console snippet, because the script references page elements. No special permissions are required beyond normal same-origin access. Replace contact-form-draft-v1 with a key specific to your form so unrelated drafts do not collide.
To verify, open DevTools, go to the Application tab, expand Local Storage, and select your origin. You should see the key appear after typing in the form. After submission, the key should disappear.
Risk: never store passwords, credit-card numbers, or health data in localStorage. The data is unencrypted and accessible to any script running on the same origin.
Trade-offs and when to switch
localStorage is convenient, but it has engineering limits:
- Main-thread blocking. Synchronous reads and writes can delay UI updates if you store large blobs.
- Size cap. Around five megabytes per origin. Larger drafts trigger
QuotaExceededError. - No encryption. Data is plain text and visible to extensions and same-origin scripts.
- String-only values. Every object requires serialization.
| Storage option | Persistence | Scope | Typical limit | Blocking? |
|---|---|---|---|---|
| localStorage | Until cleared | Same origin, all tabs | ~5 MB | Yes (synchronous) |
| sessionStorage | Tab session | Same origin, one tab | ~5 MB | Yes (synchronous) |
| IndexedDB | Until cleared | Same origin | Much larger, quota-aware | No (asynchronous) |
Use sessionStorage if the draft should vanish when the tab closes. Move to IndexedDB if you need to store large documents, rich text, or structured records without blocking the main thread.
Closing checklist
- Give each form a unique storage key.
- Wrap
setItemintry/catchand check forQuotaExceededError. - Clear the key after successful submission so old drafts do not linger.
- Verify in DevTools under Application > Local Storage that keys appear and disappear as expected.
- Keep sensitive fields out of client-side storage entirely.
For small drafts on a static site, localStorage is a pragmatic, well-supported option. Treat it as a temporary cache, not a durable database, and your users will stop losing their work.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.