# Connection Status

> Tells the user when the connection to the server is gone. Standalone component.

A page that loses its connection looks completely normal, but is frozen. If the user fills in a form over a dead connection, everything is gone when she presses send.

```ts
// main.ts, runs once when the app starts
import { defineLtConnectionStatus } from "@lo-tek/interfaces/connection-status"
import "@lo-tek/interfaces/tokens.css"
import "@lo-tek/interfaces/connection-status.css"


defineLtConnectionStatus()
```

## How to use it

**Plain HTML**

```html
<!-- The stylesheets the page needs. Without a build step it is `<link>`, not
     `import`: a browser does not resolve a package name. -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.43.0/src/tokens/tokens.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.43.0/src/components/standalone/connection-status/connection-status.css">
<!-- Or everything in one file: https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.43.0/dist/lotek.css -->
```

```html
<lt-connection-status class="lt-connection-status" role="status" aria-live="polite" data-ignore-morph></lt-connection-status>


<!-- Once for the whole app, in the page template. Not once per component. -->
<script type="module">
  import { defineLtConnectionStatus } from
    "https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.43.0/dist/components/standalone/connection-status/lt-connection-status.js"


  defineLtConnectionStatus()
</script>


<script type="module">
  import { reportFailure, reportSuccess } from
    "https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.43.0/dist/components/standalone/connection-status/lt-connection-status.js"


  const connection = document.querySelector("lt-connection-status")
  if (!connection) throw new Error("element not found")


  // The page's own saves go through this function, so every answer from the
  // server, or the lack of one, is reported. A module keeps its names to
  // itself, so it is put on window, where the page's other scripts call it
  // with what they save.
  // A request that got no answer is a lost connection, and so is an answer
  // from a gateway whose server is gone (502, 503, 504). Any other answer, an
  // error status included, means the server is there. reportSuccess and
  // reportFailure wait for the component to be registered, and warn when it
  // never is; the save does not wait for them.
  window.saveRoute = async (body) => {
    let response
    try {
      response = await fetch("/api/routes", { method: "POST", body })
    } catch {}
    if (response && ![502, 503, 504].includes(response.status)) reportSuccess(connection)
    else reportFailure(connection)
    return response
  }
</script>
```

**React**

```ts
// src/lotek.d.ts, gives <lt-connection-status> types in JSX
import "@lo-tek/interfaces/react-jsx"
```

```tsx
import "@lo-tek/interfaces/connection-status.css"
import { lt } from "@lo-tek/interfaces/react"
import {
  defineLtConnectionStatus,
  reportFailure,
  reportSuccess,
} from "@lo-tek/interfaces/connection-status"
import "@lo-tek/interfaces/tokens.css"


defineLtConnectionStatus()


export function Connection() {
  return <lt-connection-status {...lt.connectionStatus()} />
}


export async function saveRoute(body: BodyInit) {
  const connection = document.querySelector("lt-connection-status")
  if (!connection) throw new Error("element not found")
  let response: Response | undefined
  try {
    response = await fetch("/api/routes", { method: "POST", body })
  } catch {}
  // No answer, or a gateway whose server is gone, is a lost connection.
  // The save does not wait for the report.
  if (response && ![502, 503, 504].includes(response.status)) reportSuccess(connection)
  else reportFailure(connection)
  return response
}
```

`reportFailure(element)` and `reportSuccess(element)` wait until registration has run before they call the method. A call that comes before `defineLtConnectionStatus()`, such as the first message from an event stream, is then carried out as soon as the element is ready, instead of failing.

**Datastar**

```html
<!-- The stylesheets the page needs. Without a build step it is `<link>`, not
     `import`: a browser does not resolve a package name. -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.43.0/src/tokens/tokens.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.43.0/src/components/standalone/connection-status/connection-status.css">
<!-- Or everything in one file: https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.43.0/dist/lotek.css -->


<!-- Once for the whole app, in the page template. Not once per component. -->
<script type="module">
  import { defineLtConnectionStatus } from
    "https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.43.0/dist/components/standalone/connection-status/lt-connection-status.js"


  defineLtConnectionStatus()
</script>


<!-- Datastar itself, after the registration: module scripts run in the
     order they stand, so no request goes out before the web component
     exists. Without it the `data-` attributes below do nothing. -->
<script type="module"
        src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar.js"></script>


<!-- Datastar sends `datastar-fetch` on the document for every request.
     `retrying`: the request got no answer. `error`: an answer of 400 or
     more, with its status; 502, 503 and 504 come from a gateway whose
     server is gone, any other is a server that is there. A patch arrived,
     so the stream is alive; `finished` is no good as a sign, since it also
     comes after an error. An answer without content, like 204, or with
     `text/javascript`, says neither, and the next patch clears the bar. -->
<lt-connection-status class="lt-connection-status" role="status" aria-live="polite" data-ignore-morph
                      data-on:datastar-fetch__document="evt.detail.type === 'retrying' || [502, 503, 504].includes(+evt.detail.argsRaw.status) ? el.reportFailure() : (evt.detail.type === 'error' || evt.detail.type.startsWith('datastar-patch')) && el.reportSuccess()"></lt-connection-status>
```

