# SaveStatus

> The line under a setting that applies at once, which appears only when the change failed and says what failed; otherwise it takes no room.

Source: https://raft-design.botiverse.build/components/save-status. Generated from the current release; where it differs from what you remember, this page is right.

SaveStatus is the line under a setting that saves itself: a select, a segmented control or a settings row with a switch. It shows only when the change failed, and says what failed. While the change saves and once it is stored, it shows nothing and takes no room: the control showing its new value is the confirmation, as in macOS's and iOS's settings.

```tsx
import { useState } from "react";
import {
  ListRow,
  ListRowBody,
  ListRowDescription,
  ListRowGroup,
  ListRowTitle,
  SegmentedControl,
  SegmentedControlItem,
  SegmentedControlLabel,
  Select,
  SelectContent,
  SelectIcon,
  SelectItem,
  SelectItemIndicator,
  SelectItemText,
  SelectList,
  SelectTrigger,
  SelectValue,
  Switch,
} from "@botiverse/raft-ui";
import { SaveStatus } from "@botiverse/raft-ui/wip";

const WEEK_STARTS = [
  { value: "monday", label: "Monday" },
  { value: "sunday", label: "Sunday" },
  { value: "saturday", label: "Saturday" },
];

// Each change applies at once and nothing says "Saved": the control showing
// its new value is the confirmation. Each setting's SaveStatus stays empty,
// taking no room, unless its change fails (see the failed example).
export function SaveStatusPreviewContent() {
  const [weekStart, setWeekStart] = useState<string | null>("monday");
  const [timeFormat, setTimeFormat] = useState("24h");
  const [muted, setMuted] = useState(false);

  return (
    <ListRowGroup className="@container w-full max-w-lg">
      <ListRow variant="plain" interactive={false} className="@max-lg:flex-wrap">
        <ListRowBody>
          <ListRowTitle id="week-start-title">Week starts on</ListRowTitle>
          <SaveStatus id="week-start-status" />
        </ListRowBody>
        <div className="shrink-0 @max-lg:basis-full">
          <Select items={WEEK_STARTS} value={weekStart} onValueChange={setWeekStart}>
            <SelectTrigger
              className="w-44"
              aria-labelledby="week-start-title"
              aria-describedby="week-start-status"
            >
              <SelectValue />
              <SelectIcon />
            </SelectTrigger>
            <SelectContent>
              <SelectList>
                {WEEK_STARTS.map((day) => (
                  <SelectItem key={day.value} value={day.value}>
                    <SelectItemText>{day.label}</SelectItemText>
                    <SelectItemIndicator />
                  </SelectItem>
                ))}
              </SelectList>
            </SelectContent>
          </Select>
        </div>
      </ListRow>

      <ListRow variant="plain" interactive={false} className="@max-lg:flex-wrap">
        <ListRowBody>
          <ListRowTitle id="time-format-title">Time format</ListRowTitle>
          <SaveStatus id="time-format-status" />
        </ListRowBody>
        <div className="shrink-0 @max-lg:basis-full">
          <SegmentedControl
            aria-labelledby="time-format-title"
            aria-describedby="time-format-status"
            value={timeFormat}
            onValueChange={setTimeFormat}
          >
            <SegmentedControlItem value="24h">
              <SegmentedControlLabel>24-hour</SegmentedControlLabel>
            </SegmentedControlItem>
            <SegmentedControlItem value="12h">
              <SegmentedControlLabel>12-hour</SegmentedControlLabel>
            </SegmentedControlItem>
          </SegmentedControl>
        </div>
      </ListRow>

      <ListRow variant="plain" interactive={false}>
        <ListRowBody>
          <ListRowTitle id="mute-server-title">Mute this server</ListRowTitle>
          <ListRowDescription id="mute-server-description" wrap>
            Stops notifications from this server on all your devices.
          </ListRowDescription>
          <SaveStatus id="mute-server-status" />
        </ListRowBody>
        <Switch
          checked={muted}
          onCheckedChange={setMuted}
          aria-labelledby="mute-server-title"
          aria-describedby="mute-server-description mute-server-status"
        />
      </ListRow>
    </ListRowGroup>
  );
}
```

## When to use

