diff --git a/frontend/src/metabase/core/components/EditableText/EditableText.stories.tsx b/frontend/src/metabase/core/components/EditableText/EditableText.stories.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..17ae5709d2d12bdcee3a93df99f897c2c4aac5fb
--- /dev/null
+++ b/frontend/src/metabase/core/components/EditableText/EditableText.stories.tsx
@@ -0,0 +1,32 @@
+import React from "react";
+import { ComponentStory } from "@storybook/react";
+import EditableText from "./EditableText";
+
+export default {
+  title: "Core/EditableText",
+  component: EditableText,
+};
+
+const Template: ComponentStory<typeof EditableText> = args => {
+  return <EditableText {...args} />;
+};
+
+export const Default = Template.bind({});
+Default.args = {
+  initialValue: "Question",
+  placeholder: "Enter title",
+};
+
+export const Multiline = Template.bind({});
+Multiline.args = {
+  initialValue: "Question",
+  placeholder: "Enter title",
+  isMultiline: true,
+};
+
+export const WithMaxWidth = Template.bind({});
+WithMaxWidth.args = {
+  initialValue: "Question",
+  placeholder: "Enter title",
+  style: { maxWidth: 500 },
+};
diff --git a/frontend/src/metabase/core/components/EditableText/EditableText.styled.tsx b/frontend/src/metabase/core/components/EditableText/EditableText.styled.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f3fe410b15217427ff88dd9bc2407d821f38791f
--- /dev/null
+++ b/frontend/src/metabase/core/components/EditableText/EditableText.styled.tsx
@@ -0,0 +1,45 @@
+import styled from "@emotion/styled";
+import { color } from "metabase/lib/colors";
+
+export const EditableTextRoot = styled.div`
+  position: relative;
+  color: ${color("text-dark")};
+  padding: 0.25rem;
+  border: 1px solid transparent;
+
+  &:hover,
+  &:focus-within {
+    border-color: ${color("border")};
+  }
+
+  &:after {
+    content: attr(data-value);
+    visibility: hidden;
+    white-space: pre-wrap;
+    word-wrap: break-word;
+  }
+`;
+
+export const EditableTextArea = styled.textarea`
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  min-height: 0;
+  padding: inherit;
+  color: inherit;
+  font-size: inherit;
+  font-weight: inherit;
+  line-height: inherit;
+  cursor: pointer;
+  border: none;
+  resize: none;
+  outline: none;
+  overflow: hidden;
+  background: transparent;
+
+  &:focus {
+    cursor: text;
+  }
+`;
diff --git a/frontend/src/metabase/core/components/EditableText/EditableText.tsx b/frontend/src/metabase/core/components/EditableText/EditableText.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4e50c84462d2ff52786ef2bb5fcd1c14911e6ae5
--- /dev/null
+++ b/frontend/src/metabase/core/components/EditableText/EditableText.tsx
@@ -0,0 +1,79 @@
+import React, {
+  ChangeEvent,
+  KeyboardEvent,
+  forwardRef,
+  HTMLAttributes,
+  Ref,
+  useCallback,
+  useState,
+} from "react";
+import { EditableTextArea, EditableTextRoot } from "./EditableText.styled";
+
+export type EditableTextAttributes = Omit<
+  HTMLAttributes<HTMLDivElement>,
+  "onChange"
+>;
+
+export interface EditableTextProps extends EditableTextAttributes {
+  initialValue?: string | null;
+  placeholder?: string;
+  isMultiline?: boolean;
+  onChange?: (value: string) => void;
+  "data-testid"?: string;
+}
+
+const EditableText = forwardRef(function EditableText(
+  {
+    initialValue,
+    placeholder,
+    isMultiline = false,
+    onChange,
+    "data-testid": dataTestId,
+    ...props
+  }: EditableTextProps,
+  ref: Ref<HTMLDivElement>,
+) {
+  const [inputValue, setInputValue] = useState(initialValue ?? "");
+  const [submitValue, setSubmitValue] = useState(initialValue ?? "");
+
+  const handleBlur = useCallback(() => {
+    if (inputValue !== submitValue) {
+      setSubmitValue(inputValue);
+      onChange?.(inputValue);
+    }
+  }, [inputValue, submitValue, onChange]);
+
+  const handleChange = useCallback(
+    (event: ChangeEvent<HTMLTextAreaElement>) => {
+      setInputValue(event.currentTarget.value);
+    },
+    [],
+  );
+
+  const handleKeyDown = useCallback(
+    (event: KeyboardEvent<HTMLTextAreaElement>) => {
+      if (event.key === "Escape") {
+        setInputValue(submitValue);
+      } else if (event.key === "Enter" && !isMultiline) {
+        event.preventDefault();
+        event.currentTarget.blur();
+      }
+    },
+    [submitValue, isMultiline],
+  );
+
+  return (
+    <EditableTextRoot {...props} ref={ref} data-value={`${inputValue}\u00A0`}>
+      <EditableTextArea
+        value={inputValue}
+        placeholder={placeholder}
+        data-testid={dataTestId}
+        onBlur={handleBlur}
+        onChange={handleChange}
+        onKeyDown={handleKeyDown}
+      />
+    </EditableTextRoot>
+  );
+});
+
+export default EditableText;
diff --git a/frontend/src/metabase/query_builder/components/EditableText/index.ts b/frontend/src/metabase/core/components/EditableText/index.ts
similarity index 100%
rename from frontend/src/metabase/query_builder/components/EditableText/index.ts
rename to frontend/src/metabase/core/components/EditableText/index.ts
diff --git a/frontend/src/metabase/query_builder/components/EditableText/EditableText.stories.tsx b/frontend/src/metabase/query_builder/components/EditableText/EditableText.stories.tsx
deleted file mode 100644
index 26bf85ca73161a7a059039060e2159da7e77cf10..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/query_builder/components/EditableText/EditableText.stories.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import React from "react";
-import { ComponentStory } from "@storybook/react";
-import { useArgs } from "@storybook/client-api";
-import EditableText from "./EditableText";
-
-export default {
-  title: "Query Builder/EditableText",
-  component: EditableText,
-};
-
-const Template: ComponentStory<typeof EditableText> = args => {
-  const [{ initialValue }, updateArgs] = useArgs();
-  const handleChange = (value: string | null | undefined) =>
-    updateArgs({ initialValue: value });
-
-  console.log(initialValue);
-
-  return <EditableText initialValue={initialValue} onChange={handleChange} />;
-};
-
-export const Default = Template.bind({});
-Default.args = {
-  initialValue:
-    "Users with their LTV, Source, and State. Number of new saved questions the last 12 weeks by the method used to create it: GUI or SQL",
-};
diff --git a/frontend/src/metabase/query_builder/components/EditableText/EditableText.styled.tsx b/frontend/src/metabase/query_builder/components/EditableText/EditableText.styled.tsx
deleted file mode 100644
index e5c86366b38dd8947b65f17e752b52996cb39076..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/query_builder/components/EditableText/EditableText.styled.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import styled from "@emotion/styled";
-import { color } from "metabase/lib/colors";
-import { css } from "@emotion/react";
-
-export const SharedStyles = css`
-  border: 1px solid transparent;
-  border-radius: 0.25rem;
-  padding: 0.5rem;
-  grid-area: 1 / 1 / 2 / 2;
-  font-size: 0.875rem;
-  line-height: 1.25rem;
-  min-height: 0;
-  color: ${color("text-dark")};
-`;
-
-interface EditableTextRootProps {
-  value: string | null;
-}
-
-export const EditableTextRoot = styled.div<EditableTextRootProps>`
-  display: grid;
-  max-width: 500px;
-
-  &:after {
-    content: "${props => props.value?.replace(/\n/g, "\\00000a")} ";
-    white-space: pre-wrap;
-    visibility: hidden;
-    ${SharedStyles}
-  }
-`;
-
-export const EditableTextArea = styled.textarea`
-  resize: none;
-  overflow: hidden;
-  cursor: pointer;
-  outline: none;
-  &:hover,
-  &:focus {
-    border: 1px solid ${color("border")};
-  }
-
-  &:focus {
-    cursor: text;
-  }
-
-  ${SharedStyles}
-`;
diff --git a/frontend/src/metabase/query_builder/components/EditableText/EditableText.tsx b/frontend/src/metabase/query_builder/components/EditableText/EditableText.tsx
deleted file mode 100644
index 7a7c01d3dbb65e4750153e47eb70f11374b5a8be..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/query_builder/components/EditableText/EditableText.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import React, { useState, useRef } from "react";
-import { KEY_ESCAPE, KEY_ENTER } from "metabase/lib/keyboard";
-
-import {
-  EditableTextRoot,
-  EditableTextArea,
-  SharedStyles,
-} from "./EditableText.styled";
-
-interface EditableTextProps {
-  initialValue: string | null;
-  onChange?: (val: string | null) => void;
-  submitOnEnter?: boolean;
-  "data-testid"?: string;
-  placeholder?: string;
-}
-
-const EditableText = ({
-  initialValue,
-  onChange,
-  submitOnEnter,
-  "data-testid": dataTestid,
-  placeholder,
-}: EditableTextProps) => {
-  const [value, setValue] = useState<string | null>(initialValue);
-  const textArea = useRef<HTMLTextAreaElement>(null);
-
-  const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
-    setValue(e.target.value);
-  };
-
-  const handleBlur = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
-    const {
-      target: { value },
-    } = e;
-
-    if (onChange && value !== initialValue) {
-      onChange(value);
-    }
-  };
-
-  const handleKeyDown = (e: React.KeyboardEvent) => {
-    if (e.key === KEY_ESCAPE) {
-      setValue(initialValue);
-    }
-    if (e.key === KEY_ENTER && submitOnEnter) {
-      textArea.current?.blur();
-      e.preventDefault();
-    }
-  };
-
-  return (
-    <EditableTextRoot value={value}>
-      <EditableTextArea
-        placeholder={placeholder}
-        value={value || ""}
-        onChange={handleChange}
-        onBlur={handleBlur}
-        onKeyDown={handleKeyDown}
-        rows={1}
-        cols={1}
-        ref={textArea}
-        data-testid={dataTestid}
-      />
-    </EditableTextRoot>
-  );
-};
-
-export default Object.assign(EditableText, {
-  SharedStyles,
-  Root: EditableTextRoot,
-  TextArea: EditableTextArea,
-});
diff --git a/frontend/src/metabase/query_builder/components/SavedQuestionHeaderButton/SavedQuestionHeaderButton.jsx b/frontend/src/metabase/query_builder/components/SavedQuestionHeaderButton/SavedQuestionHeaderButton.jsx
index 5b9d16eb9d2598b051a311550813bd6f87987f60..7cb81fa7f5a1fd1ba7f43bbd9169ec7f6a5b1d0e 100644
--- a/frontend/src/metabase/query_builder/components/SavedQuestionHeaderButton/SavedQuestionHeaderButton.jsx
+++ b/frontend/src/metabase/query_builder/components/SavedQuestionHeaderButton/SavedQuestionHeaderButton.jsx
@@ -1,15 +1,12 @@
 import React from "react";
 import { t } from "ttag";
 import PropTypes from "prop-types";
