# Message Input UI

Message input is used for composing and editing messages. In this sense, it's a primary component
that users interact with in a chat, so it's important to get it right.

Message input is a bit more complex than it might seem at first glance. Not just a text box with a
"send" button, it has a lot of hidden features:

1. Updating the typing status
2. Uploading and previewing attachments
3. Displaying link previews
4. Auto-completing mentions, commands, emoji...

![](@chat-sdk/react/v12/_assets/message-input-ui-kitchensink.png)

We will cover this features step by step. For now, let's start with the simplest markup possible:

<Tabs>

</Tabs>

![](@chat-sdk/react/v11/_assets/message-input-ui-simple.png)

Note that you should not render your custom message input directly, but instead pass it as a prop to
either [`Channel`](/chat/docs/sdk/react/v11/components/core-components/channel/) or
[`MessageInput`](/chat/docs/sdk/react/v11/components/message-input-components/message-input/) component. That way,
you can be sure that your input is wrapped with the necessary context providers, most importantly
the [`MessageInputContext`](/chat/docs/sdk/react/v11/components/contexts/message-input-context/).

```jsx
import {
  Chat,
  Channel,
  ChannelHeader,
  ChannelList,
  MessageList,
  Thread,
  Window,
  MessageInput,
} from "stream-chat-react";

export const App = () => (
  <Chat client={chatClient}>
    <ChannelList filters={filters} sort={sort} options={options} />
    <Channel>
      <Window>
        <ChannelHeader />
        <MessageList />
        <MessageInput Input={CustomMessageInput} />
      </Window>
      <Thread />
    </Channel>
  </Chat>
);
```

For now, our custom input doesn't do anything. The
[`MessageInputContext`](/chat/docs/sdk/react/v11/components/contexts/message-input-context/) handles most of the
input-related state and actions, so instead of handling the state yourself, just use the provided
values and callbacks:

```jsx
import { useMessageInputContext } from "stream-chat-react";

const CustomMessageInput = () => {
  const { text, handleChange, handleSubmit } = useMessageInputContext();

  return (
    <div className="message-input">
      <textarea
        value={text}
        className="message-input__input"
        onChange={handleChange}
      />
      <button
        type="button"
        className="message-input__button"
        onClick={handleSubmit}
      >
        ⬆️
      </button>
    </div>
  );
};
```

The great thing is that the `handleChange` callback not only updates the text value, but also
detects links (if URL enrichment is enabled), and updates your typing status.

And with the `handleSubmit` callback added to the "send" button, our basic implementation is
complete - try it out!

## Uploading and Previewing Attachments

To support adding attachments to the message, we should start by adding a file input. And similar to
the "send" button, once a file is selected, we can use a callback provided in the
[`MessageInputContext`](/chat/docs/sdk/react/v11/components/contexts/message-input-context/) to upload it as an
attachment:

```jsx
import { useMessageInputContext } from "stream-chat-react";

const AttachmentUploadButton = () => {
  const { uploadNewFiles } = useMessageInputContext();

  function handleChange(e) {
    const files = e.currentTarget.files;

    if (files && files.length > 0) {
      uploadNewFiles(files);
      e.currentTarget.value = "";
    }
  }

  return (
    <label className="message-input__button">
      <input type="file" className="visually-hidden" onChange={handleChange} />
      📎
    </label>
  );
};
```

![](@chat-sdk/react/v11/_assets/message-input-ui-upload-button.png)

There are two cases when uploads should not be allowed:

