All writing
Engineering6 min read

Offline is a UI state, not an error

A practical field guide to designing honest offline interfaces: save locally, explain what is waiting, and make recovery predictable.

The network is not a button the person can press. It is a condition the interface has to account for.

When a connection drops, the useful question is not “How do we show an error?” It is “What can the person still do, and how will they know what happened?” That shift turns offline support from a red banner into a product promise: keep work safe, make the state legible, and resume without asking someone to start over.

A field guide to the small decisions that make an unreliable connection feel manageable.

Start with a truthful promise

An offline interface should distinguish three events that are easy to blur together:

  • Saved here: the device has a durable copy.

  • Waiting to sync: the server has not confirmed the change.

  • Synced: the server has accepted it.

“Saved” should mean the first thing, not the third. If the product cannot make that promise, say what it can do instead. Familiar labels are only useful when they describe a state the system can prove.

Name the receipt

Show “Saved on this device” as soon as the local write completes. Change it to “Synced” only after the server confirms the update.

The distinction can be expressed as a small reliability budget: the interface becomes trustworthy when a local save is fast and its explanation is clear.

Treassurance=Tlocal save+Tclear signalT_{\text{reassurance}} = T_{\text{local save}} + T_{\text{clear signal}}

These equations are models, not guarantees. They make two separate failure points visible: keeping a copy and delivering that copy later. Let ww be a successful local write, qq be eventual delivery conditional on that write, and ss be the chance the edit survives both steps:

s=w⋅qs = w \cdot q
A useful boundary

A queued update is not a confirmed update. Keep those states distinct in the interface and in the data model.

When the network returns

Let the product retry in the background, then confirm the result in place. Avoid interrupting someone who has already moved on.

Keep the work close at hand

A trustworthy offline flow needs a durable local record and a clear way to replay it. The exact storage mechanism depends on the product, but the interaction should survive a refresh and explain which changes are still waiting.

The illustration below is a contact sheet of character studies. The same principle applies to empty states: use imagery to make a quiet screen feel considered, while letting the words explain what the person can do next.

A contact sheet of illustrated characters with distinct expressions and poses.
Character studies for states that should feel human, not alarming.

The image is decorative context; the saved and pending labels carry the operational meaning.

Do not promise what storage cannot keep

Browser storage can be cleared or evicted. If losing the local copy would be serious, make that limitation visible and offer an export or recovery path.

Make the queue explain itself

An offline queue is easier to reason about when a person can see what is waiting and what will happen next. For a small sync client, the record might be as plain as:

type PendingChange = { id: string; savedAt: number; attempts: number; };
const MyComponent = () => {
  return <div>Hello Universe</div>;
};

For comparison, the same record serialized for inspection looks like:

type PendingChange = {
  id: string;
  savedAt: number;
  attempts: number;
};

The tabs let a reader compare the type and the persisted value without repeating the surrounding explanation. Keep the interaction optional: a person should still understand the idea without opening every variant.

Treat conflicts as data, not surprises

If two devices can edit the same record, a retry alone cannot decide which version should win. Preserve enough history to explain the conflict and let the person choose when automatic merging is unsafe.

Give recovery a sequence

Retries should be bounded, observable, and safe to repeat. A simple recovery path gives each step one job:

  1. Persist the change

    Write the update locally before showing that it is saved.

  2. Wait without losing context

    Keep the pending item visible while the connection is unavailable.

  3. Retry with a limit

    Use backoff and an idempotency key so repeating a request does not create a duplicate.

  4. Confirm or explain

    Mark the update as synced after confirmation; otherwise show a specific next action.

Let the person inspect the state

The same update can be explained in different ways depending on what someone needs to know first:

The change is stored locally. It is safe on this device, but it has not reached the server yet.

These descriptions should match real states rather than optimistic guesses. The web platform's online and offline events can help update the display, but a network signal alone does not prove that your server is reachable.

Make uncertainty readable

Good offline UX does not pretend the connection is reliable. It makes uncertainty small enough to act on: the person can tell what is saved, what is waiting, and what needs attention. That is a more useful promise than “works offline” on its own.

When a change is safe locally but still pending, say so plainly. When it syncs, close the loop. When it cannot, keep the work and show a way forward. A calm interface is not one that hides failure; it is one that leaves no mystery about what happens next.