-
 import { PLUGIN_MODERATION } from "metabase/plugins";
-
 import { color } from "metabase/lib/colors";
-
-import EditableText from "../EditableText";
 import {
   HeaderRoot,
   HeaderReviewIcon,
+  HeaderTitle,
 } from "./SavedQuestionHeaderButton.styled";
 
 SavedQuestionHeaderButton.propTypes = {
@@ -28,11 +25,10 @@ function SavedQuestionHeaderButton({ className, question, onSave }) {
 
   return (
     <HeaderRoot>
-      <EditableText
+      <HeaderTitle
         initialValue={question.displayName()}
-        onChange={onSave}
-        submitOnEnter
         placeholder={t`A nice title`}
+        onChange={onSave}
         data-testid="saved-question-header-title"
       />
       {reviewIconName && (
diff --git a/frontend/src/metabase/query_builder/components/SavedQuestionHeaderButton/SavedQuestionHeaderButton.styled.jsx b/frontend/src/metabase/query_builder/components/SavedQuestionHeaderButton/SavedQuestionHeaderButton.styled.jsx
index 471d21bf6e890c4d510d2dbbd130b16bfa3140da..518bd98b920558de19a3eb8ba8ce08e082332ab3 100644
--- a/frontend/src/metabase/query_builder/components/SavedQuestionHeaderButton/SavedQuestionHeaderButton.styled.jsx
+++ b/frontend/src/metabase/query_builder/components/SavedQuestionHeaderButton/SavedQuestionHeaderButton.styled.jsx
@@ -1,20 +1,18 @@
 import styled from "@emotion/styled";
-
-import EditableText from "../EditableText/EditableText";
 import Icon from "metabase/components/Icon";
+import EditableText from "metabase/core/components/EditableText";
 
 export const HeaderRoot = styled.div`
-  ${EditableText.Root}:after, ${EditableText.TextArea} {
-    font-size: 1.25rem;
-    font-weight: 700;
-    line-height: 1.5rem;
-    padding: 0.25rem;
-  }
-
   display: flex;
   align-items: center;
 `;
 
+export const HeaderTitle = styled(EditableText)`
+  font-size: 1.25rem;
+  font-weight: 700;
+  line-height: 1.5rem;
+`;
+
 export const HeaderReviewIcon = styled(Icon)`
   padding-left: 0.25rem;
 `;
diff --git a/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar.tsx b/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar.tsx
index daed291534026f153f6bd2f177ce3e55c19a5e0b..dca8218cf438910ab007ef7ae0c3adb11ce00e6b 100644
--- a/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar.tsx
+++ b/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar.tsx
@@ -1,4 +1,5 @@
 import React from "react";
+import { t } from "ttag";
 
 import {
   PLUGIN_MODERATION,
@@ -13,7 +14,7 @@ import QuestionActivityTimeline from "metabase/query_builder/components/Question
 import Question from "metabase-lib/lib/Question";
 import { Card } from "metabase-types/types/Card";
 
-import EditableText from "../../EditableText";
+import EditableText from "metabase/core/components/EditableText";
 import { Root, ContentSection } from "./QuestionInfoSidebar.styled";
 
 interface QuestionInfoSidebarProps {
@@ -49,8 +50,9 @@ export const QuestionInfoSidebar = ({
       <ContentSection>
         <EditableText
           initialValue={description}
+          placeholder={t`Description`}
+          isMultiline
           onChange={handleSave}
-          placeholder="Description"
         />
         <PLUGIN_MODERATION.QuestionModerationSection question={question} />
       </ContentSection>