> ## Documentation Index
> Fetch the complete documentation index at: https://anvil.servicetitan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Edit Card - Web

> Edit Cards group related controls and information together, allowing a user to view and edit the grouped content.

> [!NOTE]
>
> For development, import `EditCard` from
> `@servicetitan/anvil2`. For design, find it in the
> [Anvil2 file](https://www.figma.com/design/xaNQsvRKczoxu7pj0M6QT8/Anvil2-v3).

![](https://mintcdn.com/servicetitan/uz2PQSvO75TRhQ38/images/docs/web/components/shared/overview-of-an-edit-card.png?fit=max\&auto=format\&n=uz2PQSvO75TRhQ38\&q=85\&s=bea01def576ae7d85380d2ebd3e2ed79)

## Anatomy

The Edit Card consists of five primary elements that work together to group related controls and information for viewing and editing.

![Anatomy of an Edit
Card](https://mintcdn.com/servicetitan/CekIMDXcDEhGoN_I/images/docs/web/components/edit-card/design/anatomy-of-an-edit-card.png?fit=max\&auto=format\&n=CekIMDXcDEhGoN_I\&q=85\&s=951a899aead047a9f8d11a5d7bc151b4)

1. Status Icon
2. Title
3. Header action
4. Body content (presented as form actions)
5. Footer actions

## Options

The Edit Card supports multiple status states, action configurations, and flexible body content to accommodate various editing scenarios.

### Status

#### Not Started

editcard-state-not-started

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/editcard-state-not-started?file=src/App.tsx)

```tsx lines theme={null}
import { EditCard, Flex } from "@servicetitan/anvil2";

function App() {
  return (
    <Flex>
      <EditCard
        flexGrow="1"
        headerText="Not Started Header"
        state="not started"
      >
        Body content within the Edit Card.
      </EditCard>
    </Flex>
  );
}

export default App;
```

#### In Progress

The editing state uniquely contains footer actions and no header actions.

editcard-state-in-progress

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/editcard-state-in-progress?file=src/App.tsx)

```tsx lines theme={null}
import { EditCard, Flex } from "@servicetitan/anvil2";

function App() {
  return (
    <Flex>
      <EditCard
        flexGrow="1"
        headerText="In Progress Header"
        state="in progress"
      >
        Body content within the Edit Card.
      </EditCard>
    </Flex>
  );
}

export default App;
```

#### Error

editcard-state-error

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/editcard-state-error?file=src/App.tsx)

```tsx lines theme={null}
import { EditCard, Flex } from "@servicetitan/anvil2";

function App() {
  return (
    <Flex>
      <EditCard flexGrow="1" headerText="Error Header" state="error">
        Body content within the Edit Card.
      </EditCard>
    </Flex>
  );
}

export default App;
```

#### Complete

editcard-state-complete

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/editcard-state-complete?file=src/App.tsx)

```tsx lines theme={null}
import { EditCard, Flex } from "@servicetitan/anvil2";

function App() {
  return (
    <Flex>
      <EditCard flexGrow="1" headerText="Complete Header" state="complete">
        Body content within the Edit Card.
      </EditCard>
    </Flex>
  );
}

export default App;
```

### Actions

In the not started, completed, and error statuses, a single action is used in the header region. In the in progress status, a cancel and call-to-action are used in the footer region. The copy is modifiable and each action can be disabled. The actions cannot be modified beyond this.

### Body content

Edit Card supports any type of content below the title. This is typically represented as text in non-editing statuses and as form elements in the in progress status.

editcard-children

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/editcard-children?file=src/App.tsx)

```tsx lines theme={null}
import {
  EditCard,
  Flex,
  Text,
  Grid,
  TextField,
  Radio,
} from "@servicetitan/anvil2";

function App() {
  return (
    <Flex direction="column" gap={6}>
      <EditCard headerText="Edit Card" state="not started" />
      <EditCard headerText="Edit Card" state="not started">
        <Flex gap={8} grow={1} wrap="wrap" style={{ width: "500px" }}>
          <Flex direction="column" gap={1} basis="200px" shrink="0" grow="1">
            <Text variant="eyebrow" size="medium">
              Name
            </Text>
            <Text size="medium">Jane Doe</Text>
          </Flex>
          <Flex direction="column" gap={1} basis="200px" shrink="0" grow="1">
            <Text variant="eyebrow" size="medium">
              Notification Frequency
            </Text>
            <Text size="medium">Weekly</Text>
          </Flex>
        </Flex>
      </EditCard>
      <EditCard headerText="Edit Card" state="in progress">
        <Grid gap="6">
          <TextField label="Name" />
          <Radio.Group legend="Notification Frequency">
            <Radio name="ex1" value="daily" label="Daily" />
            <Radio name="ex1" value="weekly" label="Weekly" defaultChecked />
            <Radio name="ex1" value="monthly" label="Monthly" />
          </Radio.Group>
        </Grid>
      </EditCard>
    </Flex>
  );
}

export default App;
```

## Behavior

The Edit Card responds to state changes and content overflow while maintaining consistent editing interactions.

### Overflow handling

