Skip to content

Patterns / Offline

Offline: what to hold back and what to refuse

An agent app is mostly a conversation with a server. When the connection goes, the honest screen says so, keeps what the person typed, and doesn't pretend that anything was sent.

Composer has two states for this, and they differ. { kind: "offline" } locks the box: it is disabled and shows its reason, which defaults to "You're offline. Your message will wait here." or your own text through reason. Use it only if your app really does keep the message, because the default sentence promises it and a locked box can't take new text. { kind: "held", reason } keeps typing open and says why sending waits, which suits an app that queues. Either way, the send button does nothing until you change the state back to ready.

Deciding that the app is offline is the hard part. navigator.onLine is a boolean, and the online and offline events fire on window when it changes, but MDN calls it inherently unreliable: true only means a network connection, not that your server can be reached, and it can lag after a connection drops. It advises showing hints from it and not disabling features. So treat it as a hint, and move the composer to offline or held when a request to your own server actually fails.

PageState draws a page that can't load. Its offline kind is for a service that can't be reached from here: it has its own icon and is announced as an error is. Give it a title that says the connection is the cause and one action to retry. Use error, with the request id, when your server answered with a failure. Don't use unavailable, which means the page doesn't exist or isn't the person's, and never reveals which.

The components

  • ComposerFreeThe `offline` state locks the box with a reason; `held` keeps typing open and says why sending waits.
  • Page stateFreeA page that couldn't load: `offline` when the service can't be reached, `error` with the request id, and one retry action (Pro).
  • LockedFreeA control that can't be used right now says why.
  • Approval requestProA decision that needs the live state of the change; refuse it offline (Pro).

Install

With the candor-ui CLI

Each component brings in what it depends on. These are free (MIT).

Good to know

  • Safe to queue: a message the person wrote, a draft, a note, a thumbs up or down. Say that it is waiting, show it as not yet sent, and keep it where a reload doesn't lose it. When it goes, show the real outcome and don't assume it succeeded.
  • Refuse, with the reason on the control: approving or denying an agent's action, anything destructive, and any step that depends on state which may have changed. A decision made on stale information is worse than a delayed one. An approval tied to a hash should be decided against the change the server holds now.
  • Left to the app: detecting connectivity, the queue and where it is stored, retries and their limits, how long a queued message stays valid, and merging if the same thing was changed elsewhere meanwhile. Candor offers no service worker or sync layer.
  • Read against MDN's navigator.onLine page on 9 November 2026.