# Feedback and Status

Use one name, component, and icon for each system state across every Tempo product.

## Overview

Balances, activity, API keys, and benchmark tiles share a few states: a value that can't be shown, a failed request, and a transaction waiting for or included in a block. When each state looks the same everywhere, you recognize it at a glance.

This page defines that vocabulary and when each feedback component carries it. [Loading](/docs/patterns/loading) and [Empty States](/docs/patterns/empty-states) cover the states before content arrives.

## Components

| Component                  | Carries                                                                                      |
| -------------------------- | -------------------------------------------------------------------------------------------- |
| `Tag`                      | The status of one record, such as a transaction or a webhook delivery.                       |
| `Table.Cell` with `status` | The same status inside a table column.                                                       |
| `Callout`                  | A message about one section, such as Couldn't load older activity.                           |
| `Banner`                   | A message about the whole product, such as degraded network performance.                     |
| `Toast`                    | Confirmation that an action you started finished, or failed.                                 |
| `Field.Error`              | A problem with one value. See [Errors and Validation](/docs/patterns/errors-and-validation). |

## State Vocabulary

| State         | Meaning                                                    | Component                                                   | Text                                | Icon                   |
| ------------- | ---------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------- | ---------------------- |
| Unavailable   | The value doesn't exist right now, such as a missing price | Plain text in `gray.900` in place of the value              | Unavailable                         | None                   |
| Couldn't load | A request for the region's content failed                  | `Callout intent="negative"` or `Empty`, with a retry action | Couldn't load activity              | `AlertCircle`          |
| Pending       | Submitted and waiting to be included in a block            | `Tag intent="warning"`                                      | Pending                             | `Pending`              |
| Confirmed     | Included in a block                                        | `Tag intent="positive"`                                     | Confirmed                           | `CheckCircle`          |
| Failed        | Rejected or reverted by the network                        | `Tag intent="negative"`, with the reason nearby             | Failed                              | `AlertCircle`          |
| Amount hidden | You chose to hide amounts on this device                   | A fixed mask in place of `Amount`                           | `••••`, read aloud as Amount hidden | `EyeOff` on the toggle |

In a `Table`, set `status` on `Table.Cell` to `warning`, `positive`, or `negative` for Pending, Confirmed, and Failed. The cell colors its text, so keep the same word.

```tsx
import { Callout, Rows, style, Surface, Tag } from '@tempoxyz/ds'
import { AlertCircle, CheckCircle, Pending } from '@tempoxyz/ds/icons'

export const fill = true
function Balances() {
  return (
    <Surface>
      <Surface.Header>
        <Surface.Title>Balances</Surface.Title>
      </Surface.Header>
      <Rows.Root>
        <Rows.Row>
          <Rows.Info>
            <Rows.Title>USDC</Rows.Title>
            <Rows.Description>1,000.00 USDC</Rows.Description>
          </Rows.Info>
          $1,000.00
        </Rows.Row>
        <Rows.Row>
          <Rows.Info>
            <Rows.Title>EURC</Rows.Title>
            <Rows.Description>500.00 EURC</Rows.Description>
          </Rows.Info>
          <span {...styles.muted()}>Unavailable</span>
        </Rows.Row>
      </Rows.Root>
    </Surface>
  )
}

function Activity() {
  return (
    <Surface>
      <Surface.Header>
        <Surface.Title>Activity</Surface.Title>
      </Surface.Header>
      <Rows.Root>
        <Rows.Row>
          <Rows.Info>
            <Rows.Title>Sent 25.00 USDC</Rows.Title>
            <Rows.Description>To 0x71C7…976F</Rows.Description>
          </Rows.Info>
          <Tag intent="warning">
            <Pending />
            Pending
          </Tag>
        </Rows.Row>
        <Rows.Row>
          <Rows.Info>
            <Rows.Title>Received 120.00 USDC</Rows.Title>
            <Rows.Description>From 0x3C44…93BC</Rows.Description>
          </Rows.Info>
          <Tag intent="positive">
            <CheckCircle />
            Confirmed
          </Tag>
        </Rows.Row>
        <Rows.Row>
          <Rows.Info>
            <Rows.Title>Sent 40.00 USDC</Rows.Title>
            <Rows.Description>Not enough USDC left to cover the network fee</Rows.Description>
          </Rows.Info>
          <Tag intent="negative">
            <AlertCircle />
            Failed
          </Tag>
        </Rows.Row>
      </Rows.Root>
      <Callout intent="negative" size="small" {...styles.callout()}>
        <Callout.Icon />
        <Callout.Content>Couldn't load older activity.</Callout.Content>
        <Callout.Action>Try again</Callout.Action>
      </Callout>
    </Surface>
  )
}

export function Demo() {
  return (
    <div {...styles.grid()}>
      <Balances />
      <Activity />
    </div>
  )
}

namespace styles {
  export const callout = style({ marginBlockStart: '4' })

  export const grid = style({
    alignItems: 'start',
    backgroundColor: 'background.primary',
    display: 'grid',
    gap: '4',
    gridTemplateColumns: 'repeat(auto-fill, minmax(18rem, 1fr)) !custom',
    padding: '4',
  })

  export const muted = style({ color: 'gray.900', typography: 'copy.14' })
}
```

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

