# 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.

Web page: https://candor.themethodic.com/patterns/offline

## Components

- **Composer** (`composer`, free; https://candor.themethodic.com/docs/composer.md): The `offline` state locks the box with a reason; `held` keeps typing open and says why sending waits.
- **Page state** (`page-state`, free; https://candor.themethodic.com/docs/page-state.md): A page that couldn't load: `offline` when the service can't be reached, `error` with the request id, and one retry action (Pro).
- **Locked** (`locked`, free; https://candor.themethodic.com/docs/locked.md): A control that can't be used right now says why.
- **Approval request** (`approval-request`, Pro; https://candor.themethodic.com/docs/approval-request.md): A decision that needs the live state of the change; refuse it offline (Pro).

## Install

```bash
npx candor-ui add composer page-state locked
```

## Notes

- 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.
