Rich text editor

Quill based rich text editor
Import
License

Installation

Package has peer dependencies: react, react-dom, @mantine/hooks and @mantine/core:.

Install with npm:

npm install @mantine/rte @mantine/core @mantine/hooks

Install with yarn:

yarn add @mantine/rte @mantine/core @mantine/hooks

Demo

Usage

value and onChange props are required for component to work. Note that though component is controlled you cannot force value (limitation of Quill.js library).

import { useState } from 'react';
import { RichTextEditor } from '@mantine/rte';
const initialValue =
'<p>Your initial <b>html value</b> or an empty string to init editor without value</p>';
function Demo() {
const [value, onChange] = useState(initialValue);
return <RichTextEditor value={value} onChange={onChange} />;
}

Configure toolbar

RichTextEditor supports these controls in toolbar:

  • bold, strike, italic, underline – general inline formatting
  • clean – removes all inline formatting
  • h1, h2, ..., h6 – headings, in default toolbar only h1-h4 headings are displayed
  • link – link editor
  • blockquote – blockquote
  • sub, sup – super and sub scripts
  • video, image – video and image embeds
  • unorderedList, orderedList – ul and ol tags
  • alignCenter, alignLeft, alignRight – controls text-align
  • code and codeBlock – inline and block code

You can add, remove and configure controls arrangement in toolbar with controls prop:

Default toolbar:
Custom toolbar:
<RichTextEditor
controls={[
['bold', 'italic', 'underline', 'link', 'image'],
['unorderedList', 'h1', 'h2', 'h3'],
['sup', 'sub'],
['alignLeft', 'alignCenter', 'alignRight'],
]}
/>

To configure sticky toolbar properties set following props:

  • sticky – set to false to make toolbar stay at the top
  • stickyOffset – top property, used with sticky position, use it to offset elements with fixed position, for example, Mantine docs website has 60px header, in this case you should set stickyOffset to 60
// Toolbar stays at the top
<RichTextEditor sticky={false} />
// Toolbar position is set to sticky with top: 40px
<RichTextEditor stickyOffset={40} />

Images and videos embeds

Images upload

RichTextEditor will handle images upload in following situations:

  • Image button click in toolbar
  • Image was pasted from clipboard into editor
  • Image was dropped into editor

To set up images upload add onImageUpload function:

import { useState } from 'react';
import { RichTextEditor } from '@mantine/rte';
// Example with imgbb.com, usually you would use similar logic to upload to S3 like storages
// Function must return a promise that resolves with uploaded image url
// After promise is resolved blurred image placeholder with be replaced with uploaded
const handleImageUpload = (file: File): Promise<string> =>
new Promise((resolve, reject) => {
const formData = new FormData();
formData.append('image', file);
fetch('https://api.imgbb.com/1/upload?key=api_key', {
method: 'POST',
body: formData,
})
.then((response) => response.json())
.then((result) => resolve(result.data.url))
.catch(() => reject(new Error('Upload failed')));
});
function Demo() {
const [value, onChange] = useState('');
return <RichTextEditor value={value} onChange={onChange} onImageUpload={handleImageUpload} />;
}

Important! If you do not provide onImageUpload all images will be converted to base64 format. In most cases this is not a valid option to store images so make sure you provide onImageUpload if you are planning to use images.

Mentions

RichTextEditor comes with quill-mentions plugin. To add mentions support, add provide quill-mentions configuration to mentions prop:

import { useState } from 'react;
import { RichTextEditor } from '@mantine/rte';
const people = [
{ id: 1, value: 'Bill Horsefighter' },
{ id: 2, value: 'Amanda Hijacker' },
{ id: 3, value: 'Leo Summerhalter' },
{ id: 4, value: 'Jane Sinkspitter' },
];
const tags = [
{ id: 1, value: 'JavaScript' },
{ id: 2, value: 'TypeScript' },
{ id: 3, value: 'Ruby' },
{ id: 3, value: 'Python' },
];
function Demo() {
const [value, onChange] = useState('<p>Type @ or # to see mentions autocomplete</p>');
const mentions = useMemo(
() => ({
allowedChars: /^[A-Za-z\sÅÄÖåäö]*$/,
mentionDenotationChars: ['@', '#'],
source: (searchTerm, renderList, mentionChar) => {
const list = mentionChar === '@' ? people : tags;
const includesSearchTerm = list.filter((item) =>
item.value.toLowerCase().includes(searchTerm.toLowerCase())
);
renderList(includesSearchTerm);
},
}),
[]
);
return (
<RichTextEditor
value={value}
onChange={onChange}
placeholder="Type @ or # to see mentions autocomplete"
mentions={mentions}
/>
);
}

Extra modules

You can provide any amount of extra modules. Note that it is required to memoize modules object:

import { useMemo } from 'react';
import { RichTextEditor } from '@mantine/rte';
function Demo() {
const modules = useMemo(
() => ({
history: { delay: 2500, userOnly: true },
syntax: true,
}),
[]
);
return <RichTextEditor modules={modules} {...otherProps} />;
}

Read only

When editor is readonly state, user cannot edit content, toolbar is hidden:

<RichTextEditor readOnly {...otherProps} />

Keyboard shortcuts

  • ⌘ + B / Ctrl + B – toggle bold format in current selection
  • ⌘ + I / Ctrl + I – toggle italic format in current selection
  • ⌘ + U / Ctrl + U – toggle underline format in current selection
  • ⌘ + K / Ctrl + K – add link to current selection
  • ⌘ + option + 1 / Ctrl + Alt + 1 – toggle heading at current line, valid for 1-6 headings

Get editor ref

import { useRef, useEffect } from 'react';
import { RichTextEditor, Editor } from '@mantine/rte';
function Demo() {
const editorRef = useRef<Editor>();
useEffect(() => {
editorRef.current.focus();
}, []);
return <RichTextEditor ref={editorRef} {...otherProps} />;
}

Server side rendering

Quill does not support server side rendering as it relies on browser API. To make component work on server you will need to create a wrapper component with additional checks.

General strategy:

// Create a separate component which will load RichTextEditor only in browser
import React from 'react';
import type { RichTextEditorProps } from '@mantine/rte';
export function RichText(props: RichTextEditorProps) {
if (typeof window !== 'undefined') {
// eslint-disable-next-line import/extensions, global-require
const { RichTextEditor } = require('@mantine/rte');
return <RichTextEditor {...props} />;
}
// Render anything as fallback on server, e.g. loader or html content without editor
return null;
}

Usage with Next.js

To make component work with Next.js use dynamic module:

// RichText.tsx in your components folder
import dynamic from 'next/dynamic';
export default dynamic(() => import('@mantine/rte'), {
// Disable during server side rendering
ssr: false,
// Render anything as fallback on server, e.g. loader or html content without editor
loading: () => null,
});

Then when you want to use RichTextEditor import your component instead:

import RichTextEditor from '../components/RichText';
function MyPage() {
return <RichTextEditor />;
}