# Loading

Show that content or an action is on its way, matched to how long it takes and what you know about the result.

## Overview

Loading happens at three scales: a region fetching data, an action waiting for a response, and a transaction waiting for a block. Each one needs feedback that keeps the layout still, blocks duplicate requests, and ends in a result or an error.

## Components

| Component                                    | Use while                                                             |
| -------------------------------------------- | --------------------------------------------------------------------- |
| `Skeleton`                                   | Content with a known shape loads, such as a balance or a metric tile. |
| `Rows.RowGroupLoader` and `Rows.PanelLoader` | A run of rows, or an expanded row's panel, loads.                     |
| `Spinner`                                    | A region or control is busy for an unknown time.                      |
| `Progress`                                   | A task with a measurable amount of work runs, such as an export.      |
| `LoadingDots`                                | A short wait happens inside a line of text.                           |
| `Button` with `loading`                      | The action you pressed is in flight.                                  |
| `Tag` reading Pending                        | A submitted transaction waits to be included in a block.              |

## Structure

1. Keep the page frame, headings, and navigation on screen. Only the parts still loading change.
2. Put a placeholder in each loading region, shaped like the content when you know its shape.
3. Replace each placeholder with its content as it arrives, without moving the content around it.
4. If a request fails, replace the placeholder with Couldn't load and a retry action.

```tsx
import { Amount, Button, Rows, Skeleton, style, Surface } from '@tempoxyz/ds'
import { Refresh } from '@tempoxyz/ds/icons'
import { useEffect, useState } from 'react'

export const fill = true
const tokens = [
  ['USDC', '1000.00'],
  ['PathUSD', '250.00'],
] as const

export function Demo() {
  const [loading, setLoading] = useState(true)
  const [refreshing, setRefreshing] = useState(false)

  // The first load shows skeletons. A refresh keeps the old values on screen.
  useEffect(() => {
    if (!loading) return
    const timer = setTimeout(() => setLoading(false), 1500)
    return () => clearTimeout(timer)
  }, [loading])

  useEffect(() => {
    if (!refreshing) return
    const timer = setTimeout(() => setRefreshing(false), 1000)
    return () => clearTimeout(timer)
  }, [refreshing])

  return (
    <div {...styles.page()}>
      <Surface {...styles.card()}>
        <Surface.Header>
          <Surface.Title>Balance</Surface.Title>
          <Button
            aria-label="Refresh balance"
            disabled={loading}
            loading={refreshing}
            onClick={() => setRefreshing(true)}
            shape="icon"
            size="small"
            variant="secondary"
          >
            <Refresh />
          </Button>
        </Surface.Header>
        <div {...styles.total()}>
          <Skeleton show={loading}>
            <Amount currency="USD" size="lg" value="1250.00" />
          </Skeleton>
        </div>
        <div {...styles.rows()}>
          <Rows.Root dividers="full">
            {loading ? (
              <Rows.RowGroupLoader aria-label="Loading tokens" />
            ) : (
              <Rows.Group>
                {tokens.map(([symbol, value]) => (
                  <Rows.Row key={symbol} spacing="none">
                    <Rows.Label>{symbol}</Rows.Label>
                    <Amount symbol={symbol} value={value} />
                  </Rows.Row>
                ))}
              </Rows.Group>
            )}
          </Rows.Root>
        </div>
      </Surface>
      <Button onClick={() => setLoading(true)} size="small" variant="text">
        Replay first load
      </Button>
    </div>
  )
}

namespace styles {
  export const card = style({
    boxSizing: 'border-box',
    maxWidth: '24rem !custom',
    width: '100% !custom',
  })

  export const page = style({
    backgroundColor: 'background.primary',
    display: 'grid',
    gap: '3',
    justifyItems: 'center',
    padding: '4',
  })

  export const rows = style({ marginBlockStart: '4' })

  // A flex row lets the skeleton take the width of the amount instead of the card.
  export const total = style({ display: 'flex' })
}
```

[View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Patterns/BalanceSkeleton.tsx)

This reference layout uses `Skeleton` for the total and `Rows.RowGroupLoader` for the token rows. Press the refresh button to see a refresh that keeps the old values on screen.

## Guidelines

### Match the Indicator to the Wait

