# Progress

> A progress bar for something that has an end. CSS component with one class and no JavaScript.

The class goes on a real `<progress>`. The element has the semantics built in: the browser reports it as `role="progressbar"` and announces how far it has got, so you write neither a role, `aria-valuenow` nor `aria-valuemax`. If you build the bar from `<div>` elements, you have to write all of that yourself, and keep it in step with the number.

If you do not know how far it has got, [Spinner](/components/spinner.md) is the better fit.

## How to use it

**Plain HTML**

```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.40.0/src/tokens/tokens.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.40.0/src/components/css/progress/progress.css">
<!-- Or everything in one file: https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.40.0/dist/lotek.css -->
```

```html
<progress class="lt-progress" value="27" max="30"
        aria-label="Subscription CM-2079-0090" aria-valuetext="Your walls expire in 3 days."></progress>


<progress class="lt-progress" value="100" max="100" data-color="success"
        aria-label="Renewal paid"></progress>


<progress class="lt-progress" aria-label="Checking your walls"></progress>
```

**TypeScript**

```ts
import "@lo-tek/interfaces/tokens.css"
import "@lo-tek/interfaces/progress.css"
```

`lt` is used in two ways, and which one depends on whether you create the element or find it.

**If you create the markup,** you spread the attributes in. That is the route in React, Astro and anything else that renders:

```tsx
import { lt } from "@lo-tek/interfaces/react"


<progress {...lt.progress({ value: 27, max: 30, label: "Subscription CM-2079-0090", valueText: "Your walls expire in 3 days." })} />
```

`valueText` becomes `aria-valuetext`, and the screen reader reads it instead of the percentage. `max` is rarely announced, so “90 percent” is all the user gets without it; “Your walls expire in 3 days.” says how many days are left.

The import goes to `/react` here, since the block is JSX. From there the keys are called `className` and `htmlFor`. In Astro and anything else without React you import `@lo-tek/interfaces` and get `class` and `for`.

**If you update the number while something is in progress,** you set the property on the element. That is the browser’s own API, and it is faster than rebuilding the attributes for every step:

```ts
const bar = document.querySelector<HTMLProgressElement>("#update")


if (bar) bar.value = 64
```

To make the bar indeterminate again, remove the attribute: `bar.removeAttribute("value")`. If you use `lt.setAttributes(bar, lt.progress({ … }))` instead, you pass all the options again: `lt.progress()` manages `value`, `max`, `aria-label` and `aria-valuetext`, so a call without `label` removes the name, and `max` is set to 100 when you do not give it.

See [Type-safe use](/type-safe-use.md).

**kotlinx.html**

```kotlin
// Once for the whole app, in the page template: every stylesheet, from the WebJar.
head { link(rel = "stylesheet", href = LotekWebJar.CSS) }
```

On a Kotlin or Java server, the same with kotlinx.html and `lo-tek-kotlinx-html`. The builder functions are in `Lt`, with an enum for each list of values:

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


fun FlowContent.subscription() {
    progress { lt(Lt.progress(value = 27, max = 30, label = "Subscription CM-2079-0090", valueText = "Your walls expire in 3 days.")) }
    progress { lt(Lt.progress(value = 100, max = 100, color = ProgressColor.SUCCESS, label = "Renewal paid")) }
    progress { lt(Lt.progress(label = "Checking your walls")) }
}
```

## `max` defaults to 100

The builder function always writes out `max`, contrary to the rule that a default value gives no attribute. That rule is about appearance, where the CSS already has the answer. `max` is semantics, and HTML’s own default is `1`:

```html
<progress value="40"></progress>
```

That bar is full, not 40 percent, and nothing tells you. If you write the markup by hand, give both.

## Indeterminate progress

If you leave out `value`, the bar becomes indeterminate, and a stripe sweeps across it. It says “something is in progress” without promising a number:

```html
<progress class="lt-progress" aria-label="Checking your walls"></progress>
```

That is the same thing [Spinner](/components/spinner.md) says. Choose one of them in a given place, otherwise the page reports the same wait twice.

## The colours mean something

**Plain HTML**

```html
<progress class="lt-progress" value="72" max="100"
        aria-label="Installing the ICE update"></progress>


<progress class="lt-progress" value="100" max="100" data-color="success"
        aria-label="Renewal paid"></progress>


<progress class="lt-progress" value="88" max="100" data-color="warning"
        aria-label="The intrusion log is almost full"></progress>


<progress class="lt-progress" value="18" max="100" data-color="danger"
        aria-label="The ICE update stopped"></progress>
```

| `data-color` | Used for |
| - | - |
| *(omitted)* | Something is in progress, and it is going as it should |
| `success` | It is finished |
| `warning` | It is approaching a limit |
| `danger` | It stopped, or went wrong |

The colour alone says nothing. Someone who cannot tell green from red needs the text next to it, and someone listening to the page only gets the `aria-label`. So write what happened, and let the colour confirm it.

## Classes you can style

| Class | What it is |
| - | - |
| `.lt-progress` | The bar. The colour comes from `data-color`. |

| Variable | Default |
| - | - |
| `--lt-progress-fill` | `var(--lt-color-accent-fill)`, and `data-color` changes it |
| `--lt-progress-track` | `var(--lt-color-neutral-surface)` |
| `--lt-progress-border` | `var(--lt-color-neutral-border)`, the border around the track. Set it to `transparent` to remove it |
| `--lt-progress-height` | `var(--lt-spacing-2)` |
| `--lt-progress-radius` | `var(--lt-spacing-1)` |

The fill colour is carried by `color` on the bar itself, and the filled part reads `currentColor`. The reason is that the filled part has two names, `::-webkit-progress-value` in Chromium and WebKit and `::-moz-progress-bar` in Gecko, and they cannot be in the same selector list: a pseudo-element the engine does not know makes the whole rule invalid. The colour therefore has to be decided in one place and inherited. `<progress>` has no text, so `color` is free for the job, and `--lt-progress-fill` is all you need to set to change it.

If the user has asked for reduced motion, the indeterminate stripe moves more slowly, and the filled part jumps to the new number instead of sliding there. The stripe does not stop completely: an indicator that stands still no longer says that something is in progress.

An example. A bar under a field should often be thinner and without corners:

**Plain HTML**

```html
<progress class="lt-progress" value="60" max="100"
        aria-label="Scanning the perimeter"></progress>


<progress class="lt-progress thin-bar" value="60" max="100"
        aria-label="Installing the ICE update"></progress>
```

```css
.thin-bar {
--lt-progress-height: var(--lt-spacing-1);
--lt-progress-radius: 0;
}
```

See [Customising](/customising.md) for how the variables and the layer work.

## Accessibility

- Give the bar a name with `aria-label`, or tie it to a visible text with `aria-labelledby`. Without a name the screen reader reports a number without saying what it is about.
- Always give `max` together with `value`. HTML’s default is `1`, so a bar without `max` is full at anything above 1.
- Do not update the number more often than the user needs. Every change can become an announcement, and a bar that counts from 0 to 100 in steps of one becomes a hundred messages.
- When the progress is over, say so with more than the colour. Move focus to where the result appeared, or report it in a `role="status"` element.
