---
id: "packages/active-ui/app-loading-bar"
title: "AppLoadingBar"
description: "The thin loading line of an application shell, shown while any part of the page reports that it is waiting."
url: "/packages/active-ui/app-loading-bar"
locale: "en"
revision: "3f3c902fd9c32735"
---

# AppLoadingBar

`AppLoadingBar` draws one thin, indeterminate line on the top edge of the content column while
something on the page is loading. Pages do not draw it: they report a wait, and the shell draws the
line in one place. For a local area that is loading, use
[LoadingScreen](/packages/active-ui/loading-screen).

**Example: The loading line of the shell**

Client: Runs entirely in your browser. Source: `examples/ui/app-loading-bar.tsx`.

```tsx
// apps/docs/examples/ui/app-loading-bar.tsx
'use client';

import {useState} from 'react';

import {
  AppLoadingBar,
  AppLoadingProvider,
  useAppLoadingRegistration,
} from '@workspace/active-ui/components/composed/app-loading-bar';
import {Button} from '@workspace/active-ui/components/primitives/button';

/** A source of waiting: registered while `active`. */
function Wait({active}: {active: boolean}) {
  useAppLoadingRegistration(active);
  return null;
}

/**
 * The loading line of an application shell: the frame stands for the
 * content column, the line hangs on its top edge. Two sources wait for two
 * and four seconds; the line stays until both are done. In an application
 * a Suspense boundary reports with `AppLoadingSignal` as its fallback.
 */
export default function AppLoadingBarExample() {
  const [first, setFirst] = useState(false);
  const [second, setSecond] = useState(false);
  const start = () => {
    setFirst(true);
    setSecond(true);
    window.setTimeout(() => setFirst(false), 2000);
    window.setTimeout(() => setSecond(false), 4000);
  };
  return (
    <AppLoadingProvider>
      <div className="relative w-full max-w-xl overflow-hidden rounded-md border">
        <AppLoadingBar label="Loading" />
        <Wait active={first} />
        <Wait active={second} />
        <div className="grid gap-3 p-6">
          <Button
            variant="outline"
            className="justify-self-start"
            disabled={first || second}
            onClick={start}
          >
            Start two waits
          </Button>
          <p className="text-muted-foreground text-sm">
            Waiting: {[first, second].filter(Boolean).length}
          </p>
        </div>
      </div>
    </AppLoadingProvider>
  );
}
```

## The pieces

- `AppLoadingProvider`: holds the count of waits; the shell wraps it around the content column.
- `AppLoadingBar`: the line, placed once by the shell, absolutely positioned at the top of its
  nearest positioned ancestor, so nothing shifts when it appears.
- `AppLoadingSignal`: the `fallback` of a Suspense boundary; it draws nothing and reports a wait
  while it is mounted.
- `useAppLoadingRegistration(active)`: reports a wait from any other source while `active`, such
  as a link that is navigating.

The line stays while at least one source waits, and appears only after 200 ms. Outside a provider,
the signal and the hook do nothing.

## Usage

```tsx
import {
  AppLoadingBar,
  AppLoadingProvider,
  AppLoadingSignal,
} from '@workspace/active-ui/components/composed/app-loading-bar';

<AppLoadingProvider>
  <main className="relative">
    <AppLoadingBar label="Loading" />
    <Suspense fallback={<AppLoadingSignal />}>{children}</Suspense>
  </main>
</AppLoadingProvider>;
```

## What your application provides

- The placement: a positioned content column, and the provider around it.
- The sources of waiting, and the translated `label`.
- The line's colour follows `--grid-primary` where your theme defines it, else the primary colour.

## Accessibility

The line is an indeterminate `progressbar` named by `label`. It is not a live region: it appears on
every wait, and an announcement each time would be noise. It does not move focus.

## API

**AppLoadingProvider**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | yes | The content column, including the line and every source of waiting. |

**AppLoadingSignal**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

AppLoadingSignal has no props of its own.

**AppLoadingBar**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | `string` | yes | The translated name of the loading state, for example `t('common.loading')`.  Required: a bar without a name is a nameless `progressbar` to a screen reader. The text is not visible -- the line itself is the visible signal -- and comes from the caller, because `active-ui` does not translate. |
| `className` | `string` | no | Placement, only for the shell that mounts this component. A page does not touch this prop: it does not render the line at all. |

**useAppLoadingRegistration**: `(active: boolean) => void`

Reports to the shell that something is being waited for, as long as
`active` is true.

The underlying form of {@link AppLoadingSignal}, exported separately for
sources that are not a Suspense boundary. Those exist: a route change in the
App Router shows **no** fallback -- the router keeps the old page in place
until the new payload has arrived -- so without a second source the line
would never appear when clicking in the sidebar. Hook it to
`useLinkStatus()` of each navigation link as well.

Outside an {@link AppLoadingProvider} the hook does nothing and does not
throw: a boundary that accidentally sits outside the shell must not produce
an error.

**AppLoadingBarProps**

```ts
interface AppLoadingBarProps {
  label: string;
  className?: string;
}
```
