# Loader

**When to use:** A loading indicator for something in progress with no known end: a fetch, a save, a connection. To cover content while it reloads use [LoadingOverlay.md](LoadingOverlay.md), for a button use `Button`'s own `isLoading`, and for content not there yet a [Skeleton.md](Skeleton.md) in its shape reads better. For progress you can measure, use [ProgressBar.md](ProgressBar.md).

**Keywords:** busy, waiting, activity indicator, throbber, async, fetching, typing indicator, equalizer, radar, ripple

**Import:** `import { Loader, LoaderType, ThemeColor } from '@ahrowe/ui'`  
**Types:** `import type { LoaderProps, LoaderLabels } from '@ahrowe/ui'`

**Enums:** `LoaderType.Spinner` (default) | `LoaderType.Comet` | `LoaderType.Dots` | `LoaderType.Bars` | `LoaderType.Pulse`

- `Spinner`: an arc that grows and shrinks on a faint track. Reads at any size, down to 16px
- `Comet`: a glowing head trailing a tapering tail round a ring. For a hero spot: a page, a panel, an overlay
- `Dots`: three dots in a wave. For a line of text or a reply being written
- `Bars`: bars rising and falling like an equaliser. For media, uploads and data being processed
- `Pulse`: rings rippling out from a core. For waiting on something outside the app: a device, a connection, a search

```tsx
import { Loader, LoaderType, ThemeColor } from '@ahrowe/ui';

// Default: a Spinner in --primary-color, 2.5em square
{isLoading && <Loader />}

<Loader type={LoaderType.Comet} size='4em' />
<Loader type={LoaderType.Pulse} color={ThemeColor.Success} />
<Loader type={LoaderType.Bars} color='#e879f9' size={24} />

// In a line of text: Neutral takes the text's own colour, 1em its size. Dots rest on the
// baseline like an ellipsis; the round types sit centred on the text like an icon
<p>Ava is typing <Loader type={LoaderType.Dots} size='1em' color={ThemeColor.Neutral} /></p>
```

**Sizing:** `size` sets width and height, which are always equal. It is `2.5em` by default, so a
loader follows the font size of what it sits in; pass any CSS length, or a number of pixels. Size it
with `size` rather than a `width` in a class: the stylesheet reads `size`, and a class would tie with
it on specificity.

**Colour:** `color` takes a `ThemeColor` (default `Primary`) or any CSS colour, see
[ThemeColor.md](ThemeColor.md). `ThemeColor.Neutral` takes the colour of the text around it, which
is what a loader on a coloured surface wants.

## One type for the whole app

Every component with a loading state draws it with this `Loader`: `Button` (and so `SplitButton`,
`ActionButtons` and `ConfirmModal`), `Checkbox`, `VirtualList`, `Dropdown`, `KanbanBoard` and
`LoadingOverlay`. A default set once reaches all of them:

```tsx
<AhroweProvider defaultProps={{ Loader: { type: LoaderType.Dots } }}>
  <App />
</AhroweProvider>
```

An app-wide `type` and `labels` reach all of them. `color` reaches all but Button's, which keeps
the button's text colour. `size` reaches only `LoadingOverlay`'s default loader and the ones you
place yourself; the others size theirs to fit.

**Accessibility:** the root is a `role="progressbar"` with no value, which is how an indeterminate
progress indicator is announced, named "Loading" (`labels.loading`). Pass `aria-labelledby` to name
it after visible text instead, as `LoadingOverlay` does with its `message`. Inside something whose
name comes from its content (a custom checkbox) or that may only hold its own items (a list), pass
`aria-hidden` and mark the container `aria-busy` instead, as `Checkbox` and `VirtualList` do.

**Note:** a loader keeps moving under `prefers-reduced-motion`. One that stands still says the app
has hung, which is the opposite of what it is there to say.

**Key props:**

| Prop | Type | Description |
|------|------|-------------|
| `type` | `LoaderType` | The animation (default `Spinner`) |
| `size` | `number \| string` | Width and height, any CSS length or a number of pixels (default `'2.5em'`) |
| `color` | `ColorValue` | A `ThemeColor` or any CSS colour (default `ThemeColor.Primary`) |
| `labels` | `LoaderLabels` | Text the loader produces itself, see below |
| `className` / `style` | | Root `span` |

**Labels:** `loading`, the accessible name. Pass `labels` to override it, on the component or app-wide through `ConfigProvider`; keys merge individually. Type: `LoaderLabels`.

## Migration from `SpinnerLoading`

`SpinnerLoading` was removed in favour of `Loader`, whose default type is the same kind of spinner.

```tsx
// Before
<SpinnerLoading className="my-spinner" />   // .my-spinner { width: 24px; height: 24px }

// After
<Loader size={24} />
```

It used to be 70px by default and is `2.5em` now (40px at a 16px base), and it is a `span` rather
than an `svg`. It is announced as a progressbar named "Loading", where `SpinnerLoading` was not
announced at all.

**Slots:** none. The root `span` takes `className` and `style`.
