---
title: ChatMessageEditor
description: The in-place editor of a user message — edit the text, then cancel or send it again.
---

```tsx
'use client';

import { useState } from 'react';
import { ChatMessageEditor } from '@/ui/chat/ChatMessageEditor';

export function ChatMessageEditorDemo() {
  const [message, setMessage] = useState('My son is 14 years old, what activities do you suggest?');
  const [isEditing, setIsEditing] = useState(true);

  return (
    <div className="w-[380px] flex flex-col items-end gap-8 px-20">
      {isEditing ? (
        <ChatMessageEditor
          value={message}
          onCancel={() => setIsEditing(false)}
          onSubmit={(content) => {
            setMessage(content);
            setIsEditing(false);
          }}
        />
      ) : (
        <>
          <p className="text-b3 bg-lightSand rounded-32 px-24 py-12 text-black">{message}</p>
          <button
            type="button"
            onClick={() => setIsEditing(true)}
            className="text-b4 font-semibold underline underline-offset-2"
          >
            Edit again
          </button>
        </>
      )}
    </div>
  );
}
```

## Installation

Using the CLI:

```bash
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/chat-message-editor.json
```

Or manually, copy the following code into `/ui/chat/ChatMessageEditor.tsx`:

```tsx
'use client';

import { useEffect, useRef, useState } from 'react';
import type { FormEvent, KeyboardEvent } from 'react';
import { Button } from '../buttons/Button';
import type { ChatMessageLabels } from './ChatMessageActions';

export interface ChatMessageEditorProps {
  value: string;
  labels?: Partial<ChatMessageLabels>;
  onCancel: () => void;
  onSubmit: (value: string) => void;
}

/** In-place editor of a user message: edit the text, then cancel or send it again. */
export function ChatMessageEditor({
  value,
  labels,
  onCancel,
  onSubmit,
}: Readonly<ChatMessageEditorProps>) {
  const { cancel = 'Cancel', send = 'Send' } = labels ?? {};
  const [draft, setDraft] = useState(value);
  const textareaRef = useRef<HTMLTextAreaElement>(null);
  const trimmed = draft.trim();
  const canSubmit = trimmed.length > 0 && trimmed !== value.trim();

  useEffect(() => {
    const textarea = textareaRef.current;
    if (!textarea) return;

    textarea.style.height = 'auto';
    textarea.style.height = `${textarea.scrollHeight}px`;
  }, [draft]);

  useEffect(() => {
    const textarea = textareaRef.current;
    textarea?.focus();
    textarea?.setSelectionRange(textarea.value.length, textarea.value.length);
  }, []);

  function submit() {
    if (canSubmit) onSubmit(trimmed);
  }

  function handleSubmit(e: FormEvent) {
    e.preventDefault();
    submit();
  }

  function handleKeyDown(e: KeyboardEvent<HTMLTextAreaElement>) {
    if (e.key === 'Escape') {
      e.preventDefault();
      onCancel();
    } else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
      e.preventDefault();
      submit();
    }
  }

  return (
    <form
      onSubmit={handleSubmit}
      className="bg-lightSand flex w-full flex-col gap-12 rounded-32 px-24 py-16"
    >
      <textarea
        ref={textareaRef}
        value={draft}
        onChange={(e) => setDraft(e.target.value)}
        onKeyDown={handleKeyDown}
        aria-label="Edit message"
        rows={1}
        className="text-b3 scrollbar-hidden max-h-[40vh] w-full resize-none bg-transparent text-black outline-none"
      />
      <div className="flex justify-end gap-8">
        <Button type="button" size="small" theme="outline" color="black" onClick={onCancel}>
          {cancel}
        </Button>
        <Button type="submit" size="small" color="black" disabled={!canSubmit}>
          {send}
        </Button>
      </div>
    </form>
  );
}
```

## Usage

`ChatMessageEditor` replaces the bubble of a user message while it is edited.
[ChatMessage](/docs/components/chat-message#edit-in-place) already does it for you when you give it
`onEdit`: use this component to build your own message bubble.

```tsx showLineNumbers
import { ChatMessageEditor } from '@/ui/chat/ChatMessageEditor';
import { useState } from 'react';

export function Component() {
  const [message, setMessage] = useState('My son is 14 years old.');
  const [isEditing, setIsEditing] = useState(true);

  return isEditing ? (
    <ChatMessageEditor
      value={message}
      onCancel={() => setIsEditing(false)}
      onSubmit={(content) => {
        setMessage(content);
        setIsEditing(false);
      }}
    />
  ) : (
    <p>{message}</p>
  );
}
```

## Behavior

- The textarea is focused on mount, with the caret at the end, and grows with its content.
- **Send** is disabled while the text is empty or identical to `value` (ignoring the surrounding
  spaces), and `onSubmit` receives the trimmed text.
- <kbd>Esc</kbd> cancels, <kbd>Ctrl</kbd>/<kbd>Cmd</kbd> + <kbd>Enter</kbd> sends. A plain
  <kbd>Enter</kbd> adds a line break.
- Cancelling throws the draft away: the next edition starts again from `value`.

## Translate the labels

```tsx showLineNumbers
<ChatMessageEditor
  value="Bonjour"
  labels={{ cancel: 'Annuler', send: 'Envoyer' }}
  onCancel={() => {}}
  onSubmit={() => {}}
/>
```

## API Reference

### ChatMessageEditor

| Prop       | Type                             | Default | Description                                                       |
| ---------- | -------------------------------- | ------- | ----------------------------------------------------------------- |
| `value`    | `string`                         | —       | **Required.** Initial text of the editor                          |
| `onSubmit` | `(value: string) => void`        | —       | **Required.** Called with the trimmed text when it is sent        |
| `onCancel` | `() => void`                     | —       | **Required.** Called when the edition is cancelled                |
| `labels`   | `Partial<ChatMessageLabels>`     | English | Only `cancel` (`"Cancel"`) and `send` (`"Send"`) are used         |

See [ChatMessageActions](/docs/components/chat-message-actions#chatmessagelabels) for the
`ChatMessageLabels` type.

## Notes

- Only text can be edited: rich content (for instance rendered Markdown) cannot be put in a textarea.
- The textarea is labelled "Edit message".
