DEV Community

Cover image for Looking for the best React Editor library
Thomas Findlay
Thomas Findlay

Posted on

Looking for the best React Editor library

Gathering user input is very common across the internet, and forms with various fields can be found on many websites. However, forms can vary in size and complexity. Simple forms can contain just one input field, while more complex ones could involve multiple steps with different fields like textareas, dropdowns, radios, checkboxes, and so on. However, there are circumstances when a user should be able to provide richer content, such as text in different formatting and sizes, images, videos, links, etc., without a limitation of pre-defined fields. That's where Rich Text Editors come into play.

As is the case with a lot of things, there are plenty of libraries that can be used to incorporate a Rich Text Editor in React. However, they all differ in what they provide and how they work. For example, some React Rich Text Editors provide barebones that can be used and built upon to create a Rich Text Editor, while others offer full-blown editors with a lot of functionality out-of-the-box that can be configured to suit the application's needs. In this article, we will explore a few popular React Rich Text Editors, the features they provide and how to implement them, specifically:

  • Slate
  • TipTap
  • Quill
  • KendoReact Rich Text Editor

You can find the full code example in this GitHub repo and an interactive example in the Stackblitz.

If you would like to follow this article, you can clone the GitHub repo for this article and switch to the start branch. The commands below show how to do that:

$ git clone https://github.com/ThomasFindlay/looking-for-best-react-editor-library
$ cd looking-for-best-react-editor-library
$ git checkout start
$ npm install
$ npm run dev
Enter fullscreen mode Exit fullscreen mode

Slate

Slate, as per its documentation, is a completely customizable framework for building rich text editors. Therefore, it doesn't offer a feature-rich text editor but instead provides tools to build one. Let's create a component called Slate and see what the Slate editor looks like.

src/components/Slate.jsx

import { useMemo } from "react";
import { createEditor } from "slate";
import { Slate, Editable, withReact } from "slate-react";

const initialValue = [
  {
    type: "paragraph",
    children: [
      {
        text: "Hello Slate Editor",
      },
    ],
  },
];

const SlateEditor = props => {
  const editor = useMemo(() => withReact(createEditor()), []);
  return (
    <div>
      <h2> Slate </h2>
      <Slate editor={editor} value={initialValue}>
        <Editable
          style={{
            border: "1px solid grey",
            padding: "0.25rem",
          }}
        />
      </Slate>
    </div>
  );
};

export default SlateEditor;
Enter fullscreen mode Exit fullscreen mode

Next, import and render the Slate component in the App component.

src/App.jsx

import "./App.css";
import Slate from "./components/Slate";