This reference layout shows five of the states in context, built from `Surface`, `Rows`, `Tag`, and `Callout`.

### Unavailable

* Show Unavailable in place of a value that has no source, such as the USD value of a token without a price feed, or a benchmark metric a run didn't record.
* Keep the rest of the row. The token balance is still known, only its USD value isn't.
* Explain why in nearby text, or in a `Popover` behind an info button, when the reason helps you act, such as a feature that isn't enabled on testnet.

### Couldn't Load

* Use Couldn't load followed by the content, such as Couldn't load activity, when a request fails.
* Offer Try again. Use a `Callout` with a `Callout.Action` when part of a region failed, and `Empty` with a button when the whole region did.
* Keep any content that did load, and never show a request failure as empty. See [Empty States](/docs/patterns/empty-states).

### Pending

* Show Pending from the moment a transaction is submitted until it is included in a block.
* Keep a pending transaction in activity with its amount and recipient, so you can see it's on its way.
* Don't change a balance until the transaction confirms. See [Loading](/docs/patterns/loading).

### Confirmed

* Show Confirmed once the transaction is included in a block.
* Use Confirmed for every kind of transaction, including payments, deposits, and swaps.
* Announce the change with a `Toast` only for a transaction you started in this session.

### Failed

* Show Failed when the network rejects or reverts a transaction, and keep the row in activity.
* State the reason in plain words beside the tag, such as Not enough USDC left to cover the network fee.
* Say whether funds moved. A failed transfer moves no funds, but its network fee may still be charged.

### Amount Hidden

* Replace every amount with the same four-dot mask while amounts are hidden, including totals, rows, and charts.
* Toggle the state with an icon `Button` named Hide amounts that sets `aria-pressed`, with `Eye` and `EyeOff` icons.
* Remember the choice on the device, and keep amounts hidden until you show them again.