| Expected wait   | Show                                                                                                            |
| --------------- | --------------------------------------------------------------------------------------------------------------- |
| Under 1 second  | Nothing new for regions. Delay a `Skeleton` or `Spinner` by about 300ms, so fast responses never flash.         |
| 1 to 10 seconds | A `Skeleton` for content with a known shape, or a `Spinner` for a region of unknown shape.                      |
| Over 10 seconds | A `Progress` bar with a label and a count. Let you leave the page, and confirm with a `Toast` when it finishes. |

Set `loading` on a pressed button right away, whatever the wait. It acknowledges the press and blocks a second request.

Once an indicator appears, keep it for at least half a second, so it doesn't flicker when the response lands just after the delay.

### Choose Skeleton, Spinner, or Progress

These choices match [Choosing a Component](/docs/components):

* Use `Skeleton` when you know the shape of the content, such as a balance, a list of transactions, or a perf.tempo.xyz metric tile.
* Use `Spinner` when the shape is unknown, such as search results that could be one row or fifty. Name it with `aria-label`.
* Use `Progress` when you can count the work, such as months exported or files uploaded.
* Use `LoadingDots` inside a sentence, such as Checking address followed by the dots.

**Do:** Shape skeletons like the content, so the layout holds still when it arrives.

**Don't:** Put a lone spinner in a region whose layout you already know.

Wrap the real content in `Skeleton` and set `show={false}` when it arrives, instead of swapping elements. The skeleton then takes the content's exact size.

### Keep Content On Screen While It Refreshes

Show skeletons on the first load only. When you refresh content that is already on screen, keep it visible and show progress on the control that started the refresh.

**Do:** Keep the current balance visible and set `loading` on the refresh button.

**Don't:** Replace a balance you already show with a skeleton every time it refreshes.

On views that poll for new data, such as a live benchmark dashboard, update values in place without any indicator.

### Update Optimistically Only When You Can Undo It

Apply a change before the server confirms it only when the change is low-risk, easy to reverse, and almost always succeeds. Renaming an API key, flipping a notification `Switch`, and archiving a contact qualify.

* If the request fails, restore the previous value and say so in a `Toast`, such as Couldn't rename key.
* Never update balances, payments, swaps, or any onchain state optimistically. Show the transaction as Pending instead.

### Show Pending Transactions

```tsx
import { Amount, Button, Rows, style, Surface, Tag, Toast } from '@tempoxyz/ds'
import { CheckCircle, Pending } from '@tempoxyz/ds/icons'
import { useRef, useState } from 'react'

export const fill = true
type Payment = { id: number; status: 'confirmed' | 'pending' }

function Status(props: { status: Payment['status'] }) {
  if (props.status === 'pending')
    return (
      <Tag intent="warning">
        <Pending />
        Pending
      </Tag>
    )

  return (
    <Tag intent="positive">
      <CheckCircle />
      Confirmed
    </Tag>
  )
}

function Activity() {
  const toast = Toast.useToast()
  const next = useRef(1)
  const [payments, setPayments] = useState<readonly Payment[]>([])
  const [submitting, setSubmitting] = useState(false)

  const confirmed = payments.filter((payment) => payment.status === 'confirmed').length
  const balance = (1250 - confirmed * 25).toFixed(2)

  return (
    <Surface {...styles.card()}>
      <Surface.Header>
        <Surface.Title>Activity</Surface.Title>
        <Button
          loading={submitting}
          onClick={() => {
            const id = next.current++
            setSubmitting(true)

            // The button stays busy only until the network accepts the transaction.
            setTimeout(() => {
              setSubmitting(false)
              setPayments((current) => [{ id, status: 'pending' }, ...current])
            }, 800)

            setTimeout(() => {
              setPayments((current) =>
                current.map((payment) =>
                  payment.id === id ? { ...payment, status: 'confirmed' as const } : payment,
                ),
              )
              toast.add({ intent: 'positive', title: 'Payment confirmed' })
            }, 3000)
          }}
          size="small"
        >
          Send 25 USDC
        </Button>
      </Surface.Header>
      <p {...styles.caption()}>Balance</p>
      <Amount currency="USD" size="md" value={balance} />
      <div {...styles.rows()}>
        <Rows.Root dividers="full">
          {payments.map((payment) => (
            <Rows.Row key={payment.id} spacing="none">
              <Rows.Info>
                <Rows.Title>Sent 25.00 USDC</Rows.Title>
                <Rows.Description>To 0x71C7…976F</Rows.Description>
              </Rows.Info>
              <Status status={payment.status} />
            </Rows.Row>
          ))}
          <Rows.Row spacing="none">
            <Rows.Info>
              <Rows.Title>Received 120.00 USDC</Rows.Title>
              <Rows.Description>From 0x3C44…93BC</Rows.Description>
            </Rows.Info>
            <Status status="confirmed" />
          </Rows.Row>
        </Rows.Root>
      </div>
    </Surface>
  )
}

export function Demo() {
  return (
    <div {...styles.page()}>
      <Toast.Provider>
        <Activity />
        <Toast.Viewport />
      </Toast.Provider>
    </div>
  )
}

namespace styles {
  export const caption = style({ color: 'gray.900', margin: '0', typography: 'label.13' })

  export const card = style({
    boxSizing: 'border-box',
    maxWidth: '26rem !custom',
    width: '100% !custom',
  })

  export const page = style({
    backgroundColor: 'background.primary',
    display: 'grid',
    justifyItems: 'center',
    padding: '4',
  })

  export const rows = style({ marginBlockStart: '4' })
}
```

