# Unsaved Changes Bar

> Persistent save and discard actions with busy, error, and page-exit warnings.

- Category: Forms
- Kind: component
- Page: https://uaiblocks.dev/components/unsaved-changes-bar
- Registry item: https://uaiblocks.dev/r/unsaved-changes-bar.json
- Source: https://github.com/victor-teles/uai/blob/main/src/registry/uai/components/unsaved-changes-bar.tsx

## Install

```bash
bunx shadcn@latest add https://uaiblocks.dev/r/unsaved-changes-bar.json
```

## Usage

```tsx
"use client";

import { useEffect, useState } from "react";
import { FormField, FormFieldInput, FormFieldLabel } from "@/components/ui/uai/form-field";
import {
  UnsavedChangesBar,
  UnsavedChangesBarActions,
  UnsavedChangesBarDiscard,
  UnsavedChangesBarMessage,
  UnsavedChangesBarSave,
  type UnsavedChangesBarVariant,
} from "@/components/ui/uai/unsaved-changes-bar";

export function UnsavedChangesBarPreview({
  variant = "bar",
}: {
  variant?: UnsavedChangesBarVariant;
}) {
  const [saved, setSaved] = useState("Product workspace");
  const [draft, setDraft] = useState("Product studio");
  const [status, setStatus] = useState<"idle" | "saving" | "error">("idle");
  const [fail, setFail] = useState(false);
  const dirty = draft !== saved;
  useEffect(() => {
    if (status !== "saving") return;
    const timer = setTimeout(() => {
      if (fail) setStatus("error");
      else {
        setSaved(draft);
        setStatus("idle");
      }
    }, 900);
    return () => clearTimeout(timer);
  }, [status, fail, draft]);
  return (
    <div style={{ display: "grid", gap: 20 }}>
      <FormField value={draft} onValueChange={setDraft} disabled={status === "saving"}>
        <FormFieldLabel>Workspace name</FormFieldLabel>
        <FormFieldInput name="workspace" />
      </FormField>
      <label
        style={{
          display: "flex",
          gap: 8,
          alignItems: "center",
          fontSize: 12,
          color: "var(--uai-muted)",
        }}
      >
        <input
          type="checkbox"
          style={{ accentColor: "var(--uai-accent)" }}
          checked={fail}
          disabled={status === "saving"}
          onChange={(event) => setFail(event.target.checked)}
        />
        Simulate a save error
      </label>
      <UnsavedChangesBar
        variant={variant}
        dirty={dirty}
        status={status}
        warnBeforeUnload={false}
        onSave={() => setStatus("saving")}
        onDiscard={() => {
          setDraft(saved);
          setStatus("idle");
        }}
      >
        <UnsavedChangesBarMessage />
        <UnsavedChangesBarActions>
          <UnsavedChangesBarDiscard />
          <UnsavedChangesBarSave />
        </UnsavedChangesBarActions>
      </UnsavedChangesBar>
      {!dirty && (
        <p role="status" style={{ margin: 0, color: "var(--uai-muted)", fontSize: 12 }}>
          All changes saved. Edit the name to try again.
        </p>
      )}
      <p style={{ margin: 0, color: "var(--uai-muted)", fontSize: 12 }}>
        Local demo. Page-exit warnings are disabled in this preview.
      </p>
    </div>
  );
}
```

## Anatomy

- `FormField`
  - `FormFieldLabel`
  - `FormFieldInput`
- `UnsavedChangesBar`
  - `UnsavedChangesBarMessage`
  - `UnsavedChangesBarActions`
    - `UnsavedChangesBarDiscard`
    - `UnsavedChangesBarSave`

## Accessibility

- Saving disables both save and discard. Errors remain visible while the consumer keeps dirty true.
- The bar sticks to the bottom of its containing scroll region. Reserve room for other fixed app controls.
- warnBeforeUnload defaults to true and attaches a native page-exit warning while dirty. Browser wording and display are browser-controlled. SPA route changes need a consumer-owned router blocker; keep dirty until save succeeds.
