This plugin adds a “CKEditor” field type to Craft CMS, which provides a deeply-integrated rich text and longform content editor, powered by CKEditor 5.
Table of Contents:
- Requirements
- Installation
- Configuration
- Longform Content with Nested Entries
- Converting Redactor Fields
- Converting Matrix Fields
- Adding CKEditor Plugins
This plugin requires Craft CMS 5.9.0 or later.
You can install this plugin from Craft’s in-app Plugin Store or with Composer.
From the Plugin Store:
Go to the Plugin Store in your project’s Control Panel and search for “CKEditor,” then click on the “Install” button in the sidebar.
With Composer:
Open your terminal and run the following commands:
# go to the project directory
cd /path/to/my-project
# tell Composer to load the plugin
composer require craftcms/ckeditor
# tell Craft to install the plugin
php craft plugin/install ckeditorEach CKEditor field allows you to choose the available toolbar buttons, as well as any custom config options and CSS styles that should be registered with the field.
Once you have selected which toolbar buttons should be available, additional settings may be applied via Config options. Options can be defined as static JSON, or a dynamically-evaluated JavaScript snippet; the latter is used as the body of an immediately-invoked function expression, and does not receive any arguments.
Note
Available options can be found in the CKEditor's documentation. Craft will auto-complete config properties for most bundled CKEditor extensions.
Suppose we wanted to give editors more control over the layout and appearance of in-line tables. Whenever you add the “Insert table” button to an editor, inline controls are exposed for Table Row, Table Column, and Merge. These can be supplemented with Table Properties, Table Cell Properties, and Table Caption buttons by adding them in the field’s Config options section:
{
"table": {
"contentToolbar": [
"tableRow",
"tableColumn",
"mergeTableCells",
"toggleTableCaption",
"tableProperties",
"tableCellProperties"
]
}
}Some of these additional buttons can be customized further. For example, to modify the colors available for a cell’s background (within the “Table Cell Properties” balloon), you would provide an array compatible with the TableColorConfig schema under table.tableCellProperties.backgroundColors.
Multiple configuration concerns can coexist in one Config options object! You might have a table key at the top level to customize table controls (as we've done above), as well as a fontColor key that lets you control a pre-defined set of available font colors:
{
"table": { /* ... */ },
"fontColor": {
"colors": [
{
"color": "#181818",
"label": "Black"
},
{
"color": "#d64036",
"label": "Red"
},
{
"color": "#24b559",
"label": "Green"
},
{
"color": "#4fa0b0",
"label": "Blue"
}
]
}
}You no longer have to use the link configuration concern to allow authors to choose whether links should open in new tabs. The manual decorator that added this option can now be made available simply by checking a relevant checkbox under “Advanced Link Fields”.
Tip
An automatic version of this feature is available natively, via the link.addTargetToExternalLinks option.
CKEditor’s Styles plugin makes it easy to apply custom styles to your content via CSS classes.
You can define custom styles using the style config option:
return {
style: {
definitions: [
{
name: 'Tip',
element: 'p',
classes: ['note']
},
{
name: 'Warning',
element: 'p',
classes: ['note', 'note--warning']
},
]
}
}You can then register custom CSS styles that should be applied within the editor when those styles are selected:
.ck.ck-content p.note {
border-left: 4px solid #4a7cf6;
padding-left: 1rem;
color: #4a7cf6;
}
.ck.ck-content p.note--warning {
border-left-color: #e5422b;
color: #e5422b;
}CKEditor fields pass input through HTML Purifier to avoid saving malicious code to the database. This helps prevent XSS attacks and other vulnerabilities.
HTML Purifier is configured primarily via JSON files in your config/htmlpurifier/ folder. New Craft projects (based on craftcms/craft) come with a single Default.json config, which you can modify or supplement with your own configurations. Each CKEditor field with Advanced → Purify HTML enabled uses its selected HTML Purifier config. See the HTML Purifier documentation for a complete list of options!
This behavior is independent of CKEditor’s own HTML sanitization engine—the client-side editor automatically strips out any markup that isn’t supported by an enabled feature or plugin. If you install additional plugins or add custom styles, you may need to relax associated HTML Purifier rules to ensure the markup is not removed by the server when saved.
Warning
Disabling HTML Purifier entirely can expose your site to significant security risks, even if you don’t accept input from anonymous users.
HTML Purifier provides a layer of security, while CKEditor is primarily concerned with hygiene.
The HTMLPurifier_Config object can be modified directly, using the craft\ckeditor\Field::EVENT_MODIFY_PURIFIER_CONFIG event.
use craft\htmlfield\events\ModifyPurifierConfigEvent;
use craft\ckeditor\Field;
use HTMLPurifier_Config;
use yii\base\Event;
Event::on(
Field::class,
Field::EVENT_MODIFY_PURIFIER_CONFIG,
function(ModifyPurifierConfigEvent $event) {
/** @var HTMLPurifier_Config $config */
$config = $event->config;
// ...
}
);CKEditor also makes its general HTML support rules configurable, for situations where the source editor is used, or when authors expect some formatting from pasted content to be preserved:
return {
// ...
htmlSupport: {
allow: [
{
name: 'abbr',
attributes: ['title'],
classes: false,
styles: false
}
],
disallow: [
// ...
].
},
};Adding a rule to the disallow array does not guarantee that matching HTML is stripped from the markup! CKEditor always ensures that the editor’s enabled features and plugins continue to work—for example, disabling all style attributes in an editor that supports lists will still permit style="list-style-type: upper-roman;".
CKEditor 5 stores references to embedded media embeds using oembed tags. Craft CMS configures HTML Purifier to support these tags, however you will need to ensure that the URI.SafeIframeRegexp HTML Purifier setting is set to allow any domains you wish to embed content from.
{
"URI.SafeIframeRegexp": "%^(https?:)?//(www\\.youtube\\.com/|youtu\\.be|player\\.vimeo\\.com/)%"
}To automatically replace oembed tags with the media provider’s embed HTML, enable the field’s Parse embeds setting. Alternatively, see CKEditor’s media embed documentation for examples of how to show the embedded media on your front end.
Note
Be sure to cache your front-end output if you enable the “Parse embeds” setting (e.g. by using a {% cache %} tag). Otherwise, there will be a slight performance hit on each request while CKEditor fetches the embed HTML from the provider.
CKEditor fields can be configured to manage nested entries, which will be displayed as cards within your rich text content, and edited via slideouts.
Nested entries can be created anywhere within your content, and they can be moved, copied, and deleted, just like images and embedded media.
To configure a CKEditor field to manage nested entries, follow these steps:
- Go to Settings → Fields and click on the field you wish to edit (or create a new one).
- Drag the “+” menu button into the toolbar.
- Select one or more entry types which should be available within the field.
- And save.
Now the field is set up to manage nested entries! The next time you edit an element with that CKEditor field, the “+” button will be shown in the toolbar, and when you choose an entry type from its menu, a slideout will open where you can enter content for the nested entry.
An entry card will appear within the rich text content after you press Save within the slideout. The card can be moved via drag-n-drop or cut/paste from there.
Tip
You can choose which entry types should show as individual toolbar buttons and which should be nested under the “+” button. The ones selected to show as individual buttons will be displayed as an icon if the entry type is configured to have one and as text if no icon is specified. Use the 3-dots (actions) menu to toggle this behavior.
You can also copy/paste the card to duplicate the nested entry.
To delete the nested entry, simply select it and press the Delete key.
Note
Copy/pasting entry cards across separate CKEditor fields is supported, providing both fields allow the entry type of the copied nested entry.
On the front end, nested entries will be rendered automatically via their partial templates.
For each entry type selected by your CKEditor field, create a _partials/entry/<entryTypeHandle>.twig file within your templates/ folder, and place your template code for the entry type within it.
An entry variable will be available to the template, which references the entry being rendered.
Tip
If your nested entries contain any relational fields, you can eager-load the related elements using eagerly().
{# Within an element partial... #}
{% for image in entry.myAssetsField.eagerly().all() %}
{# ... #}
{% endfor %}CKEditor field content is represented by an object that can be output as a string ({{ entry.myCkeditorField }}), or used like an array:
{% for chunk in entry.myCkeditorField %}
<div class="chunk {{ chunk.type }}">
{{ chunk }}
</div>
{% endfor %}“Chunks” have two types: markup, containing CKEditor HTML; and entry, representing a single nested entry. Adjacent markup chunks are collapsed into one another in cases where an intervening nested entry is disabled.
The example above treats both chunk types as strings. For entry chunks, this is equivalent to calling {{ entry.render() }}. If you would like to customize the data passed to the element partial, or use a different representation of the entry entirely, you have access to the nested entry via chunk.entry:
{% for chunk in entry.myCkeditorField %}
{% if chunk.type == 'markup' %}
<div class="chunk markup">
{{ chunk }}
</div>
{% else %}
<div class="chunk entry" data-entry-id="{{ chunk.entry.id }}">
{# Call the render() method with custom params... #}
{{ chunk.entry.render({
isRss: true,
}) }}
{# ...or provide completely custom HTML for each supported entry type! #}
{% switch chunk.entry.type.handle %}
{% case 'gallery' %}
{% set slides = chunk.entry.slides.eagerly().all() %}
{% for slide in slides %}
<figure>
{# ... #}
</figure>
{% case 'document' %}
{% set doc = chunk.entry.attachment.eagerly().one() %}
<a href="{{ doc.url }}" download>Download {{ doc.filename }}</a> ({{ doc.size|filesize }})
{% default %}
{# For anything else: #}
{{ chunk.entry.render() }}
{% endswitch %}
</div>
{% endif %}
{% endfor %}You can use the ckeditor/convert/redactor command to convert any existing Redactor fields over to CKEditor.
The command will make changes to your project config. You should commit them, and run craft up on other environments for the changes to take effect.
php craft ckeditor/convert/redactorYou can use the ckeditor/convert/matrix command to convert a Matrix field over to CKEditor. Each of the Matrix field’s entry types will be assigned to the CKEditor field, and field values will be a mix of HTML content extracted from one of the nested entry types of your choosing (if desired) combined with nested entries.
php craft ckeditor/convert/matrix <myMatrixFieldHandle>The command will generate a new content migration, which will need to be run on other environments (via craft up) in order to update existing elements’ field values.
If you'd like to include any of the first party packages from CKEditor, you can call CkeditorConfig::registerFirstPartyPackage() in the init function of a custom module.
use craft\ckeditor\helpers\CkeditorConfig;
class Site extends BaseModule
{
public function init(): void
{
parent::init();
// Register a package with toolbar items and multiple plugins
CkeditorConfig::registerFirstPartyPackage(['SpecialCharacters', 'SpecialCharactersEssentials'], ['specialCharacters']);
// Register a package with a single plugin.
CkeditorConfig::registerFirstPartyPackage(['ImageResize']);
}
}- If you’re creating a custom CKEditor plugin, use CKEditor’s package generator to scaffold it.
Tip
Check out CKEditor’s Creating a basic plugin tutorial for an in-depth look at how to create a custom CKEditor plugin.
Once the CKEditor package is in place in your Craft plugin, create an asset bundle which extends BaseCkeditorPackageAsset. The asset bundle defines the package’s build directory, filename, namespace, a list of CKEditor plugin names provided by the package, and any toolbar items that should be made available via the plugin.
For example, here’s an asset bundle which defines a “Tokens” plugin:
<?php
namespace mynamespace\web\assets\tokens;
use craft\ckeditor\web\assets\BaseCkeditorPackageAsset;
class TokensAsset extends BaseCkeditorPackageAsset
{
public $sourcePath = __DIR__ . '/build';
public string $namespace = '@craftcms/ckeditor5-tokens';
public $js = [
['tokens.js', 'type' => 'module']
];
public array $pluginNames = [
'Tokens',
];
public array $toolbarItems = [
'tokens',
];
}Finally, ensure your asset bundle is registered whenever the core CKEditor asset bundle is. Add the following code to your plugin’s init() method:
\craft\ckeditor\Plugin::registerCkeditorPackage(TokensAsset::class, 'tokens.js');The second parameter should point to the main entry file for your JavaScript. In most cases, it will be the same as the only item in your $js array.
If you wish to use CKEditor on the front end, the safest option is to bring your own CKE build. Craft’s bundle (while technically allowed under the GPL3 license) is not suitable for front-end use, due to its control panel-dependent functionality. Simple configurations defined via the control panel can be used piecemeal in your initializer:
{# Load from the CDN, or a custom bundle... #}
<link rel="stylesheet" href="https://cdn.ckeditor.com/ckeditor5/44.3.0/ckeditor5.css" />
<script src="https://cdn.ckeditor.com/ckeditor5/44.3.0/ckeditor5.umd.js"></script>
{# Load the field definition via the field and CKEditor APIs: #}
{% set field = craft.app.fields.getFieldByHandle('primer') %}
<div id="editor">
{{ entry.myCkEditorField }}
</div>
<script>
// Select any required plugins from the distribution:
const {
ClassicEditor,
Essentials,
Bold,
Italic,
Font,
Paragraph
} = CKEDITOR;
ClassicEditor
.create(document.querySelector('#editor'), {
licenseKey: '<YOUR_LICENSE_KEY>',
plugins: [Essentials, Bold, Italic, Font, Paragraph],
// Configure dynamically from settings:
toolbar: {{ field.toolbar|json_encode|raw }},
})
.then()
.catch();
</script>You may need to build up the top-level config object from field.options, field.headingLevels, and so on to get a complete, functional editor—be mindful that configurations may rely on plugins that are only available in the control panel! Refer to craft\ckeditor\Field::inputHtml() for additional steps the CKEditor plugin when normalizing configuration, for control panel editor instances.