[View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Patterns/PendingPayment.tsx)

Press Send 25 USDC to watch the payment move from Pending to Confirmed. The balance changes only when it confirms.

* Set `loading` on the send button only until the network accepts the transaction. Then release the button and add the transaction to activity as Pending.
* Don't block the screen with a dialog while you wait for a block. You can keep working while the transaction confirms.
* Subtract the amount from the balance only once the transaction is Confirmed.
* Tempo finalizes blocks in under a second, so most payments leave Pending quickly. Design for the slow case anyway, since a wallet or RPC provider can delay the result, and keep the Pending row linked to the transaction's details.

**Do:** Keep the balance as it is, and show the payment as Pending.

**Don't:** Subtract a payment from the balance before it confirms.

### Count Long Tasks

A task that takes longer than about ten seconds needs a measure of progress. Name it with `Progress.Label`, and pass `getAriaValueText` for a value with units, such as 3 of 5 months.

**Do:** Show how much of a long task is done.

**Don't:** Show a spinner for a long task you can measure.

Use the indeterminate bar, `value={null}`, only when the remaining work is unknown.

## States

| State    | What you show                                                                                                             |
| -------- | ------------------------------------------------------------------------------------------------------------------------- |
| Default  | The loaded content, with no indicator.                                                                                    |
| Loading  | `Skeleton` or `Spinner` in each loading region, and `loading` on any button whose action is in flight.                    |
| Error    | Couldn't load in place of the placeholder, with Try again. See [Feedback and Status](/docs/patterns/feedback-and-status). |
| Empty    | An `Empty` state once loading finishes with no content. Never show it while loading.                                      |
| Disabled | Controls that depend on the loading data stay disabled until it arrives, such as Send before balances load.               |
| Success  | The content replaces the placeholder in place. Confirm finished actions with a `Toast`.                                   |

## Accessibility

* `Skeleton` sets `aria-busy` while `show` is true and hides its children with `visibility: hidden`, so placeholder text isn't read.
* `Spinner` renders a `progressbar` named Loading. Pass `aria-label` to say what is loading, such as Loading transactions.
* `LoadingDots`, `Rows.RowGroupLoader`, and `Rows.PanelLoader` render `role="status"` named Loading. Override the name with `aria-label`.
* `Button` with `loading` sets `aria-busy`. Announce the result separately, for example with a `Toast`.
* `Progress.Label` names the bar, and `aria-valuetext` defaults to the percentage.
* The skeleton sweep, spinner rotation, and pulsing dots and bars all stop under `prefers-reduced-motion: reduce`.
* Don't move focus to an indicator, and don't move focus when content arrives.

## Related

* [Skeleton](/docs/components/skeleton), [Spinner](/docs/components/spinner), and [Progress](/docs/components/progress)
* [Button](/docs/components/button) for the `loading` state
* [Feedback and Status](/docs/patterns/feedback-and-status) for Pending and Confirmed
* [Empty States](/docs/patterns/empty-states) for what follows an empty result
* [Payments](/docs/patterns/payments) for the send and review flow
