Integrating CKEditor 5 with React from CDN
CKEditor 5 has an official React integration that you can use to add a rich text editor to your application. It provides a <CKEditor> component that you configure with props for the editor build, its configuration, and event handlers. The component works with multiple editor types, including classic, inline, and decoupled (document). For the multi-root editor, use the dedicated multi-root editor hook. This guide will help you install and configure it to use the CDN distribution of CKEditor 5.
This guide assumes that you already have a React project. If you do not have one, see the React documentation to learn how to create it.
To use our Cloud CDN services, create a free account. Learn more about license key activation.
Start by installing the React integration for CKEditor 5 from npm:
npm install @ckeditor/ckeditor5-reactCopy codeOnce the integration is installed, create a new React component called Editor.jsx. It will use the useCKEditorCloud helper to load the editor code from the CDN and the <CKEditor> component to run it, both of which come from the above package. The following example shows a component with open source and premium CKEditor 5 plugins.
import React from 'react';
import { CKEditor, useCKEditorCloud } from '@ckeditor/ckeditor5-react';
const CKEditorDemo = () => {
const cloud = useCKEditorCloud( {
version: '49.0.0',
premium: true
} );
if ( cloud.status === 'error' ) {
return <div>Error!</div>;
}
if ( cloud.status === 'loading' ) {
return <div>Loading...</div>;
}
const {
ClassicEditor,
Essentials,
Paragraph,
Bold,
Italic
} = cloud.CKEditor;
const { FormatPainter } = cloud.CKEditorPremiumFeatures;
return (
<CKEditor
editor={ ClassicEditor }
data={ '<p>Hello world!</p>' }
config={ {
licenseKey: '<YOUR_LICENSE_KEY>',
plugins: [ Essentials, Paragraph, Bold, Italic, FormatPainter ],
toolbar: [ 'undo', 'redo', '|', 'bold', 'italic', '|', 'formatPainter' ]
} }
/>
);
};Copy codeIn the above example, the useCKEditorCloud helper is used to load the editor code and plugins from CDN. The premium option is set to also load premium plugins. For more information about the useCKEditorCloud helper, see the Loading CDN resources guide.
The <CKEditor> component supports the following properties:
editor(required) – TheEditorconstructor to use.data– The initial data for the created editor. See the Getting and setting data guide.config– The editor configuration. See the Configuration guide.id– The editor ID. When this property changes, the component restarts the editor with new data instead of setting it on an initialized editor.disabled– A Boolean value. Theeditoris being switched to read-only mode if the property is set totrue.onReady– A function called when the editor is ready with aneditorinstance.onAfterDestroy– A function called after the successful destruction of an editor instance rendered by the component. The component is not guaranteed to be mounted when this function is called.onChange– A function called when the editor data has changed. See theeditor.model.document#change:dataevent.onBlur– A function called when the editor was blurred. See theeditor.editing.view.document#blurevent.onFocus– A function called when the editor was focused. See theeditor.editing.view.document#focusevent.onError– A function called when an error is reported for the editor, either during the initialization or at runtime. It receives two arguments: the error instance and the error details. Error details is an object that contains one property:phase:'initialization'|'runtime'– Informs when the error has occurred (during or after the editor/context initialization).
The editor event callbacks (onChange, onBlur, onFocus) receive two arguments:
A reported error does not stop the editor. Its state may no longer be consistent, so do not leave the error unhandled. Nothing is restarted and no data is restored for you, so what happens next is your application’s decision. <CKEditorContext> has an onError of its own, with the same two arguments, for the errors of the context rather than of one of its editors.
The error handling guide covers the options: telling the user and switching the editor to read-only, recreating it, and recovering its content. If you are moving off the Watchdog, the migrating from the Watchdog guide shows how to recreate the editor by changing the component’s key, and when to do it.
The @ckeditor/ckeditor5-react package provides a ready-to-use component for the context feature that is useful to use with some CKEditor 5 collaboration features.
import React from 'react';
import { CKEditor, CKEditorContext, useCKEditorCloud } from '@ckeditor/ckeditor5-react';
export const CKEditorCloudContextDemo = () => {
const cloud = useCKEditorCloud( {
version: '49.0.0'
} );
if ( cloud.status === 'error' ) {
return <div>Error!</div>;
}
if ( cloud.status === 'loading' ) {
return <div>Loading...</div>;
}
const { ClassicEditor } = cloud.CKEditor;
return (
<CKEditorContext
context={ ClassicEditor.Context }
onChangeInitializedEditors={ editors => {
console.log( 'Initialized editors:', editors );
} }
>
<CKEditorNestedInstanceDemo
name='editor1'
content='<p>Editor 1</p>'
/>
<br />
<CKEditorNestedInstanceDemo
name='editor2'
content='<p>Editor 2</p>'
/>
</CKEditorContext>
);
};
function CKEditorNestedInstanceDemo( { name, content } ) {
const cloud = useCKEditorCloud( {
version: '49.0.0',
premium: true
} );
if ( cloud.status === 'error' ) {
console.error( cloud );
return <div>Error!</div>;
}
if ( cloud.status === 'loading' ) {
return <div>Loading...</div>;
}
const {
ClassicEditor,
Essentials,
Paragraph,
Bold,
Italic,
Mention
} = cloud.CKEditor;
return (
<CKEditor
contextItemMetadata={ {
name
} }
editor={ ClassicEditor }
data={ content }
config={ {
plugins: [
Essentials,
Paragraph,
Bold,
Italic,
Mention
],
toolbar: {
items: [ 'undo', 'redo', '|', 'bold', 'italic' ],
}
} }
/>
);
}Copy codeThe CKEditorContext component supports the following properties:
context(required) – The CKEditor 5 context class.config– The CKEditor 5 context configuration.isLayoutReady– A property that delays the context creation when set tofalse. It creates the context and the editor children once it istrueor unset. Useful when the CKEditor 5 annotations or a presence list are used.id– The context ID. When this property changes, the component restarts the context with its editor and reinitializes it based on the current configuration.onChangeInitializedEditors– A function called when any editor is initialized or destroyed in the tree. It receives a dictionary of fully initialized editors, where the key is the value of thecontextItemMetadata.nameproperty set on theCKEditorcomponent. The editor’s ID is the key if thecontextItemMetadataproperty is absent. Additional data can be added to thecontextItemMetadatain theCKEditorcomponent, which will be passed to theonChangeInitializedEditorsfunction.onReady– A function called when the context is ready and all editors inside were initialized with thecontextinstance.onError– A function called when an error is reported for the context, either during the initialization or at runtime. It receives two arguments: the error instance and the error details. Error details is an object that contains one property:phase:'initialization'|'runtime'– Informs when the error has occurred (during or after the editor/context initialization).
An example build that exposes both context and classic editor can be found in the CKEditor 5 collaboration sample.
If you use the document (decoupled) editor, you need to add the toolbar to the DOM manually:
import { useEffect, useRef, useState } from 'react';
import { CKEditor } from '@ckeditor/ckeditor5-react';
function App() {
const cloud = useCKEditorCloud( {
version: '49.0.0'
} );
const editorToolbarRef = useRef( null );
const [ isMounted, setMounted ] = useState( false );
useEffect( () => {
setMounted( true );
return () => {
setMounted( false );
};
}, [] );
if ( cloud.status === 'error' ) {
console.error( cloud );
return <div>Error!</div>;
}
if ( cloud.status === 'loading' ) {
return <div>Loading...</div>;
}
const { DecoupledEditor, Essentials, Paragraph, Bold, Italic } = cloud.CKEditor;
return (
<div>
<div ref={ editorToolbarRef }></div>
<div>
{ isMounted && (
<CKEditor
editor={ DecoupledEditor }
data='<p>Hello from CKEditor 5 decoupled editor!</p>'
config={ {
plugins: [ Essentials, Paragraph, Bold, Italic ],
toolbar: [ 'undo', 'redo', '|', 'bold', 'italic' ]
} }
onReady={ ( editor ) => {
if ( editorToolbarRef.current ) {
editorToolbarRef.current.appendChild( editor.ui.view.toolbar.element );
}
}}
onAfterDestroy={ ( editor ) => {
if ( editorToolbarRef.current ) {
Array.from( editorToolbarRef.current.children ).forEach( child => child.remove() );
}
} }
/>
) }
</div>
</div>
);
}
export default App;Copy codeSingle-root editors such as InlineEditor, BalloonEditor, and DecoupledEditor can be configured as inline editors that accept only inline content (text, bold, italic, links) instead of blocks. This is useful for short fields such as titles, captions, or single-line inputs.
Set root.modelElement to '$inlineRoot' to restrict the root to inline content. Optionally, provide a custom root.element to render the editable host as a specific tag (for example, <h1> for a title) instead of the default <div>.
import React from 'react';
import { CKEditor, useCKEditorCloud } from '@ckeditor/ckeditor5-react';
const InlineTitleDemo = () => {
const cloud = useCKEditorCloud( {
version: '49.0.0'
} );
if ( cloud.status === 'error' ) {
return <div>Error!</div>;
}
if ( cloud.status === 'loading' ) {
return <div>Loading...</div>;
}
const { BalloonEditor, Essentials, Bold, Italic } = cloud.CKEditor;
return (
<CKEditor
editor={ BalloonEditor }
config={ {
licenseKey: '<YOUR_LICENSE_KEY>',
plugins: [ Essentials, Bold, Italic ],
toolbar: [ 'bold', 'italic' ],
root: {
element: 'h1',
modelElement: '$inlineRoot',
initialData: 'Document title',
placeholder: 'Enter title...'
}
} }
/>
);
};Copy codeThe root.element property accepts:
- A tag name string, for example
'h1'or'section'. - A descriptor object with
name,classes,styles, andattributesfields.
Without modelElement: '$inlineRoot', only the host tag changes – the schema still permits blocks inside the root.
The <CKEditor> component always renders a <div> host for ClassicEditor, regardless of root.element. Classic editor wraps its toolbar and editable inside its own structure. Use InlineEditor, BalloonEditor, or DecoupledEditor to control the host element.
Rendering the editor inside a shadow root isolates it from the styles of the host page. The CDN loader injects the style sheets as <link> tags, so point it at the shadow root with the injectedStylesheetsLocation option of the useCKEditorCloud() hook. The whole editor UI, including the body collection that holds balloons and dropdown panels, stays inside the shadow root, so the scoped style sheets cover all of it. This happens because config.ui.overlayContainer is not set, so the editor falls back to the root it is in and logs the ui-overlay-container-not-configured warning. For a more robust setup, set your own overlay container and load the same style sheets into it, as described in the Where the floating user interface mounts section of the Shadow DOM guide.
Attach the shadow root before loading. A <link> that is not in the document never starts loading, which is why the hook is called from a component rendered into an already attached root rather than from the one that creates it.
import { useCallback, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { CKEditor, useCKEditorCloud } from '@ckeditor/ckeditor5-react';
function App() {
const [ shadowRoot, setShadowRoot ] = useState( null );
// `attachShadow()` can be called only once per element, and `host.shadowRoot` cannot
// report an existing root in closed mode, so track the call instead.
const attached = useRef( false );
const hostRef = useCallback( host => {
if ( host && !attached.current ) {
attached.current = true;
setShadowRoot( host.attachShadow( { mode: 'open' } ) );
}
}, [] );
return (
<div ref={ hostRef }>
{ shadowRoot && createPortal( <Editor shadowRoot={ shadowRoot } />, shadowRoot ) }
</div>
);
}
function Editor( { shadowRoot } ) {
const cloud = useCKEditorCloud( {
version: '49.0.0',
injectedStylesheetsLocation: {
targetNode: shadowRoot
}
} );
if ( cloud.status === 'error' ) {
return <div>Error!</div>;
}
if ( cloud.status === 'loading' ) {
return <div>Loading...</div>;
}
const { ClassicEditor, Essentials, Paragraph, Bold, Italic } = cloud.CKEditor;
return (
<CKEditor
editor={ ClassicEditor }
config={ {
licenseKey: '<YOUR_LICENSE_KEY>',
plugins: [ Essentials, Paragraph, Bold, Italic ],
toolbar: [ 'bold', 'italic' ],
root: {
initialData: '<p>Hello from a shadow root!</p>'
}
} }
/>
);
}
export default App;Copy codeattachShadow() can be called only once per element. If the mode of the root has to change at runtime, give the component holding the host a key so that React remounts it.
An override of a --ck-* variable on :root has no effect on an editor inside a shadow root, so put it on the shadow host instead. The Shadow DOM guide explains why, and covers the known limitations.
We provide ready-to-use integration featuring collaborative editing in a React application:
It is not mandatory to build an application on top of the above samples, however, it should help you get started.
CKEditor 5 supports multiple UI languages, and so does the official React component. To translate the editor, pass the languages you need into the translations array inside the configuration of the useCKEditorCloud hook.
import React from 'react';
import { CKEditor, useCKEditorCloud } from '@ckeditor/ckeditor5-react';
const CKEditorDemo = () => {
const cloud = useCKEditorCloud( {
version: '49.0.0',
translations: [ 'es' ]
} );
if ( cloud.status === 'error' ) {
return <div>Error!</div>;
}
if ( cloud.status === 'loading' ) {
return <div>Loading...</div>;
}
const {
ClassicEditor,
Essentials,
Bold,
Italic,
Paragraph
} = cloud.CKEditor;
return (
<CKEditor
editor={ ClassicEditor }
data={ '<p>Hello world!</p>' }
config={ {
licenseKey: '<YOUR_LICENSE_KEY>',
toolbar: [ 'undo', 'redo', '|', 'bold', 'italic' ],
plugins: [ Bold, Essentials, Italic, Paragraph ],
} }
/>
);
};Copy codeFor more information, please refer to the Setting the UI language guide.
The official React integration for CKEditor 5 is written in TypeScript and fully supports it. If you use TypeScript in your project, you can use the CKEditor component without additional configuration. However, if you want to use some specific types from the CKEditor 5 packages, you can import them directly from a special package containing type definitions. Take a look at the following example:
import React from 'react';
import { CKEditor, useCKEditorCloud } from '@ckeditor/ckeditor5-react';
import type { EventInfo } from 'https://cdn.ckeditor.com/typings/ckeditor5.d.ts';
const CKEditorDemo = () => {
const cloud = useCKEditorCloud( {
version: '49.0.0',
translations: [ 'es' ]
} );
if ( cloud.status === 'error' ) {
return <div>Error!</div>;
}
if ( cloud.status === 'loading' ) {
return <div>Loading...</div>;
}
const {
ClassicEditor,
Essentials,
Bold,
Italic,
Paragraph
} = cloud.CKEditor;
return (
<CKEditor
editor={ ClassicEditor }
data={ '<p>Hello world!</p>' }
config={ {
licenseKey: '<YOUR_LICENSE_KEY>',
toolbar: [ 'undo', 'redo', '|', 'bold', 'italic' ],
plugins: [ Bold, Essentials, Italic, Paragraph ],
} }
onBlur={ ( event: EventInfo ) => {
// your event handler
} }
/>
);
};Copy codeIn the example above, the EventInfo type is imported from the https://cdn.ckeditor.com/typings/ckeditor5.d.ts package, while the editor itself loads from the CDN. Note that https://cdn.ckeditor.com/typings/ckeditor5.d.ts is not an actual URL to the CKEditor 5 typings file but a synthetic TypeScript module providing typings for the editor. The ckeditor5 package supplies the actual types, which depend on the @ckeditor/ckeditor5-react package.
Although this setup might seem complex, it prevents users from directly importing anything from the ckeditor5 package, which could lead to duplicated code issues.
If you want to use types for premium features, you can import them similarly to the base editor types. Remember that you need to install the ckeditor5-premium-features package to use them. You can do it by running the following command:
npm install --save-dev ckeditor5-premium-featuresCopy codeAfter installing the package, you can import the types in the following way:
<script setup>
// ...
import type { Mention } from 'https://cdn.ckeditor.com/typings/ckeditor5-premium-features.d.ts';
// ...
</script>Copy codeWhile type definitions for the base editor should be available out of the box, some bundlers do not install the ckeditor5 package, which provides typing for the editor. If you encounter any issues with the type definitions, you can install the ckeditor5 package manually:
npm install --save-dev ckeditor5Copy codeThe source code of rich text editor component for React is available on GitHub in https://github.com/ckeditor/ckeditor5-react.
- See how to manipulate the editor’s data in the Getting and setting data guide.
- Refer to further guides in the setup section to see how to customize your editor further.
- Check the features category to learn more about individual features.