1. Uploads are disabled for the current channel. We should check the
   [`isUploadEnabled`](/chat/docs/sdk/react/v11/components/contexts/message-input-context#isuploadenabled/) value
   from the `MessageInputContext` to make sure.
2. The maximum number of message attachments has been reached. For this we should check the
   [`maxFilesLeft`](/chat/docs/sdk/react/v11/components/contexts/message-input-context#maxfilesleft/) value from the
   `MessageInputContext`.

Let's add these two checks:

```jsx
import { useMessageInputContext } from "stream-chat-react";

const AttachmentUploadButton = () => {
  const { uploadNewFiles, isUploadEnabled, maxFilesLeft } =
    useMessageInputContext();

  function handleChange(e) {
    const files = e.currentTarget.files;

    if (files && files.length > 0) {
      uploadNewFiles(files);
    }
  }

  if (!isUploadEnabled || maxFilesLeft === 0) {
    return null;
  }

  return (
    <label className="message-input__button">
      <input type="file" className="visually-hidden" onChange={handleChange} />
      📎
    </label>
  );
};
```

Now we need a way to preview the added attachments. The SDK provides a ready-made component for
this:
[`AttachmentPreviewList`](/chat/docs/sdk/react/v11/components/contexts/component-context#attachmentpreviewlist/).
Instead of importing it directly, you can grab it from the
[`ComponentContext`](/chat/docs/sdk/react/v11/components/contexts/component-context/), which is used throughout the
SDK to provide overridable UI components, and only fall back to the default implementation if it
hasn't been overridden:

<Tabs>

</Tabs>

The nice thing about this approach is that our custom message input is not tied to a particular
implementation of the attachment preview component, and the preview component can be overridden at
the `Channel` level.

However, if you need deep customization of the attachment preview list, and the out-of-the-box
component doesn't meet your needs, it's fairly easy to implement it from scratch.

The two relevant values from the `MessageInputContext` are
[`fileUploads`](/chat/docs/sdk/react/v11/components/contexts/message-input-context#fileuploads/) and
[`imageUploads`](/chat/docs/sdk/react/v11/components/contexts/message-input-context#imageuploads/), which map upload
ids to the objects that describe an attachment. Image attachments are usually treated differently
from generic files, so it is convenient to keep the two attachment types are separate.

Since object keys come in no particular order, you can use
[`fileOrder`](/chat/docs/sdk/react/v11/components/contexts/message-input-context#fileorder/) and
[`imageOrder`](/chat/docs/sdk/react/v11/components/contexts/message-input-context#imageorder/) arrays, which specify
the order of attachments by their ids.

```jsx
import { useMessageInputContext } from "stream-chat-react";

const CustomAttachmentPreviewList = () => {
  const { fileOrder, imageOrder } = useMessageInputContext();

  if (fileOrder.length === 0 && imageOrder.length === 0) {
    return null;
  }

  return (
    <ul className="message-input__attachments">
      {imageOrder.map((id) => (
        <li className="message-input__attachment" key={id}>
          <ImageAttachmentPreview id={id} />
        </li>
      ))}
      {fileOrder.map((id) => (
        <li key={id}>
          <FileAttachmentPreview id={id} />
        </li>
      ))}
    </ul>
  );
};
```

Note that an attachment can be in three different states:

1. `'uploading'` means the attachment is still uploading; you may want to display a spinner in this
   case.
2. `'failed'` means that something went wrong while uploading the attachment; you may want to show
   an option to retry the upload.
3. `'finished'` means the attachment had been successfully uploaded.

For image attachments, the `previewUri` property is available, which is a temporary URL that can be
used to display an image preview before the image is uploaded. Once the image is uploaded, this
temporary URL is no longer available, and `url` should be used instead.

```jsx
import { useMessageInputContext } from "stream-chat-react";

const ImageAttachmentPreview = ({ id }) => {
  const { imageUploads } = useMessageInputContext();
  const image = imageUploads[id];
  const url = image.previewUri ?? image.url ?? "fallback.webm";

  return (
    <div
      className="message-input__attachment-preview message-input__attachment-preview_image"
      style={{ backgroundImage: `url(${url})` }}
      aria-label={image.file.name}
    >
      <AttachmentActions attachment={image} type="image" />
    </div>
  );
};

const AttachmentActions = ({ attachment }) => {
  const { removeImage, uploadImage, removeFile, uploadFile } =
    useMessageInputContext();
  let [remove, upload] =
    type === "image" ? [removeImage, uploadImage] : [removeFile, uploadFile];
  let children = null;

  if (attachment.state === "uploading") {
    children = (
      <div className="message-input__attachment-action">Loading...</div>
    );
  }

  if (attachment.state === "finished") {
    children = (
      <>
        <a
          className="message-input__attachment-action"
          href={attachment.url}
          target="_blank"
          rel="noreferrer"
        >
          📥
        </a>
        <button
          className="message-input__attachment-action"
          onClick={() => remove(attachment.id)}
        >
          ❌
        </button>
      </>
    );
  }

  if (attachment.state === "failed") {
    <button
      className="message-input__attachment-action"
      onClick={() => upload(attachment.id)}
    >
      Failed. Retry?
    </button>;
  }

  return <div className="message-input__attachment-actions">{children}</div>;
};
```

For generic file attachments, the file name (`file.name`) and MIME type (`file.type`) can be used to
display a preview:

```jsx
import { useMessageInputContext } from "stream-chat-react";

const FileAttachmentPreview = ({ id }) => {
  const { fileUploads } = useMessageInputContext();
  const attachment = fileUploads[id];

  return (
    <div className="message-input__attachment-preview message-input__attachment-preview_file">
      📄 {attachment.file.name} <br />({attachment.file.size} bytes)
      <AttachmentActions attachment={attachment} type="file" />
    </div>
  );
};
```

Putting it all together, this is a complete implementation of the attachment preview component:

![](@chat-sdk/react/v11/_assets/message-input-ui-attachments.png)

<Tabs>

</Tabs>

## Displaying Link Previews

If URL enrichment is enabled both in channel settings (enabled by default) and in the
[`urlEnrichmentConfig`](/chat/docs/sdk/react/v11/components/message-input-components/message-input/#urlenrichmentconfig/)
of the `MessageInput` component (disabled by default), the SDK will automatically detect links in
the message text (as long as it's set properly in the
[`MessageInputContext`](/chat/docs/sdk/react/v11/components/contexts/message-input-context/)) and create previews
for them.

To display link previews, you can use a pre-built
[`LinkPreviewList`](/chat/docs/sdk/react/v11/components/contexts/component-context#linkpreviewlist/) component
available in the [`ComponentContext`](/chat/docs/sdk/react/v11/components/contexts/component-context/). Using the
`ComponentContext` allows you to hook into the component override mechanism used throughout the SDK.

So the rough idea is:

1. Grab the [`LinkPreviewList`](/chat/docs/sdk/react/v11/components/contexts/component-context#linkpreviewlist/)
   from the `ComponentContext` (fall back to the default implementation if the component wasn't
   overridden).
2. Grab the [`linkPreviews`](/chat/docs/sdk/react/v11/components/contexts/message-input-context#linkpreviews/) from
   the `MessageInputContext` and pass them to the `LinkPreviewList`.

The only thing to note here is that
[`linkPreviews`](/chat/docs/sdk/react/v11/components/contexts/message-input-context#linkpreviews/) is a Map with
URLs as keys and enriched data as values. Before passing it to the `LinkPreviewList`, we should
convert it to an array:

```jsx
import {
  useMessageInputContext,
  useComponentContext,
  AttachmentPreviewList as DefaultAttachmentPreviewList,
  LinkPreviewList as DefaultLinkPreviewList,
} from "stream-chat-react";

const CustomMessageInput = () => {
  const { text, linkPreviews, handleChange, handleSubmit } =
    useMessageInputContext();
  const {
    LinkPreviewList = DefaultLinkPreviewList,
    AttachmentPreviewList = DefaultAttachmentPreviewList,
  } = useComponentContext();

  return (
    <div className="message-input">
      <LinkPreviewList linkPreviews={Array.from(linkPreviews.values())} />
      <div className="message-input__composer">
        <AttachmentUploadButton />
        <textarea
          className="message-input__input"
          value={text}
          onChange={handleChange}
        />
        <button
          type="button"
          className="message-input__button"
          onClick={handleSubmit}
        >
          ⬆️
        </button>
      </div>
      <AttachmentPreviewList />
    </div>
  );
};
```

As always, if you need deeper customization, you can implement the link preview component from
scratch. Since all the necessary data can be found in the
[`linkPreviews`](/chat/docs/sdk/react/v11/components/contexts/message-input-context#linkpreviews/) value of the
`MessageInputContext`, the implementation itself is quite simple.

The link preview itself goes through several lifecycle states:

1. `'queued'` means that the URL enrichment process hasn't started yet. This process is debounced,
   so to avoid flashing UI we should ignore queued previews.
2. `'loading'` means that URL enrichment is in progress. Depending on the desired UX, you can either
   ignore loading previews, or show a spinner or other loading state.
3. `'loaded'` means that the preview is ready.
4. `'dismissed'` means the preview has been dismissed by the user.

So the only bit of interactivity we need to add is an option to dismiss a link preview:

<Tabs>

</Tabs>

![](@chat-sdk/react/v12/_assets/message-input-ui-link.png)

## Handling Slow Mode

If the Slow Mode is configured for the current channel, we should prevent messages from being sent
before the cooldown period has passed.

The first thing we should do is disable or hide the "send" button while the cooldown period is in
effect. We can use the
[`cooldownRemaining`](/chat/docs/sdk/react/v11/components/contexts/message-input-context#cooldownremaining/) value
from the `MessageInputContext`: it's guaranteed to have a non-zero numeric value while the user is
within a cooldown period, and to reset to a falsy value when the cooldown period ends. Note that to
avoid excessive rendering, the value itself does not "tick down" every second.

To provide visual feedback with a countdown, we can use the
[`CooldownTimer`](/chat/docs/sdk/react/v11/components/contexts/component-context#cooldowntimer/) component from the
[`ComponentContext`](/chat/docs/sdk/react/v11/components/contexts/component-context/):

```jsx
import {
  useMessageInputContext,
  useComponentContext,
  AttachmentPreviewList as DefaultAttachmentPreviewList,
  CooldownTimer as DefaultCooldownTimer,
} from "stream-chat-react";

const CustomMessageInput = () => {
  const {
    text,
    cooldownRemaining,
    setCooldownRemaining,
    cooldownInterval,
    handleChange,
    handleSubmit,
  } = useMessageInputContext();
  const {
    CooldownTimer = DefaultCooldownTimer,
    AttachmentPreviewList = DefaultAttachmentPreviewList,
  } = useComponentContext();

  return (
    <div className="message-input">
      <div className="message-input__composer">
        <AttachmentUploadButton />
        <textarea
          className="message-input__input"
          value={text}
          onChange={handleChange}
        />
        <button
          type="button"
          className="message-input__button"
          disabled={!!cooldownRemaining}
          onClick={handleSubmit}
        >
          {cooldownRemaining ? (
            <>
              ⏳{" "}
              <CooldownTimer
                cooldownInterval={cooldownInterval}
                setCooldownRemaining={setCooldownRemaining}
              />
            </>
          ) : (
            <>⬆️</>
          )}
        </button>
      </div>
      <AttachmentPreviewList />
    </div>
  );
};
```

The component itself is very simple, it's just a single `<div>` with a timer that counts down
seconds. Most customizations can be done with CSS, but if you need to implement the whole component
from scratch, the only trick is to add a timer that counts down from the
[`cooldownInterval`](/chat/docs/sdk/react/v11/components/contexts/component-context#cooldowninterval/):

```jsx
const CustomCooldownTimer = () => {
  const { cooldownRemaining, cooldownInterval } = useMessageInputContext();
  const enabled = !!cooldownRemaining;
  const [secondsPassed, setSecondsPassed] = useState(0);

  useEffect(() => {
    const startedAt = Date.now();
    const interval = setInterval(() => {
      setSecondsPassed(Math.floor((Date.now() - startedAt) / 1000));
    }, 500);
    return () => clearInterval(interval);
  }, [enabled]);

  return <strong>⏳ {cooldownInterval - secondsPassed}</strong>;
};
```

![](@chat-sdk/react/v12/_assets/message-input-ui-cooldown.png)


---

This page was last updated at 2026-05-22T16:32:11.003Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v11/guides/theming/input-ui/](https://getstream.io/chat/docs/sdk/react/v11/guides/theming/input-ui/).