| Situation                                                           | Use                                                                                                                                                    |
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| A setting that applies at once failed to save                       | SaveStatus                                                                                                                                             |
| A value that is wrong before it is saved: a missing name, a bad URL | `FieldError` ([Field](https://raft-design.botiverse.build/components/field.md))                                                                                                              |
| The result of a card saved with Save                                | The Save button, which reads "Saved" with a check for a moment ([Forms](https://raft-design.botiverse.build/patterns/forms.md#settings-saved-together))              |
| Where a section's settings are stored ("Saved on this device")      | One line for the section, under its header or its group, not SaveStatus ([Forms](https://raft-design.botiverse.build/patterns/forms.md#settings-that-apply-at-once)) |
| A failure that is about more than one setting                       | Where its scope puts it ([Feedback](https://raft-design.botiverse.build/patterns/feedback.md#errors-by-scope))                                                       |

Which settings apply at once is in [Forms](https://raft-design.botiverse.build/patterns/forms.md#when-a-change-takes-effect).

## Anatomy

- `SaveStatus`: the line. Put it right after the control: after the select or the segmented control in a `Field`, before any `FieldDescription`, or last in a settings row's `ListRowBody`, after the description or, in a row with none, the title. Render it in every state; with nothing in it, it is hidden and takes no height. Required.
- `SaveStatusError`: what failed, in `danger-strong`, one footnote line (18px) that wraps. Put it in `SaveStatus` only while the change has failed.
- `SaveStatusSaved`: deprecated. It renders nothing, whatever its children, so a `SaveStatus` holding it stays empty. Leave it out.

## States

```tsx
import { useCallback, useEffect, useRef, useState } from "react";
import {
  ListRow,
  ListRowBody,
  ListRowDescription,
  ListRowGroup,
  ListRowTitle,
  Switch,
} from "@botiverse/raft-ui";
import { SaveStatus, SaveStatusError } from "@botiverse/raft-ui/wip";

export function SaveStatusFailedContent() {
  const [published, setPublished] = useState(false);
  const [failed, setFailed] = useState(false);
  const request = useRef<number | undefined>(undefined);
  useEffect(() => () => window.clearTimeout(request.current), []);

  // The switch moves at once; the save is refused, so it goes back to its old
  // value and a line appears under the description saying what failed, until
  // the next change.
  const handlePublished = useCallback((checked: boolean) => {
    window.clearTimeout(request.current);
    setFailed(false);
    setPublished(checked);
    request.current = window.setTimeout(() => {
      setPublished(!checked);
      setFailed(true);
    }, 600);
  }, []);

  return (
    <ListRowGroup className="w-full max-w-md">
      <ListRow variant="plain" interactive={false}>
        <ListRowBody>
          <ListRowTitle id="public-page-title">Public page</ListRowTitle>
          <ListRowDescription id="public-page-description" wrap>
            Anyone with the link can see the server&apos;s name and channels.
          </ListRowDescription>
          <SaveStatus id="public-page-status">
            {failed ? <SaveStatusError>Couldn&apos;t publish. Try again.</SaveStatusError> : null}
          </SaveStatus>
        </ListRowBody>
        <Switch
          checked={published}
          onCheckedChange={handlePublished}
          aria-labelledby="public-page-title"
          aria-describedby="public-page-description public-page-status"
        />
      </ListRow>
    </ListRowGroup>
  );
}
```

- **Idle, saving and saved:** `SaveStatus` with nothing in it. It shows nothing and takes no room. The control already shows the new value, which is the confirmation; a "Saved" would repeat it, and a line kept for it made every such row taller than its neighbors (cindyz, 2026-11-10, #proj-raft-ui:4ae1a74e).
- **Failed:** the control goes back to its old value, so it never shows a value that isn't true, and `SaveStatusError` says what failed until the next change.

## Behavior

- Empty, the line takes no height and no gap, so a setting with it is exactly as tall as one without it. In a settings row, the switch (or the value, chevron or button at the row's end) centers on the whole row, as in any row.
- When a change fails, the line appears 4px under what comes before it: the select or the segmented control in a `Field`, or the description in a settings row, or its title when the row has no description, so the error takes the description's place. The setting grows by that line, and in a settings row the switch moves down by half of it, centered on the whole row with the error in it. In a `Field` the line moves up 2px from the field's 6px gap; in a `ListRowBody` it adds 2px to the body's 2px gap. Anywhere else, put the control and the line together in a block of their own with no gap, and the line is 4px under it.
- A failure longer than one line wraps rather than being cut off; keep it to one short sentence.

## Content

- Write a failure as what didn't happen and what to do: Couldn't mute this server. Try again. When the request comes back with a reason, show that reason ([Writing](https://raft-design.botiverse.build/content/writing.md)).
- Don't write "Saved", and don't say where a setting is stored on its row. "Saved on this device" is said once for a section ([Forms](https://raft-design.botiverse.build/patterns/forms.md#settings-that-apply-at-once)).

## Accessibility

- `SaveStatusError` has `role="alert"`, so the failure is announced when it appears.
- The control announces its own new value (a switch's on or off, a segmented control's or a select's choice), so a change that saves needs no announcement of its own.
- Point the control's `aria-describedby` at the line's `id` (on `SaveStatus`), beside any description, so a failure is read with the control.

## API

### SaveStatus

The line under a setting that applies at once. Render it whatever the setting's state, with `SaveStatusError` inside when the change failed and nothing inside otherwise: empty, it is hidden and takes no height. A `span` (shown as a block), like every ListRow part, so it stays valid inside a row's body; give it an `id` for the control's `aria-describedby`.

Also takes the attributes of its element.

### SaveStatusError

What failed. It is an alert, announced when it appears, and stays until the next change. Its line appears with it, so the setting grows by one line while it shows.

Also takes the attributes of its element.

### SaveStatusSaved

Deprecated: A setting that applies at once no longer says "Saved": the
control showing its new value is the confirmation (Forms, settings that
apply at once). Renders nothing, whatever its children, so its
`SaveStatus` stays empty and takes no height. Leave it out: render
`SaveStatusError` when the change failed and nothing otherwise. It drops
every prop, its `id` too: point a control's `aria-describedby` at the
`SaveStatus` instead.

Also takes the attributes of its element.

## Changes

- 0.83.0 (Fixed): **A settings row's switch centers on its text, not on the SaveStatus line under it**: in a `ListRow variant="plain"` whose `ListRowBody` ends with a `SaveStatus`, both ends (the switch, value, chevron, checkbox, radio or button, and a leading icon, avatar or tile) center on the title and description. The row moves the line's reserved space (4px and one footnote line) into its bottom padding, so its height is unchanged and nothing moves when "Saved" or a one-line error appears. There is no new prop.
- 0.54.0 (Added): **SaveStatus** (wip, + `SaveStatusSaved`, `SaveStatusError`): the result line under a setting that applies at once. It always keeps one footnote line of height, so "Saved" appearing or going never resizes the card or moves a switch, and it sits 4px under a select, a segmented control or a settings row's description alike.
