Looking for the best React Editor library
Thomas Findlay
Posted on January 28, 2023
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
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;
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;
The initial Slate editor looks just like a normal textarea field.
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",
},
],
},
];
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;
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);
};
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;
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;
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;
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;
The gif below shows what the Slate editor with an image looks like.
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;
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;
}
}
}
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;
Below you can see what the TipTap editor looks like.
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;
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;
Here's what the Quill editor looks like.
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;
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;
And here's what it looks like.
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.
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.
Posted on January 28, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.