function App() {
  return (
    <div className="App">
      <h1>React Editors</h1>

      <div
        style={{
          width: 700,
          display: "flex",
          flexDirection: "column",
          gap: 32,
        }}
        >
        <Slate />
      </div>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

The initial Slate editor looks just like a normal textarea field.

Barebones Slate Editor

As mentioned before, Slate offers tools that can be used to build a Rich Text Editor. Functionality such as boldening and italicizing text or adding images is not provided and instead needs to be implemented. This isn't great if all you want is an easily configurable text editor. However, it can be useful if more custom functionality is needed. Let's have a look at how to add functionality to insert an image with Slate.

The usefulness of Slate lies in the fact that it is completely customizable, and we can define what Slate should render and how. In the initial example above, we provided the following object to display the Hello Slate Editor message in the editor:

const initialValue = [
  {
    type: "paragraph",
    children: [
      {
        text: "Hello Slate Editor",
      },
    ],
  },
];
Enter fullscreen mode Exit fullscreen mode

We can customise what the Slate editor renders by providing a custom component via the renderElement prop. The custom component can decide how to render a block in the Slate editor based on its type. The default element type is a paragraph, but we will add a new one called image. First, let's create a button and logic that will trigger a popup to enter a URL and then insert an image into the editor.

src/components/slate/components/InsertImageButton.jsx

import { Transforms } from "slate";
import { useSlateStatic } from "slate-react";
import { isImageUrl } from "../helpers/isImageUrl";

const insertImage = (editor, url) => {
  const text = { text: "" };
  const image = { type: "image", url, children: [text] };
  Transforms.insertNodes(editor, image);
};

export const withImages = editor => {
  const { insertData, isVoid } = editor;

  editor.isVoid = element => {
    return element.type === "image" ? true : isVoid(element);
  };

  editor.insertData = data => {
    const text = data.getData("text/plain");

    const { files } = data;

    if (files && files.length > 0) {
      for (const file of files) {
        const reader = new FileReader();
        const [mime] = file.type.split("/");

        if (mime === "image") {
          reader.addEventListener("load", () => {
            const url = reader.result;
            insertImage(editor, url);
          });

          reader.readAsDataURL(file);
        }
      }
    } else if (isImageUrl(text)) {
      insertImage(editor, text);
    } else {
      insertData(data);
    }
  };

  return editor;
};

const InsertImageButton = () => {
  const editor = useSlateStatic();
  return (
    <button
      onMouseDown={event => {
        event.preventDefault();
        const url = window.prompt("Enter the URL of the image:");
        if (url && !isImageUrl(url)) {
          alert("URL is not an image");
          return;
        }
        url && insertImage(editor, url);
      }}
    >
      Add Image
    </button>
  );
};

export default InsertImageButton;
Enter fullscreen mode Exit fullscreen mode

We have three main pieces in the code above - insertImage and withImages functions and the InsertImageButton component.

The first one is responsible for creating and inserting a new image node. The withImages function is a higher-order function that modifies the editor instance. Specifically, it monkey patches isVoid and insertData methods to handle the new image node type. The InsertImageButton component, as the name suggests, renders a button that prompts a user to enter a URL and then executes the insertImage function.

The withImages function utilises a helper called isImageUrl, but it doesn't exist yet, so let's take care of that.

src/components/slate/helpers/isImageUrl.js

import imageExtensions from "image-extensions";
import isUrl from "is-url";

export const isImageUrl = url => {
  if (!url) return false;
  if (!isUrl(url)) return false;
  const ext = new URL(url).pathname.split(".").pop();
  return imageExtensions.includes(ext);
};
Enter fullscreen mode Exit fullscreen mode

Next, we need to create a custom Element component and the Image component so the Slate editor knows how to handle nodes with the image type.

src/components/slate/components/slate-elements/Element.jsx

import Image from "./Image";

const Element = props => {
  const { attributes, children, element } = props;
  switch (element.type) {
    case "image":
      return <Image {...props} />;
    default:
      return <p {...attributes}>{children}</p>;
  }
};

export default Element;
Enter fullscreen mode Exit fullscreen mode

The Element component uses the element.type value to render either a paragraph element or the Image component that we will create now.

src/components/slate/components/slate-elements/Image.jsx

import { useFocused, useSelected } from "slate-react";
import { css } from "@emotion/css";

const Image = ({ attributes, children, element }) => {
  const selected = useSelected();
  const focused = useFocused();
  return (
    <div {...attributes}>
      {children}
      <div
        contentEditable={false}
        className={css`
          position: relative;
        `}
      >
        <img
          src={element.url}
          className={css`
            display: block;
            max-width: 100%;
            max-height: 20em;
            box-shadow: ${selected && focused ? "0 0 0 3px #B4D5FF" : "none"};
          `}
        />
      </div>
    </div>
  );
};

export default Image;
Enter fullscreen mode Exit fullscreen mode

The Image component renders an image element and utilises the selected and focused values to highlight the rendered image.

Now we can create a new component to render the Slate editor with image capabilities.

src/components/slate/SlateWithImage.jsx

import { useMemo } from "react";
import { createEditor } from "slate";
import { Slate, Editable, withReact } from "slate-react";
import InsertImageButton, { withImages } from "./components/InsertImageButton";
import Element from "./components/slate-elements/Element";

const initialValue = [
  {
    type: "paragraph",
    children: [
      {
        text: "Hello Slate Editor",
      },
    ],
  },
];

const SlateEditor = props => {
  const editor = useMemo(() => withImages(withReact(createEditor())), []);
  return (
    <div>
      <h2> Slate With Image </h2>
      <Slate editor={editor} value={initialValue}>
        <div style={{ marginBottom: "1rem" }}>
          <InsertImageButton />
        </div>
        <Editable
          renderElement={props => <Element {...props} />}
          style={{
            border: "1px solid grey",
            padding: "0.25rem",
          }}
        />
      </Slate>
    </div>
  );
};

export default SlateEditor;
Enter fullscreen mode Exit fullscreen mode

Last but not least, let's render the SlateWithImage component.

src/App.jsx

import "./App.css";
import SlateWithImage from "./components/slate/SlateWithImage";

function App() {
  return (
    <div className="App">
      <h1>React Editors</h1>
      <div
        style={{
          width: 700,
          display: "flex",
          flexDirection: "column",
          gap: 32,
        }}
        >
        <SlateWithImage />
      </div>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

The gif below shows what the Slate editor with an image looks like.

Slate editor with image functionality

If you're up for a challenge, you can enhance the image upload functionality and allow a user to upload an image from their device instead of only providing a URL.

TipTap

TipTap, built on top of ProseMirror, is a headless editor framework that gives full control over every single aspect of the text editor experience. Similarly to Slate, TipTap doesn't offer a fully featured Rich Text Editor; instead, it offers a lot of extensions and can be customized to incorporate new features. Let's have a look at how we can implement a TipTap editor with the image extension that will provide similar functionality to the one we implemented in the last section for the Slate editor.

src/components/TipTap.jsx

import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import style from "./TipTap.module.scss";
import { Image } from "@tiptap/extension-image";
import { useCallback } from "react";

Image.configure({});

const TipTap = props => {
  const editor = useEditor({
    extensions: [StarterKit, Image],
    content: "<p>Hello from TipTap Rich Text Editor!</p>",
  });

  const addImage = useCallback(() => {
    const url = window.prompt("URL");
    if (!url) return;
    editor.chain().focus().setImage({ src: url }).run();
  }, [editor]);

  if (!editor) return null;

  return (
    <div className={style.tiptapContainer}>
      <h2>TipTap</h2>
      <div className="k-display-flex k-gap-2">
        <button onClick={() => editor.chain().focus().toggleBold().run()}>
          B
        </button>
        <button onClick={() => editor.chain().focus().toggleItalic().run()}>
          I
        </button>
        <button onClick={addImage}>Add Image</button>
      </div>
      <EditorContent editor={editor} />
    </div>
  );
};

export default TipTap;
Enter fullscreen mode Exit fullscreen mode

A TipTap editor instance is created using the useEditor hook and then passed to the EditorContent component. The useEditor hook accepts a config object as an argument. In our example, we have two extensions - StarterKit and Image. The former includes several common features for formatting text and adding richer content, such as headings, lists, bullets, code blocks, etc. The Image extension provides the logic that handles the insertion of images into the Tiptap editor. Besides the EditorContent component that was mentioned earlier, we also have three buttons. The first two allow boldening and italicization of text, whilst the last one triggers a prompt asking for an image URL and then adds the image to the editor.

TipTap is a headless editor, so it doesn't provide any styles by default. However, TipTap's documentation has some basic styles for the editor.

src/components/TipTap.module.scss

/* Basic editor styles */
.tiptapContainer {
  .ProseMirror {
    padding: 0.5rem;

    > * + * {
      margin-top: 0.75em;
    }

    ul,
    ol {
      padding: 0 1rem;
    }

    h1,
    h2,
    h3,
    h4,
    h5,
    h6 {
      line-height: 1.1;
    }

    code {
      background-color: rgba(#616161, 0.1);
      color: #616161;
    }

    pre {
      background: #0d0d0d;
      color: #fff;
      font-family: "JetBrainsMono", monospace;
      padding: 0.75rem 1rem;
      border-radius: 0.5rem;

      code {
        color: inherit;
        padding: 0;
        background: none;
        font-size: 0.8rem;
      }
    }

    img {
      max-width: 100%;
      height: auto;
    }

    blockquote {
      padding-left: 1rem;
      border-left: 2px solid rgba(#0d0d0d, 0.1);
    }

    hr {
      border: none;
      border-top: 2px solid rgba(#0d0d0d, 0.1);
      margin: 2rem 0;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

After the styles are ready, we need to render the TipTap component, so let's update the App component.

src/App.jsx

import "./App.css";
import TipTap from "./components/TipTap";

function App() {
  return (
    <div className="App">
      <h1>React Editors</h1>

      <div
        style={{
          width: 700,
          display: "flex",
          flexDirection: "column",
          gap: 32,
        }}
        >
        <TipTap />
      </div>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Below you can see what the TipTap editor looks like.

TipTap editor with image capabilities

Quill

Quill.js is a rich text editor that provides a configurable editor with various features. Compared to Slate and TipTap, Quill is a drop-in solution that doesn't require building common text editing functionality from scratch.

To demonstrate how Quill works, we are going to use the React-Quill package, which is a React wrapper around the Quill editor. Below we have a component that will render the React Quill editor with a toolbar that offers tools for formatting text.

src/components/Quill.jsx

import React, { useState } from "react";
import ReactQuill from "react-quill";
import "react-quill/dist/quill.snow.css";

const modules = {
  toolbar: [
    [{ header: [1, 2, false] }],
    ["bold", "italic", "underline", "strike", "blockquote"],
    [
      { list: "ordered" },
      { list: "bullet" },
      { indent: "-1" },
      { indent: "+1" },
    ],
    ["link", "image"],
    ["clean"],
  ],
};

const Quill = props => {
  const [value, setValue] = useState("");

  return (
    <div>
      <h2>Quill</h2>
      <ReactQuill
        theme="snow"
        value={value}
        onChange={setValue}
        modules={modules}
      />
    </div>
  );
};

export default Quill;
Enter fullscreen mode Exit fullscreen mode

The modules prop can be used to configure the Quill editor. In this example, we configure the toolbar and specify what text editing features should be active and in which order they should be displayed.

We need to update the App component to render the Quill editor.

src/App.jsx

import "./App.css";
import Quill from "./components/Quill";

function App() {
  return (
    <div className="App">
      <h1>React Editors</h1>
      <div
        style={{
          width: 700,
          display: "flex",
          flexDirection: "column",
          gap: 32,
        }}
        >
        <Quill />
      </div>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Here's what the Quill editor looks like.

Quill React Rich Text Editor

KendoReact Editor

KendoReact Rich Text Editor, similarly to Quill, provides a feature-rich editor component that offers a large number of editor tools. It can be easily configured with as little or as many text editing tools as needed. Let's see it in action.

src/components/KendoReactEditor.jsx

import "@progress/kendo-theme-material/dist/all.css";
import { Editor, EditorTools } from "@progress/kendo-react-editor";

const {
  Bold,
  Italic,
  Underline,
  Strikethrough,
  Subscript,
  Superscript,
  AlignLeft,
  AlignCenter,
  AlignRight,
  AlignJustify,
  Indent,
  Outdent,
  OrderedList,
  UnorderedList,
  Undo,
  Redo,
  FontSize,
  FontName,
  FormatBlock,
  Link,
  Unlink,
  InsertImage,
  ViewHtml,
  InsertTable,
  AddRowBefore,
  AddRowAfter,
  AddColumnBefore,
  AddColumnAfter,
  DeleteRow,
  DeleteColumn,
  DeleteTable,
  MergeCells,
  SplitCell,
} = EditorTools;

const content = "Hello from KendoReact Editor";

const KendoReactEditor = props => {
  return (
    <div>
      <h2>KendoReact Editor</h2>
      <Editor
        tools={[
          [Bold, Italic, Underline, Strikethrough],
          [Subscript, Superscript],
          [AlignLeft, AlignCenter, AlignRight, AlignJustify],
          [Indent, Outdent],
          [OrderedList, UnorderedList],
          FontSize,
          FontName,
          FormatBlock,
          [Undo, Redo],
          [Link, Unlink, InsertImage, ViewHtml],
          [InsertTable],
          [AddRowBefore, AddRowAfter, AddColumnBefore, AddColumnAfter],
          [DeleteRow, DeleteColumn, DeleteTable],
          [MergeCells, SplitCell],
        ]}
        contentStyle={{ height: 320 }}
        defaultContent={content}
      />
    </div>
  );
};

export default KendoReactEditor;
Enter fullscreen mode Exit fullscreen mode

The EditorTools object contains toolbar components for the rich text editor. These are then passed to the Editor component via the tools prop. The tools can be grouped into sections and ordered using nested arrays and changing the order in which they are defined.

Finally, let's render the KendoReact Editor.

src/App.jsx

import "./App.css";
import KendoReactEditor from "./components/KendoReactEditor";

function App() {
  return (
    <div className="App">
      <h1>React Editors</h1>

      <div>
        <div
          style={{
            width: 700,
            display: "flex",
            flexDirection: "column",
            gap: 32,
          }}
        >
          <KendoReactEditor />
        </div>
      </div>
    </div>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

And here's what it looks like.

KendoReact Rich Text Editor add image functionality

KendoReact Text Editor, besides an impressive number of text editing features, also has built-in support for tables, find and replace, globalization and many other features. Here's an example of the table functionality in the editor.

KendoReact Rich Text Editor table functionality

Summary

There are multiple React Editor libraries available on the market that developers can choose from. However, it's important to choose an appropriate library for your project. If you want to build a custom rich text editor, Slate and TipTap could be great choices, as they can provide the necessary tools. However, if you want to easily add a rich text editor that can be configured to provide a user with a lot of options for text editing, then Quill and KendoReact Editor are the appropriate choices, as they will require much less code to achieve the same outcome. What's more, KendoReact Editor comes with another benefit, as it is a part of an enterprise-grade suite of React UI components called KendoReact UI that offers over 100 ready-to-use React components.

Top comments (0)