editcard-overflow

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/editcard-overflow?file=src/App.tsx)

```tsx lines theme={null}
import { EditCard } from "@servicetitan/anvil2";

function App() {
  return (
    <EditCard
      style={{ maxWidth: 325 }}
      flexGrow="1"
      headerText="An Edit Card title will wrap when not enough space is available for it."
      state="not started"
    >
      Content inside the Edit Card will overflow depending on how it is
      implemented, with the most typical being wrapping.
    </EditCard>
  );
}

export default App;
```

## Usage Guidelines

| Standard | Requirement |
| - | - |
| DoChunk actions with a toggleable editing state | Use an Edit Card when a set of actions and information should be chunked in the UI, while also needing the ability to toggle an editing state. |
| DoUse for progressive disclosure | Edit Cards serve as a form of progressive disclosure, showing a summary of configurations without also showing all the underlying UI controls. |
| Don'tUse an excess number of them | Edit Cards create friction to accessing controls. Avoid using an excess number of them. |
| Don'tPair with page-level save controls | When using Page-level save controls, Edit Cards cause confusion and unnecessary friction. In these cases, group UI controls another way. |

### Alternatives

#### Edit Card vs Stepper

Steppers move through a linear progression of steps. Edit Cards support both linear and unrelated blocks of content.

Steppers are typically scoped to the whole view, e.g. a fullscreen Dialog, while Edit Card can be scoped within smaller sections of a view, e.g. part of a Drawer screen.

Steppers only give summaries of the flow indirectly, typically on the final step of a flow, while Edit Card shows a summary in most of its states.

Steppers and Edit Cards work together for complex flows.

### How to Use

#### Summarize user selections

The incomplete, success, and error state of Edit Cards show a summary of the user's configuration in its body section. Customize this to fit the context of the Edit Card controls.

editcard-summary-do

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/editcard-summary-do?file=src/App.tsx)

```tsx lines theme={null}
import {
  EditCard,
  Grid,
  TextField,
  Radio,
  Flex,
  Text,
  type EditCardState,
  type EditCardChange,
  type RadioState,
} from "@servicetitan/anvil2";
import { useState, type ChangeEvent } from "react";

function App() {
  const [state, setState] = useState<EditCardState>("complete");
  const onStateChange = (change: EditCardChange) => {
    if (change === "edit") {
      console.log("in progress");
      setState("in progress");
    }
    if (change === "save") {
      console.log("saved");
      setState("complete");
    }

    if (change === "cancel") {
      setState("complete");
    }
  };

  const [inputValue, setInputValue] = useState("Jane Doe");

  const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
    setInputValue(event.target.value);
  };

  const [list, setList] = useState([
    {
      label: "Email",
      checked: false,
    },
    {
      label: "Text",
      checked: true,
    },
  ]);

  const selectedPref = list.find((item) => item.checked);
  console.info("selectedPref:", selectedPref);

  const handleUpdate = (_state: RadioState | undefined, index: number) => {
    setList((prev) =>
      prev.map((item, i) => ({
        label: item.label,
        checked: index === i,
      })),
    );
  };

  const active = (
    <Grid gap="6">
      <TextField label="Name" value={inputValue} onChange={handleInputChange} />

      <Radio.Group legend="Preferred communication method">
        {list.map((item, i) => {
          return (
            <Radio
              key={i}
              checked={item.checked}
              label={item.label}
              onChange={(
                _e?: ChangeEvent<HTMLInputElement>,
                state?: RadioState,
              ) => handleUpdate(state, i)}
            />
          );
        })}
      </Radio.Group>
    </Grid>
  );

  const summary = (
    <Flex gap={8} grow={1} wrap="wrap">
      <Flex direction="column" gap={1} basis="200px">
        <Text variant="eyebrow" size="medium">
          Name
        </Text>
        <Text size="medium">{inputValue}</Text>
      </Flex>
      <Flex direction="column" gap={1} basis="200px">
        <Text variant="eyebrow" size="medium">
          Communication Method
        </Text>
        <Text size="medium">{selectedPref?.label}</Text>
      </Flex>
    </Flex>
  );

  return (
    <EditCard
      state={state}
      headerText="User Information"
      onStateChange={onStateChange}
    >
      <Flex grow={1} style={{ width: "500px", maxWidth: "100%" }}>
        {(state === "not started" || state === "complete") && summary}
        {state === "in progress" && active}
      </Flex>
    </EditCard>
  );
}

export default App;
```

#### Number of controls in the Edit Card

No specific rule exists for how many controls exist in Edit Card.

* Avoid having only one control in Edit Card. This creates excessive friction in the UI.
* Avoid having excessively long Edit Cards. Tall editing states push the Save controls out of view. Break up the content into multiple Edit Cards, or omit Edit Card in favor of a page-level flow.

#### Unordered set of Edit Cards

Edit Cards work together without being dependent on other Edit Cards.

* Multiple Edit Cards can be in the editing state at once
* Avoid disabling any edit or save actions across Edit Cards

#### Ordered set of Edit Cards