Datastar keeps a long-lived stream open to the server. If it drops, the page looks right but stops updating. The browser only reports when the machine loses the network, not when the server stops answering, so `data-on` tells the web component about every request that went wrong, through `reportFailure()` and `reportSuccess()`. The page has no script of its own, and the web component knows nothing about Datastar.

**kotlinx.html**

On a Kotlin or Java server, with kotlinx.html and `lo-tek-kotlinx-html`, and the Datastar recipe. `Lt.connectionStatus()` gives the bar its role and `data-ignore-morph`, and the bar shows by itself when the browser loses the network. That the server stopped answering, only whoever sends the requests can tell, and here that is Datastar: `data-on` passes it on to the web component, as in the Datastar tab. The page has no script of its own.

```kotlin
import kotlinx.html.*
import no.lotek.*
import no.lotek.html.lt
import no.lotek.html.ltConnectionStatus


fun FlowContent.connection() {
    ltConnectionStatus {
        lt(Lt.connectionStatus())
        // `retrying`: the request got no answer. `error`: an answer of 400 or
        // more; 502, 503 and 504 come from a gateway whose server is gone,
        // any other is a server that is there. A patch: the stream is alive.
        attributes["data-on:datastar-fetch__document"] =
            "evt.detail.type === 'retrying' || [502, 503, 504].includes(+evt.detail.argsRaw.status) ? el.reportFailure() : (evt.detail.type === 'error' || evt.detail.type.startsWith('datastar-patch')) && el.reportSuccess()"
    }
}
```

```kotlin
// Once for the whole app, in the page template. Not once per component.
head { link(rel = "stylesheet", href = LotekWebJar.CSS) }
body {
    connection()
    script(type = "module") { unsafe { +"import { defineLt } from \"${LotekWebJar.REGISTER}\"; defineLt()" } }
    // Datastar itself, once for the whole app, after the registration:
    // module scripts run in the order they stand, so no request goes out
    // before the web components exist.
    script(type = "module", src = "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar.js") {}
}
```

## Why it creates the content itself

The server by definition cannot tell you that it is unavailable. If it is down, nothing comes from it to render. So the component creates the content itself, and the builder function sets `data-ignore-morph` so that a later patch does not tear the bar away the moment the connection is back. See [Markup and behaviour](/markup-and-behaviour.md).

## `navigator.onLine` is not enough

The browser only knows whether the machine has a network, not whether the server answers. An office network with no route to the internet, a server that is down, or a stream that has dropped all count as “online”.

So the app reports it itself:

```ts
const connection = document.querySelector("lt-connection-status")
if (!connection) throw new Error("element not found")


let response
try {
  response = await fetch("/api/routes", { method: "POST", body })
} catch {}
if (response && ![502, 503, 504].includes(response.status)) connection.reportSuccess()
else connection.reportFailure()
```

| Method | When you call it |
| - | - |
| `reportFailure()` | A request got no answer, or a gateway answered 502, 503 or 504 because the server behind it is gone |
| `reportSuccess()` | The server answered, an error status included: a 400 or a 500 comes from a server that is there |

`reportFailure(element)` and `reportSuccess(element)` from `@lo-tek/interfaces/connection-status` do the same, but first wait until the element is registered.

The component also listens to the browser’s `offline` and `online`, so a network that disappears is caught without you doing anything.

## Attributes and events

| Attribute | Default |
| - | - |
| `offline-text` | “No connection. What you type is not being saved.” |
| `online-text` | “Connection restored.” |

| Event | When |
| - | - |
| `connection-lost` | The bar appears. Once per outage, however many `offline` events the browser sends |
| `connection-restored` | The connection is back |

## Classes you can style

| Class | What it is |
| - | - |
| `.lt-connection-status` | The host element. It is `display: contents` and takes up no space |
| `.lt-connection-status__bar` | The bar itself. Has `data-state="offline"` or `data-state="online"` |

| Variable | Fallback |
| - | - |
| `--lt-connection-status-layer` | `1000` |
| `--lt-connection-status-padding` | `var(--lt-spacing-2) var(--lt-spacing-4)` |

```css
.lt-connection-status__bar {
  --lt-connection-status-layer: 9999;
}
```

## Accessibility

What a screen reader is given when the recipe above is used, step by step. It is checked through AT-SPI, the interface Orca reads, in Chromium and Firefox, on every change, by `check-accessibility-tree.ts`, and every change the check lists as a mutation must make it fail.

1. Call `document.querySelector("lt-connection-status").reportFailure()`: "No connection. What you type is not being saved." announced once, polite.
2. Call `document.querySelector("lt-connection-status").reportSuccess()`: "Connection restored." announced once, polite.

The component itself is `role="status"` with `aria-live="polite"`, not `alert`, from the moment it is connected, so the region exists before the bar and its text arrive; a role or politeness you write stays. The message is important, but should not interrupt in the middle of a word. It stays until the connection is back, so someone who reads slowly has time to read it.

While a modal dialog is open, the bar is inert and sits under the backdrop, like everything else outside the dialog. If the connection drops then, the user learns about it only when the dialog closes.

Colour alone does not carry the message: the text says what is wrong. In Windows high contrast mode, custom colours and backgrounds are removed, so the bar gets a border in `CanvasText`; otherwise it would be invisible.