```tsx
import { Amount, Button, Rows, style, Surface, Tooltip, variants } from '@tempoxyz/ds'
import { Eye, EyeOff } from '@tempoxyz/ds/icons'
import { useState } from 'react'

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

// A fixed mask hides how many digits the amount has.
function Hidden(props: { large?: boolean | undefined }) {
  return (
    <span {...styles.mask({ large: props.large ?? false })}>
      <span aria-hidden="true">••••</span>
      <span {...styles.label()}>Amount hidden</span>
    </span>
  )
}

export function Demo() {
  const [hidden, setHidden] = useState(false)

  return (
    <div {...styles.page()}>
      <Surface {...styles.card()}>
        <Surface.Header>
          <Surface.Title>Balance</Surface.Title>
          <Tooltip.Root>
            {/* A toggle keeps one name and reports its state through aria-pressed. */}
            <Tooltip.Trigger
              aria-label="Hide amounts"
              aria-pressed={hidden}
              onClick={() => setHidden(!hidden)}
              render={<Button shape="icon" size="small" variant="secondary" />}
            >
              {hidden ? <EyeOff /> : <Eye />}
            </Tooltip.Trigger>
            <Tooltip.Popup>Hide amounts</Tooltip.Popup>
          </Tooltip.Root>
        </Surface.Header>
        {hidden ? <Hidden large /> : <Amount currency="USD" size="lg" value="1250.00" />}
        <div {...styles.rows()}>
          <Rows.Root dividers="full">
            {tokens.map(([symbol, value]) => (
              <Rows.Row key={symbol} spacing="none">
                <Rows.Label>{symbol}</Rows.Label>
                {hidden ? <Hidden /> : <Amount symbol={symbol} value={value} />}
              </Rows.Row>
            ))}
          </Rows.Root>
        </div>
      </Surface>
    </div>
  )
}

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

  // Visually hidden, so screen readers announce the state instead of the dots.
  export const label = style({
    clipPath: 'inset(50%) !custom',
    height: '1px !custom',
    overflow: 'hidden',
    position: 'absolute',
    whiteSpace: 'nowrap',
    width: '1px !custom',
  })

  export const mask = variants({
    base: { color: 'foreground', display: 'block', position: 'relative' },
    variants: {
      large: { false: { typography: 'copy.14.strong' }, true: { typography: 'heading.32' } },
    },
  })

  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/HiddenBalance.tsx)

## Guidelines

### Choose a Channel by Scope

| Scope                                 | Use                          | Example                                     |
| ------------------------------------- | ---------------------------- | ------------------------------------------- |
| One value in a form                   | `Field.Error`                | Enter 1,250.00 USDC or less.                |
| One record                            | `Tag` or `Table.Cell` status | Pending                                     |
| One section                           | `Callout`                    | Couldn't load older activity. Try again     |
| The whole product or page             | `Banner`                     | Testnet faucets are paused for maintenance. |
| An action you just finished           | `Toast`                      | Payment sent                                |
| A decision before an action continues | `AlertDialog`                | Revoke this API key?                        |

Don't put information you need to act on in a `Toast`, since it dismisses itself after five seconds by default.

### Pair Color With Text and an Icon

Each state has a word and an icon as well as a color, so it reads the same in grayscale, in high contrast, and on a screen reader.

**Do:** Show the state's word, with its icon before the text.

**Don't:** Use colored dots or tints alone to show a state.

### Use the Same Word Everywhere

Use the words in the vocabulary table for every product and every transaction type. Don't swap in synonyms such as Success, Settled, Complete, Processing, or Error.

**Do:** Use Pending and Confirmed for every kind of transaction.

**Don't:** Describe the same state with different words and colors.

### Never Show a Stand-In as a Real Value

A zero, a dash, or a stale number reads as a real measurement. Show Unavailable when there is no value, and Couldn't load when the request for it failed.

**Do:** Show Unavailable when a value has no source.

**Don't:** Show $0.00 for a price you don't know.

### Hide Amounts Without Hinting at Them

A mask that keeps the digit count or the decimal point still tells an onlooker the size of a balance. Use the same fixed mask for every amount, the way `SecretValue` always shows 16 dots.

**Do:** Use one fixed-length mask for every hidden amount.

**Don't:** Mask each digit, which reveals how large the amount is.

## States

| State    | Feedback you show                                                                                         |
| -------- | --------------------------------------------------------------------------------------------------------- |
| Default  | No message. Records show their status `Tag`, and values show as formatted `Amount` text.                  |
| Loading  | A `Skeleton` or `Spinner` named after the content. See [Loading](/docs/patterns/loading).                 |
| Error    | Couldn't load for a failed request, Failed for a rejected transaction, and `Field.Error` for a bad value. |
| Empty    | An `Empty` state that says why there's nothing to show. See [Empty States](/docs/patterns/empty-states).  |
| Disabled | Unavailable for a value or feature that can't be used right now, with the reason when it helps.           |
| Success  | Confirmed on the record, and a positive `Toast` for an action you just finished.                          |

## Accessibility

* `Tag` renders a `span` with no role, so its text is the state. Icons from `@tempoxyz/ds/icons` are hidden from assistive technology unless you name them.
* A `Tag` that changes from Pending to Confirmed isn't announced. Announce the change with a `Toast` when you started the transaction.
* `Toast.Viewport` is a live `region` that F6 focuses. Pass `priority: 'high'` only for urgent toasts, which are announced assertively.
* `Callout` renders `role="alert"` for `negative` and `warning`, and `role="status"` for `info` and `positive`.
* `Banner` has no live region. Add `role="status"` or `role="alert"` only when it appears after the page loads.
* A hidden amount needs text such as Amount hidden for screen readers, in place of the dots.

## Related

* [Tag](/docs/components/tag) for record status
* [Callout](/docs/components/callout) and [Banner](/docs/components/banner) for section and product messages
* [Toast](/docs/components/toast) for confirmations
* [SecretValue](/docs/components/secret-value) for masked secrets
* [Loading](/docs/patterns/loading) for Pending transactions
* [Blockchain Values](/docs/patterns/blockchain-values) for amounts and addresses
