# Messages and feedback

Which component tells the user what, from field errors to toasts, banners and confirmations.

Messages guide the user, confirm what happened and help them recover. They also interrupt, so
every message has to earn its place.

## Principles

1. **First, try to remove the need for the message.** A disabled button with a tooltip, a sensible
   default or a clearer label often does more than an error after the fact.
2. **Interrupt only when the user must decide.** Everything else is shown without stopping their
   work.
3. **Help the user recognize, understand and fix the problem.** Say what happened and what to do,
   next to where it happened.
4. **Never rely on color alone.** Pair status colors with text, and with an icon where space
   allows.

## Choosing the component

| Situation                                                                            | Component                                                                                                             | Interrupts |
| ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | ---------- |
| A field value is wrong or missing                                                    | `FieldError` under the field, `aria-invalid` on the control ([Field](/components/field))                              | No         |
| An action finished                                                                   | `toast.success()` ([Toaster](/components/sonner))                                                                     | No         |
| An action failed and the user can carry on                                           | `toast.error()` with what to do                                                                                       | No         |
| The page cannot work as expected (partial data, delayed sync, a missing integration) | [Alert](/components/alert) or [Info Banner](/patterns/info-banner), inline at the top of the affected area            | No         |
| Context the user should know before acting                                           | [Info Banner](/patterns/info-banner) (`role="note"`)                                                                  | No         |
| An action is destructive or cannot be undone                                         | `confirm()` ([Confirm Dialog](/patterns/confirm-dialog))                                                              | Yes        |
| Leaving would lose unsaved changes                                                   | `confirm()` with Discard as the action                                                                                | Yes        |
| A list or card has nothing to show                                                   | [List Empty State](/patterns/list-empty-state)                                                                        | No         |
| A list or card failed to load                                                        | [List Error State](/patterns/list-error-state)                                                                        | No         |
| Something is loading                                                                 | [Skeleton](/components/skeleton) when the layout is known, [Spinner](/components/spinner) in a button or a small area | No         |
| Several items were processed                                                         | See [Several items](#several-items)                                                                                   | Depends    |

## Field validation

Validate when the user leaves a field and again on submit, not on every keystroke. Put the error
under the field it belongs to, mark the control with `aria-invalid`, and move focus to the first
invalid field on submit. The message says how to fix the value: "Enter a domain, such as
acme.com.", not "Invalid input."

```tsx
import { Field, FieldError, FieldLabel } from "@zenoxai/ui/components/field"
import { Input } from "@zenoxai/ui/components/input"

function FieldInvalid() {
  return (
    <Field data-invalid className="max-w-sm">
      <FieldLabel htmlFor="monitored-domain">Monitored domain</FieldLabel>
      <Input
        id="monitored-domain"
        defaultValue="https://acme"
        aria-invalid
        aria-describedby="monitored-domain-error"
      />
      <FieldError id="monitored-domain-error">
        Enter a domain without the protocol, such as acme.com.
      </FieldError>
    </Field>
  )
}
```

## Toasts

A toast confirms the result of something the user just did, and goes away on its own.

- One toast per action. A bulk action raises one toast with the count, not one per item.
- Success toasts are short past-tense statements: "Rule created".
- Use the toast type that matches the result: `toast.success()`, `toast.info()`,
  `toast.warning()`, `toast.error()`. Each takes the colors and icon of its type; a plain
  `toast()` is neutral.
- Do not put the only copy of an important error in a toast: it disappears. If the user must act
  on it, show it inline or in a dialog.
- Offer at most one action in a toast, such as Undo or View, and give that toast at least 10
  seconds (`duration: 10000`) so the user can read it and reach the button. Toasts pause while
  the pointer is over them.

```tsx
import { toast } from "sonner"
import { Button } from "@zenoxai/ui/components/button"

function ToasterTypes() {
  return (
    <>
      <Button variant="outline" onClick={() => toast("Filter saved")}>
        Default
      </Button>
      <Button
        variant="outline"
        onClick={() => toast.success("Alert assigned to Marina Santos")}
      >
        Success
      </Button>
      <Button
        variant="outline"
        onClick={() => toast.info("Scan queued behind 2 other scans")}
      >
        Info
      </Button>
      <Button
        variant="outline"
        onClick={() => toast.warning("API key expires in 3 days")}
      >
        Warning
      </Button>
      <Button
        variant="outline"
        onClick={() => toast.error("Could not reach the SIEM connector")}
      >
        Error
      </Button>
    </>
  )
}
```

## Long-running work

Scans, exports and imports can take minutes. Never hold the user in a modal or a page-wide
spinner for them.

1. Start the work and confirm it at once: `toast("Export started")`.
2. Show the progress where the result will appear: a "Running" status on the row, a
   [Progress](/components/progress) bar in the card.
3. When it finishes, say so with an action that leads to the result: "Export ready" with
   Download, "Scan finished" with View results.
4. When it fails, `toast.error()` with what to do, and keep the failed state visible where the
   result would have been, so the user finds it after the toast is gone.

## Inline messages

Use an [Alert](/components/alert) for a message with a title and details, and an
[Info Banner](/patterns/info-banner) for a single line. Place it at the top of the area it
describes, not at the top of the page when only one card is affected. An inline message stays
until the condition behind it is gone; do not make the user dismiss information that is still
true.

```tsx
import { InfoBanner } from "@zenoxai/ui/patterns/info-banner"

function InfoBannerTones() {
  return (
    <div className="grid w-full max-w-md gap-3">
      <InfoBanner>Results include 2 of 3 tenants you can access.</InfoBanner>
      <InfoBanner tone="success">
        All connectors reported in the last hour.
      </InfoBanner>
      <InfoBanner tone="warning">
        The CrowdStrike sync is delayed. Data may be 20 minutes old.
      </InfoBanner>
      <InfoBanner tone="neutral">
        This report is a snapshot from October 1 and does not update.
      </InfoBanner>
      <InfoBanner tone="destructive">
        Exports are disabled until the storage quota is raised.
      </InfoBanner>
    </div>
  )
}
```

## Confirmations

Ask for confirmation only before an action that destroys data, cannot be undone or affects other
people. Asking before routine actions teaches users to confirm without reading.

- The title asks the question and names the object: "Delete asset vpn-01?"
- The body says what else changes and whether it can be undone.
- The confirm button repeats the verb ("Delete asset") with the `destructive` variant; the other
  button is Cancel.
- For the most destructive actions on a named resource, such as deleting a tenant, ask the user
  to type its name.

```tsx
import * as React from "react"
import { Button } from "@zenoxai/ui/components/button"
import { confirm } from "@zenoxai/ui/patterns/confirm-dialog"

function ConfirmDialogDestructive() {
  const [result, setResult] = React.useState<boolean | null>(null)

  async function deleteReport() {
    const confirmed = await confirm({
      title: "Delete this report?",
      description:
        "Monthly exposure report, September 2026 will be deleted for everyone in the tenant. This cannot be undone.",
      confirmLabel: "Delete report",
      confirmVariant: "destructive",
    })
    setResult(confirmed)
  }

  return (
    <div className="flex flex-col items-center gap-3">
      <Button variant="destructive" onClick={deleteReport}>
        Delete report
      </Button>
      <p className="text-sm text-text-secondary">
        {result === null
          ? "Not asked yet."
          : `confirm() resolved to ${result}.`}
      </p>
    </div>
  )
}
```

## Several items

When an action runs on many selected items, report the result once:

| Result                                  | Feedback                                                                                                                         |
| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Every item succeeded                    | A success toast with the count: "12 alerts resolved"                                                                             |
| The action applies to none of the items | Disable the action before it runs; the tooltip says why                                                                          |
| Some items cannot take the action       | Before running, a confirmation with the counts and the reason, offering to run on the rest                                       |
| Some items failed while running         | A dialog that summarizes the result ("9 of 12 alerts resolved"), then lists each failed item with its reason, and a Close button |

Keep the items selected afterwards, so the user can act on what failed.

## Writing the message

The words matter as much as the component. Follow [Writing](/guidelines/writing#messages):
what happened, why when it helps, what to do; no blame; no raw error messages; no
"successfully".