Edit Cards work together to create a sequential flow, where each step builds on top of each other. Edit Cards do not provide any additional interactions for this scenario.

* When a user clicks on the Next/Save action within one Edit Card, have the subsequent card automatically open.
* Disable any edit actions on Edit Cards that aren't ready to be interacted with.
* Factor in downstream effects, e.g. when a user returns to an early step and modifies a setting that impacts already completed steps.

#### Saving Edit Cards

* When in the editing state, the footer action has either an explicit Save or Next/Finish.
* When Edit Card has an explicit Save action, the system remembers this save in some way.
* If Edit Card uses Next/Finish instead of Save, changed content doesn't get preserved until another save occurs.

##### Guidance for Edit Cards and Page-level saves

* In general, avoid having both Edit Card and page-level save at once.
* If there's both an Edit Card save and page-level save, saving Edit Card but not saving the page-level save produces a draft state that preserves Edit Card's new content.
  * If this cannot be done, use the Next/Finish copy, or make major UI revisions to avoid the scenario entirely.
* Edit Card and page-level saves work simultaneously. In usability testing multiple save scenarios, users correctly utilized the contextually relevant save every time.

#### Recommended spacing between Edit Cards

Use a 24px / 1.5rem / size-6 gap between stacked Edit Cards.

editcard-spacing

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/editcard-spacing?file=src/App.tsx)

```tsx lines theme={null}
import { EditCard, Flex } from "@servicetitan/anvil2";

function App() {
  return (
    <Flex direction="column" gap={6}>
      <EditCard headerText="Edit Card" state="not started" />
      <EditCard headerText="Edit Card" state="not started" />
      <EditCard headerText="Edit Card" state="not started" />
    </Flex>
  );
}

export default App;
```

## Content

Content within the Edit Card should clearly communicate the current state and provide appropriate summaries or editing controls.

## Keyboard Interaction

Users can navigate the Edit Card using standard keyboard controls.

### Accessibility

#### Suggested focus management

Consider these factors when handling Edit Card's focus behavior between state changes.

* When entering the 'In progress' state, focus goes to the first focusable element in its body content.
* When leaving the 'In progress' state, focus placement varies:
  * If Edit Cards are not part of an ordered flow, return focus to the element that triggered the state originally, if still available. If not, place it on the next tabbable element.
  * If Edit Card is part of a set of ordered Edit Cards, focus goes to the next focusable element within the next Edit Card, assuming it is not the last Edit Card. If it's the last Edit Card in the flow, bring focus to the most logical place.

For more guidance on focus management with dynamic content, see [changing content best practices](https://anvil.servicetitan.com/docs/accessibility/changing-content.md).

## Code

**Implementation**

editcard-playground

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/editcard-playground?file=src/App.tsx)

```tsx lines expandable theme={null}
import {
  EditCard,
  type EditCardState,
  type EditCardChange,
} from "@servicetitan/anvil2";
import { useState } from "react";

function App() {
  const [state, setState] = useState<EditCardState>("not started");
  const onStateChange = (change: EditCardChange) => {
    if (change === "edit") {
      setState("in progress");
    }
    if (change === "save") {
      setState("complete");
    }
    if (change === "cancel") {
      setState("error");
    }
  };

  return (
    <EditCard headerText="Header" state={state} onStateChange={onStateChange}>
      Edit card content
    </EditCard>
  );
}

export default App;
```

## Common Examples

```tsx theme={null}
import { EditCard, EditCardState } from "@servicetitan/anvil2";

const ExampleComponent = () => {
  const [state, setState] = useState<EditCardState>("not started");
  const onChange = (change: EditCardChange) => {
    if (change === "edit") {
      setState("in progress");
    }
    if (change === "save") {
      setState("complete");
    }
    if (change === "cancel") {
      setState("not started");
    }
  };
  return <EditCard state={state} headerText="Header" />;
};
```

### Validation

Saving can be disabled while the contents of the component fail validation.

editcard-disablesave

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/editcard-disablesave?file=src/App.tsx)

```tsx lines expandable theme={null}
import { EditCard } from "@servicetitan/anvil2";

function App() {
  return (
    <EditCard headerText="Disabled save" state="in progress" disableSave />
  );
}

export default App;
```

**EditCard Props**

```tsx theme={null}
import { EditCard } from "@servicetitan/anvil2";

function ExampleComponent() {
return (

<EditCard
  headerText="Header"
  state="not started"
  disableEdit={false}
  disableSave={false}
  editButtonText="Edit"
  saveButtonText="Save"
/>
); }

```

## `EditCard` Props

**headerText** `string | \{ "not started": string; "in progress": string; error: string; complete: string \}` *required*

Pass an object in order to change the header text based on the current state.
This property is required.

**state** `"not started" | "in progress" | "error" | "complete"` *required*

**disableEdit** `boolean` (default: false)

**disableSave** `boolean` (default: false)

**editButtonText** `string` (default: Edit)

**onStateChange** `(changeType: "edit" | "save" | "cancel") =\> void`

Called when a button is pressed intended to change state (edit, save, or
cancel).

**saveButtonText** `string` (default: Save)

The text displayed on the save button.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.