diff --git a/frontend/package.json b/frontend/package.json
index dcd0689547baf2f356f505b388e53a51c99f60c4..ff501b7b9bf8f3976cfb8bf4e1120e611e89a71f 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -4,33 +4,34 @@
   "private": true,
   "proxy": "http://localhost:8080",
   "dependencies": {
+    "@matejmazur/react-katex": "^3.0.2",
     "@testing-library/jest-dom": "^4.2.4",
     "@testing-library/react": "^9.3.2",
     "@testing-library/user-event": "^7.1.2",
     "@types/jest": "^24.0.0",
+    "@types/lodash": "^4.14.106",
     "@types/node": "^12.0.0",
+    "@types/pdfjs-dist": "^2.0.0",
     "@types/react": "^16.9.0",
     "@types/react-dom": "^16.9.0",
-    "react": "^16.12.0",
-    "react-dom": "^16.12.0",
-    "react-scripts": "3.4.0",
-    "typescript": "~3.7.2",
-    "@matejmazur/react-katex": "^3.0.2",
-    "@types/lodash": "^4.14.106",
-    "@types/pdfjs-dist": "^2.0.0",
     "@types/react-router-dom": "^5.1.3",
     "@types/react-syntax-highlighter": "^10.1.0",
+    "emotion": "^10.0.27",
     "glamor": "^2.20.40",
     "katex": "^0.10.0",
     "lodash": "^4.17.5",
     "moment": "^2.22.2",
     "pdfjs-dist": "^2.0.489",
+    "prettier": "^1.18.2",
+    "react": "^16.12.0",
+    "react-dom": "^16.12.0",
     "react-feather": "^2.0.3",
     "react-markdown": "^4.0.3",
     "react-router-dom": "^5.1.2",
+    "react-scripts": "3.4.0",
     "react-syntax-highlighter": "^10.2.1",
     "remark-math": "^1.0.5",
-    "prettier": "^1.18.2",
+    "typescript": "~3.7.2",
     "worker-loader": "^2.0.0"
   },
   "scripts": {
diff --git a/frontend/src/components/Editor/BasicEditor.tsx b/frontend/src/components/Editor/BasicEditor.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..014aec0e528e52f61bcaa87387f4db0586eae754
--- /dev/null
+++ b/frontend/src/components/Editor/BasicEditor.tsx
@@ -0,0 +1,121 @@
+import * as React from "react";
+import { useRef, useCallback, useEffect } from "react";
+import { css, cx } from "emotion";
+import { Range } from "./utils/types";
+
+const wrapperStyle = css`
+  position: relative;
+`;
+const commonStyle = css`
+  font-family: "Fira Code", monospace;
+  font-size: 14px;
+  white-space: pre-wrap;
+  word-wrap: break-word;
+  width: 100%;
+  box-sizing: border-box;
+  padding: 0;
+  margin: 0;
+`;
+const textareaStyle = css`
+  position: absolute;
+  top: 0;
+  color: black;
+  caret-color: inherit;
+  background: transparent;
+  resize: none;
+  border: none;
+  &:focus {
+    outline: none;
+  }
+`;
+const preStyle = css`
+  user-select: none;
+  color: transparent;
+`;
+
+interface Props {
+  value: string;
+  onChange: (newValue: string) => void;
+
+  getSelectionRangeRef: React.RefObject<() => Range | undefined>;
+  setSelectionRangeRef: React.RefObject<(newSelection: Range) => void>;
+
+  onMetaKey: (str: string, shift: boolean) => boolean;
+}
+const BasicEditor: React.FC<Props> = ({
+  value,
+  onChange,
+  getSelectionRangeRef,
+  setSelectionRangeRef,
+  onMetaKey,
+}) => {
+  const textareaElRef = useRef<HTMLTextAreaElement>(null);
+  const preElRef = useRef<HTMLPreElement>(null);
+
+  // tslint:disable-next-line: no-any
+  (getSelectionRangeRef as any).current = () => {
+    const textarea = textareaElRef.current;
+    if (textarea === null) return;
+    return {
+      start: textarea.selectionStart,
+      end: textarea.selectionEnd,
+    };
+  };
+
+  // tslint:disable-next-line: no-any
+  (setSelectionRangeRef as any).current = (newSelection: Range) => {
+    const textarea = textareaElRef.current;
+    if (textarea === null) return;
+    setTimeout(() => {
+      textarea.selectionStart = newSelection.start;
+      textarea.selectionEnd = newSelection.end;
+    }, 0);
+  };
+
+  const onTextareaChange = useCallback(
+    e => {
+      const newContent = e.currentTarget.value;
+      onChange(newContent);
+    },
+    [onChange],
+  );
+
+  const onTextareaKeyDown = useCallback(
+    (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
+      if (e.ctrlKey || e.metaKey) {
+        if (onMetaKey(e.key.toLowerCase(), e.shiftKey)) {
+          e.preventDefault();
+        }
+      }
+    },
+    [onMetaKey],
+  );
+
+  const onResize = useCallback(() => {
+    const textareaEl = textareaElRef.current;
+    if (textareaEl === null) return;
+    const preEl = preElRef.current;
+    if (preEl === null) return;
+    textareaEl.style.height = `${preEl.clientHeight}px`;
+  }, []);
+
+  useEffect(() => {
+    onResize();
+  }, [value, onResize]);
+
+  return (
+    <div className={wrapperStyle}>
+      <pre ref={preElRef} className={cx(commonStyle, preStyle)}>
+        {value + "\n"}
+      </pre>
+      <textarea
+        value={value}
+        onChange={onTextareaChange}
+        onKeyDown={onTextareaKeyDown}
+        ref={textareaElRef}
+        className={cx(commonStyle, textareaStyle)}
+      />
+    </div>
+  );
+};
+export default BasicEditor;
diff --git a/frontend/src/components/Editor/Container.tsx b/frontend/src/components/Editor/Container.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..759b5aa6fe2f90dd8adf87aa8ec6c24c15eeea40
--- /dev/null
+++ b/frontend/src/components/Editor/Container.tsx
@@ -0,0 +1,14 @@
+import * as React from "react";
+import { css } from "emotion";
+const containerStyle = css`
+  width: 100%;
+  max-width: 600px;
+  margin: auto;
+  padding: 1em;
+  box-sizing: border-box;
+`;
+
+const Container: React.FC<{}> = ({ children }) => {
+  return <div className={containerStyle}>{children}</div>;
+};
+export default Container;
diff --git a/frontend/src/components/Editor/Dropzone.tsx b/frontend/src/components/Editor/Dropzone.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..be3e6d3c5bf52e90e25a65cd0014828c6669196c
--- /dev/null
+++ b/frontend/src/components/Editor/Dropzone.tsx
@@ -0,0 +1,64 @@
+import * as React from "react";
+import { css } from "emotion";
+import { useCallback } from "react";
+
+const dropZoneStyle = css`
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+`;
+
+interface Props {
+  onDragLeave: () => void;
+  onDrop: (files: File[]) => void;
+}
+
+const DropZone: React.FC<Props> = ({ onDragLeave, onDrop }) => {
+  const onDragLeaveHandler = useCallback(
+    (e: React.DragEvent<HTMLDivElement>) => {
+      e.preventDefault();
+      e.stopPropagation();
+      onDragLeave();
+    },
+    [onDragLeave],
+  );
+  const onDropHandler = useCallback(
+    (e: React.DragEvent<HTMLDivElement>) => {
+      e.preventDefault();
+      e.stopPropagation();
+      onDragLeave();
+      const items = e.dataTransfer.items;
+      if (items === undefined) return;
+      const files: File[] = [];
+      for (let i = 0; i < items.length; i++) {
+        const item = items.item ? items.item(i) : items[i];
+        if (item.kind !== "file") continue;
+        const file = item.getAsFile();
+        if (file === null) continue;
+        files.push(file);
+      }
+      if (files.length > 0) {
+        onDrop(files);
+      }
+    },
+    [onDrop, onDragLeave],
+  );
+  const onDragOverHandler = useCallback(
+    (e: React.DragEvent<HTMLDivElement>) => {
+      e.preventDefault();
+      e.stopPropagation();
+    },
+    [],
+  );
+  return (
+    <div
+      className={dropZoneStyle}
+      onDragLeave={onDragLeaveHandler}
+      onDrop={onDropHandler}
+      onDragOver={onDragOverHandler}
+    />
+  );
+};
+export default DropZone;
diff --git a/frontend/src/components/Editor/EditorFooter.tsx b/frontend/src/components/Editor/EditorFooter.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e953f119407ac49fa2d873288f0262552ac51c8a
--- /dev/null
+++ b/frontend/src/components/Editor/EditorFooter.tsx
@@ -0,0 +1,111 @@
+import * as React from "react";
+import { css } from "emotion";
+import { useRef, useCallback } from "react";
+import { Image as ImageIcon, Plus } from "react-feather";
+import { ImageHandle } from "./utils/types";
+
+const addImageIconStyle = css`
+  margin: 0;
+  border: none;
+  cursor: pointer;
+  background-color: transparent;
+  padding: 6px;
+`;
+const footerStyle = css`
+  border-top: 1px solid rgba(0, 0, 0, 0.1);
+`;
+const rowStyle = css`
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+`;
+const spacer = css`
+  flex-grow: 1;
+`;
+const fileInputStyle = css`
+  visibility: hidden;
+  display: none;
+`;
+const addImageButtonStyle = css`
+  border: none;
+  background-color: transparent;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  color: rgba(0, 0, 0, 0.4);
+  transition: color 0.1s;
+  &:hover {
+    color: rgba(0, 0, 0, 0.8);
+  }
+`;
+const addImageTextStyle = css`
+  font-size: 12px;
+`;
+interface Props {
+  onFiles: (files: File[]) => void;
+  onOpenOverlay: () => void;
+  attachments: ImageHandle[];
+  onDelete: (handle: ImageHandle) => void;
+}
+const EditorFooter: React.FC<Props> = ({
+  onFiles,
+  attachments,
+  onDelete,
+  onOpenOverlay,
+}) => {
+  const iconSize = 15;
+  const fileInputRef = useRef<HTMLInputElement>(null);
+
+  const onFile = useCallback(() => {
+    const fileInput = fileInputRef.current;
+    if (fileInput === null) return;
+    fileInput.click();
+  }, []);
+  const onChangeHandler = useCallback(
+    (e: React.ChangeEvent<HTMLInputElement>) => {
+      const fileInput = fileInputRef.current;
+      if (fileInput === null) return;
+      const fileList = fileInput.files;
+      if (fileList === null) return;
+      const files: File[] = [];
+      for (let i = 0; i < fileList.length; i++) {
+        const file = fileList.item(i);
+        if (file === null) continue;
+        files.push(file);
+      }
+      onFiles(files);
+      fileInput.value = "";
+    },
+    [onFiles],
+  );
+  return (
+    <div className={footerStyle}>
+      <div className={rowStyle}>
+        <div className={spacer} />
+        <button onClick={onOpenOverlay} className={addImageButtonStyle}>
+          <div className={addImageIconStyle}>
+            <ImageIcon size={iconSize} />
+          </div>
+          <div className={addImageTextStyle}>Browse Images</div>
+        </button>
+        <button onClick={onFile} className={addImageButtonStyle}>
+          <div className={addImageIconStyle}>
+            <Plus size={iconSize} />
+          </div>
+          <div className={addImageTextStyle}>Add Image</div>
+        </button>
+        <input
+          type="file"
+          className={fileInputStyle}
+          ref={fileInputRef}
+          onChange={onChangeHandler}
+        />
+      </div>
+      <small>
+        You can use Markdown. Use ``` code ``` for code. Use $ math $ or $$ \n
+        math \n $$ for latex math.
+      </small>
+    </div>
+  );
+};
+export default EditorFooter;
diff --git a/frontend/src/components/Editor/EditorHeader.tsx b/frontend/src/components/Editor/EditorHeader.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6805c201ed1b5e7dd0f1846d46e4f530c7f4298f
--- /dev/null
+++ b/frontend/src/components/Editor/EditorHeader.tsx
@@ -0,0 +1,84 @@
+import { EditorMode } from "./utils/types";
+import * as React from "react";
+import TabBar from "./TabBar";
+import { css } from "emotion";
+import { Bold, Italic, Link, Code, DollarSign } from "react-feather";
+
+const iconButtonStyle = css`
+  margin: 0;
+  border: none;
+  cursor: pointer;
+  background-color: transparent;
+  padding: 6px;
+  color: rgba(0, 0, 0, 0.4);
+  transition: color 0.1s;
+  &:hover {
+    color: rgba(0, 0, 0, 0.8);
+  }
+`;
+const headerStyle = css`
+  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+  display: flex;
+  flex-direction: row;
+  align-items: flex-end;
+`;
+const spacer = css`
+  flex-grow: 1;
+`;
+
+interface Props {
+  activeMode: EditorMode;
+  onActiveModeChange: (newMode: EditorMode) => void;
+
+  onMathClick: () => void;
+  onCodeClick: () => void;
+  onLinkClick: () => void;
+  onItalicClick: () => void;
+  onBoldClick: () => void;
+}
+const EditorHeader: React.FC<Props> = ({
+  activeMode,
+  onActiveModeChange,
+  ...handlers
+}) => {
+  const iconSize = 15;
+  return (
+    <div className={headerStyle}>
+      <TabBar
+        items={[
+          {
+            title: "Write",
+            active: activeMode === "write",
+            onClick: () => onActiveModeChange("write"),
+          },
+          {
+            title: "Preview",
+            active: activeMode === "preview",
+            onClick: () => onActiveModeChange("preview"),
+          },
+        ]}
+      />
+      <div className={spacer} />
+      {activeMode === "write" && (
+        <>
+          <button className={iconButtonStyle} onClick={handlers.onMathClick}>
+            <DollarSign size={iconSize} />
+          </button>
+          <button className={iconButtonStyle} onClick={handlers.onCodeClick}>
+            <Code size={iconSize} />
+          </button>
+          <button className={iconButtonStyle} onClick={handlers.onLinkClick}>
+            <Link size={iconSize} />
+          </button>
+          <button className={iconButtonStyle} onClick={handlers.onItalicClick}>
+            <Italic size={iconSize} />
+          </button>
+          <button className={iconButtonStyle} onClick={handlers.onBoldClick}>
+            <Bold size={iconSize} />
+          </button>
+        </>
+      )}
+    </div>
+  );
+};
+export default EditorHeader;
diff --git a/frontend/src/components/Editor/TabBar.tsx b/frontend/src/components/Editor/TabBar.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d6b4acb1779555707ae40ec6fac3f1a713e33113
--- /dev/null
+++ b/frontend/src/components/Editor/TabBar.tsx
@@ -0,0 +1,57 @@
+import * as React from "react";
+import { css, cx } from "emotion";
+
+const buttonStyle = css`
+  border: none;
+  background-color: transparent;
+  cursor: pointer;
+  padding: 0.4em;
+  font-weight: 400;
+  font-size: 14px;
+  margin: 0;
+  color: rgba(0, 0, 0, 0.5);
+  transition: color 0.1s, border-bottom 0.1s;
+  border-radius: 0;
+  &:hover {
+    background-color: transparent;
+  }
+`;
+const inactiveButtonStyle = css`
+  border-bottom: 1px solid transparent;
+  &:hover {
+    color: rgba(0, 0, 0, 0.9);
+    border-bottom: 1px solid rgba(0, 0, 0, 0.3);
+  }
+`;
+const activeButtonStyle = css`
+  color: rgba(0, 0, 0, 1);
+  border-bottom: 1px solid #3867d6;
+`;
+
+interface Item {
+  title: string;
+  active: boolean;
+  onClick: () => void;
+}
+interface Props {
+  items: Item[];
+}
+const TabBar: React.FC<Props> = ({ items }) => {
+  return (
+    <div>
+      {items.map(item => (
+        <button
+          key={item.title}
+          onClick={item.onClick}
+          className={cx(
+            buttonStyle,
+            item.active ? activeButtonStyle : inactiveButtonStyle,
+          )}
+        >
+          {item.title}
+        </button>
+      ))}
+    </div>
+  );
+};
+export default TabBar;
diff --git a/frontend/src/components/Editor/index.tsx b/frontend/src/components/Editor/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..01dd6e28e60395595235a215172ca64995af4672
--- /dev/null
+++ b/frontend/src/components/Editor/index.tsx
@@ -0,0 +1,275 @@
+import * as React from "react";
+import { useCallback, useState, useRef } from "react";
+import { css, cx } from "emotion";
+import { Range, EditorMode, ImageHandle } from "./utils/types";
+import BasicEditor from "./BasicEditor";
+import EditorHeader from "./EditorHeader";
+import DropZone from "./Dropzone";
+import EditorFooter from "./EditorFooter";
+import ImageOverlay from "../image-overlay";
+import { UndoStack, push, undo, redo } from "./utils/undo-stack";
+
+const editorWrapperStyle = css`
+  padding: 1.2em;
+`;
+
+const wrapperStyle = css`
+  position: relative;
+  border: 1px solid transparent;
+`;
+const dragHoveredWrapperStyle = css`
+  border: 1px solid #fed330;
+  border-radius: 3px;
+`;
+interface Props {
+  value: string;
+  onChange: (newValue: string) => void;
+  imageHandler: (file: File) => Promise<ImageHandle>;
+  preview: (str: string) => React.ReactNode;
+
+  undoStack: UndoStack;
+  setUndoStack: (newStack: UndoStack) => void;
+}
+const Editor: React.FC<Props> = ({
+  value,
+  onChange,
+  imageHandler,
+  preview,
+  undoStack,
+  setUndoStack,
+}) => {
+  const [mode, setMode] = useState<EditorMode>("write");
+  const [isDragHovered, setIsDragHovered] = useState(false);
+  const [attachments, setAttachments] = useState<ImageHandle[]>([]);
+  const [overlayOpen, setOverlayOpen] = useState(false);
+
+  const setCurrent = useCallback(
+    (newValue: string, newSelection?: Range) => {
+      if (newSelection) setSelectionRangeRef.current(newSelection);
+      onChange(newValue);
+      const selection = getSelectionRangeRef.current();
+      if (selection === undefined) return;
+      const newStack = push(undoStack, value, selection);
+      setUndoStack(newStack);
+    },
+    [undoStack, setUndoStack, onChange, value],
+  );
+
+  const setSelectionRangeRef = useRef<(newSelection: Range) => void>(
+    (a: Range) => undefined,
+  );
+  const getSelectionRangeRef = useRef<() => Range | undefined>(() => ({
+    start: 0,
+    end: 0,
+  }));
+
+  const insertImage = useCallback(
+    (handle: ImageHandle) => {
+      const selection = getSelectionRangeRef.current();
+      if (selection === undefined) return;
+      const before = value.substring(0, selection.start);
+      const content = value.substring(selection.start, selection.end);
+      const after = value.substring(selection.end);
+      const newContent = "![" + content + `](${handle.src})`;
+      const newSelection = {
+        start: selection.start + 2,
+        end: selection.start + content.length + 2,
+      };
+      setCurrent(before + newContent + after, newSelection);
+    },
+    [setCurrent, value],
+  );
+
+  const insertLink = useCallback(() => {
+    const selection = getSelectionRangeRef.current();
+    if (selection === undefined) return;
+    const before = value.substring(0, selection.start);
+    const content = value.substring(selection.start, selection.end);
+    const after = value.substring(selection.end);
+    const newContent = "[" + content + "](https://www.example.com)";
+    const newSelection = {
+      start: selection.start + content.length + 3,
+      end: selection.start + newContent.length - 1,
+    };
+    setCurrent(before + newContent + after, newSelection);
+  }, [setCurrent, value]);
+
+  const wrapSelection = useCallback(
+    (str: string) => {
+      const selection = getSelectionRangeRef.current();
+      if (selection === undefined) return;
+      const before = value.substring(0, selection.start);
+      const content = value.substring(selection.start, selection.end);
+      const after = value.substring(selection.end);
+      const newContent = str + content + str;
+
+      if (content.length === 0) {
+        const newSelection = {
+          start: selection.start + str.length,
+          end: selection.end + str.length,
+        };
+        setCurrent(before + newContent + after, newSelection);
+      } else {
+        const newSelection = {
+          start: selection.start,
+          end: selection.end + newContent.length - content.length,
+        };
+        setCurrent(before + newContent + after, newSelection);
+      }
+    },
+    [setCurrent, value],
+  );
+
+  const onMathClick = useCallback(() => {
+    wrapSelection("$");
+  }, [wrapSelection]);
+
+  const onCodeClick = useCallback(() => {
+    wrapSelection("`");
+  }, [wrapSelection]);
+
+  const onLinkClick = useCallback(() => {
+    insertLink();
+  }, [insertLink]);
+
+  const onItalicClick = useCallback(() => {
+    wrapSelection("*");
+  }, [wrapSelection]);
+
+  const onBoldClick = useCallback(() => {
+    wrapSelection("**");
+  }, [wrapSelection]);
+
+  const onMetaKey = useCallback(
+    (key: string, shift: boolean) => {
+      if (key.toLowerCase() === "b") {
+        onBoldClick();
+        return true;
+      } else if (key.toLowerCase() === "i") {
+        onItalicClick();
+        return true;
+      } else if (key === "z" && !shift) {
+        if (undoStack.prev.length > 0) {
+          const selection = getSelectionRangeRef.current();
+          if (selection === undefined) return true;
+          const [newState, newStack] = undo(undoStack, {
+            value: value,
+            selection,
+            time: new Date(),
+          });
+          setUndoStack(newStack);
+          onChange(newState.value);
+          setSelectionRangeRef.current(newState.selection);
+        }
+        return true;
+      } else if (key === "z" && shift) {
+        if (undoStack.next.length > 0) {
+          const selection = getSelectionRangeRef.current();
+          if (selection === undefined) return true;
+          const [newState, newStack] = redo(undoStack, {
+            value: value,
+            selection,
+            time: new Date(),
+          });
+          setUndoStack(newStack);
+          onChange(newState.value);
+          setSelectionRangeRef.current(newState.selection);
+        }
+        return true;
+      }
+      return false;
+    },
+    [onBoldClick, onItalicClick, onChange, setUndoStack, undoStack, value],
+  );
+
+  const onDragEnter = useCallback(() => {
+    setIsDragHovered(true);
+  }, []);
+
+  const onDragLeave = useCallback(() => {
+    setIsDragHovered(false);
+  }, []);
+
+  const onFile = useCallback(
+    async (file: File) => {
+      const handle = await imageHandler(file);
+      setAttachments(a => [...a, handle]);
+      insertImage(handle);
+    },
+    [imageHandler, insertImage],
+  );
+
+  const onFiles = useCallback(
+    (files: File[]) => {
+      for (const file of files) {
+        onFile(file);
+      }
+    },
+    [onFile],
+  );
+
+  const onDeleteAttachment = useCallback(async (handle: ImageHandle) => {
+    await handle.remove();
+    setAttachments(a => a.filter(h => h !== handle));
+  }, []);
+
+  const onImageDialogClose = useCallback(
+    (image: string) => {
+      setOverlayOpen(false);
+      if (image.length === 0) return;
+      insertImage({
+        name: image,
+        src: image,
+        remove: () => Promise.resolve(),
+      });
+    },
+    [insertImage],
+  );
+
+  const onOpenOverlay = useCallback(() => {
+    setOverlayOpen(true);
+  }, []);
+
+  return (
+    <>
+      <div
+        className={cx(wrapperStyle, isDragHovered && dragHoveredWrapperStyle)}
+        onDragEnter={onDragEnter}
+      >
+        <EditorHeader
+          activeMode={mode}
+          onActiveModeChange={setMode}
+          onMathClick={onMathClick}
+          onCodeClick={onCodeClick}
+          onLinkClick={onLinkClick}
+          onItalicClick={onItalicClick}
+          onBoldClick={onBoldClick}
+        />
+        <div className={editorWrapperStyle}>
+          {mode === "write" ? (
+            <BasicEditor
+              value={value}
+              onChange={newValue => setCurrent(newValue)}
+              setSelectionRangeRef={setSelectionRangeRef}
+              getSelectionRangeRef={getSelectionRangeRef}
+              onMetaKey={onMetaKey}
+            />
+          ) : (
+            preview(value)
+          )}
+        </div>
+        <EditorFooter
+          onFiles={onFiles}
+          attachments={attachments}
+          onDelete={onDeleteAttachment}
+          onOpenOverlay={onOpenOverlay}
+        />
+        {isDragHovered && (
+          <DropZone onDragLeave={onDragLeave} onDrop={onFiles} />
+        )}
+      </div>
+      {overlayOpen && <ImageOverlay onClose={onImageDialogClose} />}
+    </>
+  );
+};
+export default Editor;
diff --git a/frontend/src/components/Editor/utils/types.ts b/frontend/src/components/Editor/utils/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0db2f012bf2ae4125cf9d1dde97bf7bb4595d7e1
--- /dev/null
+++ b/frontend/src/components/Editor/utils/types.ts
@@ -0,0 +1,10 @@
+export interface Range {
+  start: number;
+  end: number;
+}
+export interface ImageHandle {
+  name: string;
+  src: string;
+  remove: () => Promise<void>;
+}
+export type EditorMode = "write" | "preview";
diff --git a/frontend/src/components/Editor/utils/undo-stack.ts b/frontend/src/components/Editor/utils/undo-stack.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e07b400cb740f0dd5e7898f8943d4f9bb10ece77
--- /dev/null
+++ b/frontend/src/components/Editor/utils/undo-stack.ts
@@ -0,0 +1,73 @@
+import { Range } from "./types";
+
+export interface UndoState {
+  value: string;
+  selection: Range;
+  time: Date;
+}
+export interface UndoStack {
+  prev: UndoState[];
+  next: UndoState[];
+}
+/**
+ * Determines if the two `UndoState` instances `a` and `b` can be merged.
+ * Currently only states can be merged where at most a single line was
+ * changed and in that line only a single word was appended.
+ * @param a
+ * @param b
+ */
+const canBeMerged = (a: UndoState, b: UndoState) => {
+  const timeDiff = Math.abs(a.time.getTime() - b.time.getTime());
+  if (timeDiff > 10000) return false;
+  const aLines = a.value.split(/[\r\n]+/);
+  const bLines = b.value.split(/[\r\n]+/);
+  if (aLines.length !== bLines.length) return false;
+  let changeLine = -1;
+  for (let i = 0; i < aLines.length; i++) {
+    if (aLines[i] === bLines[i]) continue;
+    if (changeLine !== -1) return false;
+    changeLine = i;
+  }
+  if (changeLine === -1) return true;
+  const aLine = aLines[changeLine];
+  const bLine = bLines[changeLine];
+  const [baseContent, newContent] =
+    aLine.length < bLine.length ? [aLine, bLine] : [bLine, aLine];
+  if (newContent.indexOf(baseContent) !== 0) return false;
+  const diff = newContent.substring(baseContent.length);
+  const words = diff.split(/\b/);
+  const res = words.length <= 1;
+  return res;
+};
+export const push = (prevStack: UndoStack, value: string, selection: Range) =>
+  prevStack.prev.length > 0 &&
+  canBeMerged(prevStack.prev[prevStack.prev.length - 1], {
+    value,
+    selection,
+    time: new Date(),
+  })
+    ? {
+        prev: prevStack.prev,
+        next: [],
+      }
+    : {
+        prev: [...prevStack.prev, { value, selection, time: new Date() }],
+        next: [],
+      };
+export const undo = (prevStack: UndoStack, currentState: UndoState) =>
+  [
+    prevStack.prev[prevStack.prev.length - 1],
+    {
+      prev: prevStack.prev.slice(0, -1),
+      next: [...prevStack.next, currentState].slice(-100),
+    },
+  ] as [UndoState, UndoStack];
+
+export const redo = (prevStack: UndoStack, currentState: UndoState) =>
+  [
+    prevStack.next[prevStack.next.length - 1],
+    {
+      prev: [...prevStack.prev, currentState].slice(-100),
+      next: prevStack.next.slice(0, -1),
+    },
+  ] as [UndoState, UndoStack];
diff --git a/frontend/src/components/answer.tsx b/frontend/src/components/answer.tsx
index 3dc42971f9e7d7a803bfc4f94c80e4a9a7f15edf..925248cff73108066faac2694f4b5c1fa39a673e 100644
--- a/frontend/src/components/answer.tsx
+++ b/frontend/src/components/answer.tsx
@@ -4,14 +4,14 @@ import moment from "moment";
 import Comment from "./comment";
 import { css } from "glamor";
 import MarkdownText from "./markdown-text";
-import { fetchpost } from "../fetch-utils";
-import ImageOverlay from "./image-overlay";
+import { fetchpost, imageHandler } from "../fetch-utils";
 import Colors from "../colors";
 import { Link } from "react-router-dom";
 import globalcss from "../globalcss";
 import GlobalConsts from "../globalconsts";
 import colors from "../colors";
-import { listenEnter } from "../input-utils";
+import Editor from "./Editor";
+import { UndoStack } from "./Editor/utils/undo-stack";
 
 interface Props {
   isReadonly: boolean;
@@ -27,8 +27,8 @@ interface Props {
 interface State {
   editing: boolean;
   imageDialog: boolean;
-  imageCursorPosition: number;
   text: string;
+  undoStack: UndoStack;
   savedText: string;
   addingComment: boolean;
   allCommentsVisible: boolean;
@@ -129,6 +129,7 @@ const styles = {
     boxSizing: "border-box",
   }),
   actionButtons: css({
+    width: "100%",
     display: "flex",
     justifyContent: "flex-end",
     marginRight: "25px",
@@ -161,11 +162,11 @@ export default class AnswerComponent extends React.Component<Props, State> {
   state: State = {
     editing: this.props.answer.canEdit && this.props.answer.text.length === 0,
     imageDialog: false,
-    imageCursorPosition: -1,
     savedText: this.props.answer.text,
     text: this.props.answer.text,
     allCommentsVisible: false,
     addingComment: false,
+    undoStack: { prev: [], next: [] },
   };
 
   componentDidUpdate(
@@ -222,7 +223,6 @@ export default class AnswerComponent extends React.Component<Props, State> {
   startEdit = () => {
     this.setState({
       editing: true,
-      imageCursorPosition: -1,
     });
   };
 
@@ -232,29 +232,9 @@ export default class AnswerComponent extends React.Component<Props, State> {
     }));
   };
 
-  startImageDialog = () => {
-    this.setState({ imageDialog: true });
-  };
-
-  endImageDialog = (image: string) => {
-    if (image.length > 0) {
-      const imageTag = `![Image Description](${image})`;
-      this.setState(prevState => ({
-        imageDialog: false,
-        text:
-          prevState.text.slice(0, prevState.imageCursorPosition) +
-          imageTag +
-          prevState.text.slice(prevState.imageCursorPosition),
-      }));
-    } else {
-      this.setState({ imageDialog: false });
-    }
-  };
-
-  answerTextareaChange = (event: React.FormEvent<HTMLTextAreaElement>) => {
+  answerTextareaChange = (newValue: string) => {
     this.setState({
-      text: event.currentTarget.value,
-      imageCursorPosition: event.currentTarget.selectionStart,
+      text: newValue,
     });
   };
 
@@ -407,38 +387,25 @@ export default class AnswerComponent extends React.Component<Props, State> {
             )}
           </div>
         </div>
-        <div {...styles.answer}>
-          <MarkdownText value={this.state.text} />
-        </div>
+        {!this.state.editing && (
+          <div {...styles.answer}>
+            <MarkdownText value={this.state.text} />
+          </div>
+        )}
         {this.state.editing && (
           <div>
             <div {...styles.answerInput}>
-              <textarea
-                {...styles.textareaInput}
-                onKeyUp={this.answerTextareaChange}
-                onChange={this.answerTextareaChange}
-                cols={120}
-                rows={20}
+              <Editor
                 value={this.state.text}
-                onKeyPress={listenEnter(this.saveAnswer, true)}
+                onChange={this.answerTextareaChange}
+                imageHandler={imageHandler}
+                preview={str => <MarkdownText value={str} />}
+                undoStack={this.state.undoStack}
+                setUndoStack={undoStack => this.setState({ undoStack })}
               />
             </div>
             <div {...styles.answerTexHint}>
-              <div>
-                <small>
-                  You can use Markdown. Use ``` code ``` for code. Use $ math $
-                  or $$ \n math \n $$ for latex math.
-                </small>
-              </div>
               <div {...styles.actionButtons}>
-                <div {...styles.actionButton} onClick={this.startImageDialog}>
-                  <img
-                    {...styles.actionImg}
-                    src="/static/images.svg"
-                    title="Images"
-                    alt="Images"
-                  />
-                </div>
                 <div {...styles.actionButton} onClick={this.saveAnswer}>
                   <img
                     {...styles.actionImg}
@@ -522,9 +489,6 @@ export default class AnswerComponent extends React.Component<Props, State> {
             )}
           </div>
         )}
-        {this.state.imageDialog && (
-          <ImageOverlay onClose={this.endImageDialog} />
-        )}
 
         {(answer.comments.length > 0 || this.state.addingComment) && (
           <div {...styles.comments}>
diff --git a/frontend/src/components/comment.tsx b/frontend/src/components/comment.tsx
index 24874c6694db43b604511dea8b325039dd709982..ab5db565674856441196eec96fa81c260f1ce124 100644
--- a/frontend/src/components/comment.tsx
+++ b/frontend/src/components/comment.tsx
@@ -3,13 +3,13 @@ import { AnswerSection, Comment } from "../interfaces";
 import moment from "moment";
 import { css } from "glamor";
 import MarkdownText from "./markdown-text";
-import { fetchpost } from "../fetch-utils";
-import ImageOverlay from "./image-overlay";
+import { fetchpost, imageHandler } from "../fetch-utils";
 import { Link } from "react-router-dom";
 import globalcss from "../globalcss";
 import GlobalConsts from "../globalconsts";
-import { listenEnter } from "../input-utils";
 import Colors from "../colors";
+import Editor from "./Editor";
+import { UndoStack } from "./Editor/utils/undo-stack";
 
 interface Props {
   isReadonly: boolean;
@@ -26,8 +26,7 @@ interface State {
   editing: boolean;
   text: string;
   savedText: string;
-  imageDialog: boolean;
-  imageCursorPosition: number;
+  undoStack: UndoStack;
 }
 
 const styles = {
@@ -72,8 +71,7 @@ export default class CommentComponent extends React.Component<Props, State> {
     editing: !!this.props.isNewComment,
     savedText: this.props.comment.text,
     text: this.props.comment.text,
-    imageDialog: false,
-    imageCursorPosition: -1,
+    undoStack: { prev: [], next: [] },
   };
 
   removeComment = () => {
@@ -127,32 +125,12 @@ export default class CommentComponent extends React.Component<Props, State> {
     }
   };
 
-  commentTextareaChange = (event: React.FormEvent<HTMLTextAreaElement>) => {
+  commentTextareaChange = (newValue: string) => {
     this.setState({
-      text: event.currentTarget.value,
-      imageCursorPosition: event.currentTarget.selectionStart,
+      text: newValue,
     });
   };
 
-  startImageDialog = () => {
-    this.setState({ imageDialog: true });
-  };
-
-  endImageDialog = (image: string) => {
-    if (image.length > 0) {
-      const imageTag = `![Image Description](${image})`;
-      this.setState(prevState => ({
-        imageDialog: false,
-        text:
-          prevState.text.slice(0, prevState.imageCursorPosition) +
-          imageTag +
-          prevState.text.slice(prevState.imageCursorPosition),
-      }));
-    } else {
-      this.setState({ imageDialog: false });
-    }
-  };
-
   render() {
     const { comment } = this.props;
     return (
@@ -199,33 +177,26 @@ export default class CommentComponent extends React.Component<Props, State> {
               )}
           </div>
         </div>
-        <div {...styles.comment}>
-          <MarkdownText
-            value={this.state.editing ? this.state.text : comment.text}
-          />
-        </div>
+        {!this.state.editing && (
+          <div {...styles.comment}>
+            <MarkdownText
+              value={this.state.editing ? this.state.text : comment.text}
+            />
+          </div>
+        )}
         {this.state.editing && (
           <div>
             <div>
-              <textarea
-                {...styles.textareaInput}
-                onKeyUp={this.commentTextareaChange}
-                onChange={this.commentTextareaChange}
-                cols={80}
-                rows={5}
+              <Editor
                 value={this.state.text}
-                onKeyPress={listenEnter(this.saveComment, true)}
+                onChange={this.commentTextareaChange}
+                imageHandler={imageHandler}
+                preview={str => <MarkdownText value={str} />}
+                undoStack={this.state.undoStack}
+                setUndoStack={undoStack => this.setState({ undoStack })}
               />
             </div>
             <div {...styles.actionButtons}>
-              <div {...styles.actionButton} onClick={this.startImageDialog}>
-                <img
-                  {...styles.actionImg}
-                  src="/static/images.svg"
-                  title="Images"
-                  alt="Images"
-                />
-              </div>
               <div {...styles.actionButton} onClick={this.saveComment}>
                 <img
                   {...styles.actionImg}
@@ -247,9 +218,6 @@ export default class CommentComponent extends React.Component<Props, State> {
             </div>
           </div>
         )}
-        {this.state.imageDialog && (
-          <ImageOverlay onClose={this.endImageDialog} />
-        )}
       </div>
     );
   }
diff --git a/frontend/src/components/image-overlay.tsx b/frontend/src/components/image-overlay.tsx
index a0ddb9a222988c1908ef51f4aa4d007103d2bf27..4d08fc5ee94015665ad187db276a337504fc75a8 100644
--- a/frontend/src/components/image-overlay.tsx
+++ b/frontend/src/components/image-overlay.tsx
@@ -14,6 +14,7 @@ const styles = {
     bottom: "0",
     paddingTop: "200px",
     paddingBottom: "200px",
+    zIndex: 100,
     "@media (max-height: 799px)": {
       paddingTop: "50px",
       paddingBottom: "50px",
diff --git a/frontend/src/fetch-utils.tsx b/frontend/src/fetch-utils.tsx
index 00415b2582d48f42df097a26de84c763d9e2387e..bda8bca52d259de0d01125f01695e6df1cd4e30f 100644
--- a/frontend/src/fetch-utils.tsx
+++ b/frontend/src/fetch-utils.tsx
@@ -1,3 +1,5 @@
+import { ImageHandle } from "./components/Editor/utils/types";
+
 export async function fetchpost(url: string, data: { [key: string]: any }) {
   const formData = new FormData();
   // Convert the `data` object into a `formData` object by iterating
@@ -47,6 +49,23 @@ export async function fetchapi(url: string) {
   }
 }
 
+export function imageHandler(file: File): Promise<ImageHandle> {
+  return new Promise((resolve, reject) => {
+    fetchpost("/api/uploadimg", {
+      file: file,
+    })
+      .then(res => {
+        resolve({
+          name: file.name,
+          src: res.filename,
+          remove: async () => {
+            await fetchpost(`/api/image/${res.filename}/remove`, {});
+          },
+        });
+      })
+      .catch(e => reject(e));
+  });
+}
 export function getCookie(name: string): string | null {
   let cookieValue = null;
   if (document.cookie && document.cookie !== "") {
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 49a83966d376fb538b16f79d08c5dc765dfa2916..704d66a25f900e988e1c5dedd276087e5d5dce09 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -142,7 +142,7 @@
   dependencies:
     "@babel/types" "^7.8.3"
 
-"@babel/helper-module-imports@^7.8.3":
+"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz#7fe39589b39c016331b6b8c3f441e8f0b1419498"
   dependencies:
@@ -796,6 +796,62 @@
   version "10.1.0"
   resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-10.1.0.tgz#f0950bba18819512d42f7197e56c518aa491cf18"
 
+"@emotion/cache@^10.0.27":
+  version "10.0.29"
+  resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0"
+  integrity sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==
+  dependencies:
+    "@emotion/sheet" "0.9.4"
+    "@emotion/stylis" "0.8.5"
+    "@emotion/utils" "0.11.3"
+    "@emotion/weak-memoize" "0.2.5"
+
+"@emotion/hash@0.8.0":
+  version "0.8.0"
+  resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
+  integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
+
+"@emotion/memoize@0.7.4":
+  version "0.7.4"
+  resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb"
+  integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==
+
+"@emotion/serialize@^0.11.15", "@emotion/serialize@^0.11.16":
+  version "0.11.16"
+  resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.16.tgz#dee05f9e96ad2fb25a5206b6d759b2d1ed3379ad"
+  integrity sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==
+  dependencies:
+    "@emotion/hash" "0.8.0"
+    "@emotion/memoize" "0.7.4"
+    "@emotion/unitless" "0.7.5"
+    "@emotion/utils" "0.11.3"
+    csstype "^2.5.7"
+
+"@emotion/sheet@0.9.4":
+  version "0.9.4"
+  resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5"
+  integrity sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==
+
+"@emotion/stylis@0.8.5":
+  version "0.8.5"
+  resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04"
+  integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==
+
+"@emotion/unitless@0.7.5":
+  version "0.7.5"
+  resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed"
+  integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
+
+"@emotion/utils@0.11.3":
+  version "0.11.3"
+  resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.3.tgz#a759863867befa7e583400d322652a3f44820924"
+  integrity sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==
+
+"@emotion/weak-memoize@0.2.5":
+  version "0.2.5"
+  resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
+  integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
+
 "@hapi/address@2.x.x":
   version "2.1.4"
   resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
@@ -1815,6 +1871,22 @@ babel-plugin-dynamic-import-node@^2.3.0:
   dependencies:
     object.assign "^4.1.0"
 
+babel-plugin-emotion@^10.0.27:
+  version "10.0.29"
+  resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-10.0.29.tgz#89d8e497091fcd3d10331f097f1471e4cc3f35b4"
+  integrity sha512-7Jpi1OCxjyz0k163lKtqP+LHMg5z3S6A7vMBfHnF06l2unmtsOmFDzZBpGf0CWo1G4m8UACfVcDJiSiRuu/cSw==
+  dependencies:
+    "@babel/helper-module-imports" "^7.0.0"
+    "@emotion/hash" "0.8.0"
+    "@emotion/memoize" "0.7.4"
+    "@emotion/serialize" "^0.11.16"
+    babel-plugin-macros "^2.0.0"
+    babel-plugin-syntax-jsx "^6.18.0"
+    convert-source-map "^1.5.0"
+    escape-string-regexp "^1.0.5"
+    find-root "^1.1.0"
+    source-map "^0.5.7"
+
 babel-plugin-istanbul@^5.1.0:
   version "5.2.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz#df4ade83d897a92df069c4d9a25cf2671293c854"
@@ -1830,7 +1902,7 @@ babel-plugin-jest-hoist@^24.9.0:
   dependencies:
     "@types/babel__traverse" "^7.0.6"
 
-babel-plugin-macros@2.8.0:
+babel-plugin-macros@2.8.0, babel-plugin-macros@^2.0.0:
   version "2.8.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138"
   dependencies:
@@ -1842,6 +1914,11 @@ babel-plugin-named-asset-import@^0.3.6:
   version "0.3.6"
   resolved "https://registry.yarnpkg.com/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.6.tgz#c9750a1b38d85112c9e166bf3ef7c5dbc605f4be"
 
+babel-plugin-syntax-jsx@^6.18.0:
+  version "6.18.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946"
+  integrity sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=
+
 babel-plugin-syntax-object-rest-spread@^6.8.0:
   version "6.13.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
@@ -2570,7 +2647,7 @@ content-type@~1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
 
-convert-source-map@1.7.0, convert-source-map@^1.4.0, convert-source-map@^1.7.0:
+convert-source-map@1.7.0, convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
   dependencies:
@@ -2652,6 +2729,16 @@ create-ecdh@^4.0.0:
     bn.js "^4.1.0"
     elliptic "^6.0.0"
 
+create-emotion@^10.0.27:
+  version "10.0.27"
+  resolved "https://registry.yarnpkg.com/create-emotion/-/create-emotion-10.0.27.tgz#cb4fa2db750f6ca6f9a001a33fbf1f6c46789503"
+  integrity sha512-fIK73w82HPPn/RsAij7+Zt8eCE8SptcJ3WoRMfxMtjteYxud8GDTKKld7MYwAX2TVhrw29uR1N/bVGxeStHILg==
+  dependencies:
+    "@emotion/cache" "^10.0.27"
+    "@emotion/serialize" "^0.11.15"
+    "@emotion/sheet" "0.9.4"
+    "@emotion/utils" "0.11.3"
+
 create-hash@^1.1.0, create-hash@^1.1.2:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196"
@@ -2901,7 +2988,7 @@ cssstyle@^1.0.0, cssstyle@^1.1.1:
   dependencies:
     cssom "0.3.x"
 
-csstype@^2.2.0:
+csstype@^2.2.0, csstype@^2.5.7:
   version "2.6.9"
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.9.tgz#05141d0cd557a56b8891394c1911c40c8a98d098"
 
@@ -3256,6 +3343,14 @@ emojis-list@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
 
+emotion@^10.0.27:
+  version "10.0.27"
+  resolved "https://registry.yarnpkg.com/emotion/-/emotion-10.0.27.tgz#f9ca5df98630980a23c819a56262560562e5d75e"
+  integrity sha512-2xdDzdWWzue8R8lu4G76uWX5WhyQuzATon9LmNeCy/2BHVC6dsEpfhN1a0qhELgtDVdjyEA6J8Y/VlI5ZnaH0g==
+  dependencies:
+    babel-plugin-emotion "^10.0.27"
+    create-emotion "^10.0.27"
+
 encodeurl@~1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
@@ -3849,6 +3944,11 @@ find-cache-dir@^3.2.0:
     make-dir "^3.0.2"
     pkg-dir "^4.1.0"
 
+find-root@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4"
+  integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==
+
 find-up@4.1.0, find-up@^4.0.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
@@ -8447,7 +8547,7 @@ source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, sourc
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
 
-source-map@^0.5.0, source-map@^0.5.6:
+source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7:
   version "0.5.7"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"