diff --git a/web/chat/chat-input-text-area.react.js b/web/chat/chat-input-text-area.react.js
new file mode 100644
--- /dev/null
+++ b/web/chat/chat-input-text-area.react.js
@@ -0,0 +1,97 @@
+// @flow
+
+import invariant from 'invariant';
+import * as React from 'react';
+
+import css from './chat-input-bar.css';
+
+type Props = {
+  +send?: () => mixed,
+  +escape?: () => void,
+  +focus: boolean,
+  +currentText: string,
+};
+
+const ChatInputTextArea: React.ComponentType<Props> = React.memo<Props>(
+  function ChatInputTextArea(props: Props) {
+    const { currentText, focus, escape, send } = props;
+    const textareaRef = React.useRef(null);
+
+    const focusAndUpdateText = React.useCallback(() => {
+      if (!focus) {
+        return;
+      }
+
+      // We need to call focus() first on Safari, otherwise the cursor
+      // ends up at the start instead of the end for some reason
+      const textarea = textareaRef.current;
+      invariant(textarea, 'textarea should be set');
+      textarea.focus();
+
+      // We reset the textarea to an empty string at the start so that
+      // the cursor always ends up at the end, even if the text doesn't
+      // actually change
+      textarea.value = '';
+      if (currentText) {
+        textarea.value = currentText;
+      }
+      // The above strategies make sure the cursor is at the end,
+      // but we also need to make sure that we're scrolled to the bottom
+      textarea.scrollTop = textarea.scrollHeight;
+    }, [currentText, focus]);
+
+    const updateHeight = React.useCallback(() => {
+      const textarea = textareaRef.current;
+      if (textarea) {
+        textarea.style.height = 'auto';
+        const newHeight = Math.min(textarea.scrollHeight, 150);
+        textarea.style.height = `${newHeight}px`;
+      }
+    }, []);
+
+    React.useEffect(() => {
+      updateHeight();
+    }, [currentText, updateHeight]);
+
+    React.useEffect(() => {
+      focusAndUpdateText();
+      // eslint-disable-next-line react-hooks/exhaustive-deps
+    }, []);
+
+    const onKeyDown = (event: SyntheticKeyboardEvent<HTMLTextAreaElement>) => {
+      if (event.key === 'Escape') {
+        event.preventDefault();
+        if (!escape) {
+          return;
+        }
+        escape();
+      } else if (event.key === 'Enter' && !event.shiftKey) {
+        event.preventDefault();
+        if (!send) {
+          return;
+        }
+        send();
+      }
+    };
+
+    const onChangeMessageText = () => {
+      updateHeight();
+    };
+
+    return (
+      <div className={css.inputBarTextInput}>
+        <textarea
+          rows="1"
+          placeholder="Type your message"
+          value={currentText}
+          onChange={onChangeMessageText}
+          onKeyDown={onKeyDown}
+          ref={textareaRef}
+          autoFocus
+        />
+      </div>
+    );
+  },
+);
+
+export default ChatInputTextArea;