CKEditor 5 integration for REDAXO with profile-based configuration, REDAXO media/link dialogs, snippets, style management, and import/export workflows.
- Install the addon via the REDAXO installer.
- Go to CKEditor 5 → Profiles and create a profile (e.g.
default).
<textarea
class="form-control cke5-editor"
name="REX_INPUT_VALUE[1]"
data-profile="default"
data-lang="<?php echo \Cke5\Utils\Cke5Lang::getUserLang(); ?>"
data-content-lang="<?php echo \Cke5\Utils\Cke5Lang::getOutputLang(); ?>"
>REX_VALUE[1]</textarea><div class="ck-content">
REX_VALUE[id="1" output="html"]
</div>The bundled CSS file provides correct rendering of lists, tables, media, and other CKEditor elements. Include it in the <head> of every page:
<link rel="stylesheet" href="/assets/addons/cke5/cke5_content_styles.css">Note: All styles are prefixed with
.ck-content. The output element must carry this class for the styles to apply. Base typography (font family, size, color, line height) inherits from the parent element — your project CSS (UIkit, Bootstrap, etc.) is not overridden.
- Modern CKEditor 5 integration in REDAXO backend
- Theme support (
dark,auto,notheme) - Language-aware placeholders and UI/content language handling
- Height control via data attributes (
data-min-height,data-max-height) - Stable initialization for repeated/dynamic fields (for example MBlock reindex)
- Profile manager for editor configurations
- Drag and drop/tag-based profile editing
- Expert mode with raw
expert_definition+expert_suboption - Live preview page for profile output and integration snippets
- Single style entities with element/classes and optional CSS
- Style groups with JSON configuration and optional CSS
- Snippet entities selectable per profile (replacement for templates)
- Auto-generated backend CSS from configured style/style-group CSS definitions
- REDAXO media integration (
openREXMedia) for image insertion/replacement - REDAXO link integration (
openLinkMap, media links,mailto:,tel:, YTable) - Image upload endpoint for media pool upload workflows
- Image toolbar safeguards for image linking (
linkImage)
- Native addon plugins loaded at runtime:
RedaxoLinkIntegrationRedaxoMediaImageRedaxoClearWidgetRedaxoQuickEditRedaxoSnippetsRedaxoPastePlainTextToggleRedaxoMarkdownPasteToggleRedaxoMinimapToggleRedaxoForListsRedaxoForTableRedaxoVideoWidgetTest
- External plugin registry support via addon API and JS config
- Toolbar alias transformations for external plugins
for_tableproperties for table, column, row and cell via native dropdown panels- Optional class presets for table/column/row/cell in profile settings
- Global table class defaults in
Profiles > Defaults > Global settings - Backward-compatible toolbar normalization:
tableProperties->forTablePropertiestableColumnProperties->forTableColumnPropertiestableRowProperties->forTableRowPropertiestableCellProperties->forTableCellProperties
- If
forTablePropertiesis present in old profiles, column/row properties are auto-added - Unified ordered-list numbering logic across browsers (including nested decimal levels and start index)
QuickEdit opens an inline command menu at the cursor position when typing / in the editor. It only lists commands that fit the active profile configuration, for example enabled heading levels or media/widget actions whose toolbar item is present in the profile.
The feature can be disabled globally in CKEditor 5 > Profiles > Defaults > Global settings. It uses the normal rex_config_form storage format: |1| enables QuickEdit, null or an empty value disables it. If the config key does not exist yet, QuickEdit defaults to enabled for compatibility.
Other addons can add menu entries by loading a JavaScript file before editor instances are created. The recommended way is to register that file via the CKE5 plugin registry in the addon boot.php:
<?php
use Cke5\PluginRegistry;
if (rex::isBackend() && rex_addon::get('cke5')->isAvailable()) {
PluginRegistry::addPlugin(
'my_quickedit_commands',
rex_url::addonAssets('my_addon', 'js/cke5-quickedit-commands.js')
);
}The loaded JavaScript file extends window.CKE5_QUICKEDIT_COMMANDS:
window.CKE5_QUICKEDIT_COMMANDS = window.CKE5_QUICKEDIT_COMMANDS || [];
window.CKE5_QUICKEDIT_COMMANDS.push({
id: 'myQuickAction',
label: 'My action',
keys: ['my', 'action'],
icon: 'M',
toolbarItem: 'link',
execute: function (editor) {
editor.execute('link', 'https://redaxo.org');
}
});Supported properties:
id: unique technical command name.label: text shown in the QuickEdit menu.keys: optional search aliases for filtering after/....icon: short text/icon for the leading column.toolbarItem: toolbar item that must be present in the active profile.toolbarAny: alternative toolbar items; at least one must be present.command: CKEditor command to execute.commandArgs: optional arguments foreditor.execute(command, commandArgs).execute(editor): custom execution logic if a regular CKEditor command is not enough.
An entry is shown only if its toolbarItem or one of its toolbarAny items exists in the active profile. When command is used, that CKEditor command must also be registered.
- Install addon (Installer or package deployment).
- Run REDAXO update/install routine.
- Open
CKEditor 5 > Profilesand configure at least one profile. - Use the profile in your textarea via
data-profile.
For regular CKEditor vendor updates, use the addon-local workflow:
cd public/redaxo/src/addons/cke5
pnpm install
pnpm run vendor:updateThis regenerates the vendor under assets/vendor/ckeditor5-modern and synchronizes the shipped copy under public/assets/addons/cke5/vendor/ckeditor5-modern.
The file /assets/addons/cke5/cke5_content_styles.css is automatically regenerated and overwritten on every pnpm run content-styles:update. Any direct changes will be lost.
The correct approach: Include the bundled CSS, then load your own project file afterwards that only overrides the desired variables.
<!-- In <head>: bundled CKEditor CSS first, then your own -->
<link rel="stylesheet" href="/assets/addons/cke5/cke5_content_styles.css">
<link rel="stylesheet" href="/assets/project/cke5-content.css">Your own cke5-content.css only contains overrides:
/* assets/project/cke5-content.css */
/* Adapt colors to the project */
:root {
--ck-content-blockquote-border: #your-color;
--ck-content-pre-bg: #f5f5f5;
--ck-content-pre-color: #222;
--ck-content-pre-border: #ddd;
}These variables can be overridden in your project:
| Variable | Affects |
|---|---|
--ck-content-blockquote-border |
Blockquote border color |
--ck-content-pre-color |
Code block text color |
--ck-content-pre-bg |
Code block background |
--ck-content-pre-border |
Code block border color |
--ck-content-hr-bg |
Horizontal rule color |
--ck-content-pagebreak-label-bg |
Page break label background |
--ck-content-pagebreak-label-color |
Page break label text color |
--ck-content-pagebreak-line |
Page break line color |
--ck-content-color-image-caption-background |
Image caption background |
--ck-content-color-image-caption-text |
Image caption text color |
--ck-content-color-table-caption-background |
Table caption background |
--ck-content-color-table-caption-text |
Table caption text color |
For projects with a custom dark mode toggle (e.g. [data-theme="dark"] on <html>):
/* assets/project/cke5-content.css */
[data-theme="dark"] {
--ck-content-blockquote-border: #555;
--ck-content-pre-color: #d4d4d4;
--ck-content-pre-bg: rgba(255, 255, 255, 0.06);
--ck-content-pre-border: #444;
--ck-content-hr-bg: #444;
--ck-content-pagebreak-label-bg: #2a2a2a;
--ck-content-pagebreak-label-color: #ccc;
--ck-content-pagebreak-line: #555;
--ck-content-color-image-caption-background: #1e1e1e;
--ck-content-color-image-caption-text: #ccc;
--ck-content-color-table-caption-background: #1e1e1e;
--ck-content-color-table-caption-text: #ccc;
}
prefers-color-scheme: darkis already covered by the bundled CSS. An additional block is only needed when the project toggle is controlled via a custom CSS class or attribute.
<textarea
class="form-control cke5-editor"
data-profile="default"
data-lang="<?php echo \Cke5\Utils\Cke5Lang::getUserLang(); ?>"
data-content-lang="<?php echo \Cke5\Utils\Cke5Lang::getOutputLang(); ?>"
name="REX_INPUT_VALUE[1]"
>REX_VALUE[1]</textarea><textarea
class="form-control cke5-editor"
data-profile="default"
data-min-height="220"
data-max-height="700"
data-lang="<?php echo \Cke5\Utils\Cke5Lang::getUserLang(); ?>"
name="REX_INPUT_VALUE[2]"
>REX_VALUE[2]</textarea>REX_VALUE[id="1" output="html"]$mform = new MForm();
$mform->addTextAreaField(1, [
'label' => 'Text',
'class' => 'cke5-editor',
'data-profile' => 'default',
'data-lang' => \Cke5\Utils\Cke5Lang::getUserLang(),
'data-content-lang' => \Cke5\Utils\Cke5Lang::getOutputLang(),
]);
echo $mform->show();$id = 1;
$mform = new MForm();
$mform->addFieldset('Accordion');
$mform->addTextField("$id.0.title", ['label' => 'Title']);
$mform->addTextAreaField("$id.0.text", [
'label' => 'Text',
'class' => 'cke5-editor',
'data-profile' => 'default',
'data-lang' => \Cke5\Utils\Cke5Lang::getUserLang(),
'data-content-lang' => \Cke5\Utils\Cke5Lang::getOutputLang(),
]);
echo MBlock::show($id, $mform->show());{"class":"cke5-editor","data-profile":"default","data-lang":"en","data-content-lang":"en"}- Toolbar uses CKEditor-style identifiers (
link,insertImage,snippets, ...). - Legacy aliases are migrated/normalized internally where applicable.
- Snippets are selected per profile.
- Style groups and styles are selected per profile and merged for output config.
- In profile edit mode, language placeholders can be configured per REDAXO locale.
Several profile fields expect JSON input. This section gives working starter examples.
This profile field defines manual link decorators. They appear as switches in the link dialog and add classes or attributes to the generated <a> element.
Use the official CKEditor format: a JSON object whose keys are unique technical decorator names. Every entry contains at least mode: "manual", a label, and output configuration through classes, attributes, or styles.
{
"btnPrimary": {
"mode": "manual",
"label": "Button Primary",
"classes": ["btn", "btn-primary"],
"attributes": {
"role": "button"
}
},
"newWindow": {
"mode": "manual",
"label": "Open in a new window",
"attributes": {
"target": "_blank",
"rel": "noopener noreferrer"
}
}
}The legacy format containing a list of individual objects is still accepted, but should not be used for new configurations.
| Property | Type | Meaning |
|---|---|---|
mode |
String | Must be "manual" in this JSON field. |
label |
String | Label of the switch in the link dialog. |
classes |
String or string array | CSS classes applied to the link. An array such as ["btn", "btn-primary"] is recommended. |
attributes |
Object | HTML attributes such as role, rel, target, download, or data-*. |
styles |
Object | Inline styles as property/value pairs. Use only when classes are insufficient. |
defaultValue |
Boolean | When true, the decorator is enabled by default for new links. |
redaxoExclusiveGroup |
String | Optional CKE5 extension for mutually exclusive manual decorators. |
For CSS classes, classes is the official and recommended CKEditor property:
"classes": ["btn", "btn-default"]CKEditor can also process "attributes": {"class": "btn btn-default"} as a regular HTML attribute. New configurations should still use classes because CKEditor then explicitly handles the values as class tokens. "attributes": "classes" is invalid because attributes must always be a JSON object.
Compatible alternative using class inside attributes:
{
"btnDefault": {
"mode": "manual",
"label": "Bootstrap 3 Button Default",
"attributes": {
"class": "btn btn-default",
"role": "button"
}
}
}Classes and additional attributes can be combined:
{
"btnDefault": {
"mode": "manual",
"label": "Bootstrap 3 Button Default",
"classes": ["btn", "btn-default"],
"attributes": {
"role": "button",
"data-variant": "default"
}
}
}CKEditor treats manual decorators independently by default. If multiple button variants must not be active at the same time, assign the same redaxoExclusiveGroup value to at least two decorators:
{
"btnPrimary": {
"mode": "manual",
"label": "Button Primary",
"classes": ["btn", "btn-primary"],
"redaxoExclusiveGroup": "linkButtonStyle"
},
"btnSuccess": {
"mode": "manual",
"label": "Button Success",
"classes": ["btn", "btn-success"],
"redaxoExclusiveGroup": "linkButtonStyle"
},
"nofollow": {
"mode": "manual",
"label": "Add nofollow",
"attributes": {
"rel": "nofollow"
}
}
}redaxoExclusiveGroup is not an official CKEditor option; it is an extension provided by this addon. When saving a link, CKE5 disables the other active decorators in the same group. A group therefore has an effect only when it contains at least two entries. Independent decorators such as nofollow can remain active at the same time.
Link decorators can be enabled centrally under CKEditor 5 > Profiles > Defaults > Global settings and stored as a JSON object for all profiles with a link toolbar. Profile-specific decorators are then merged by their technical key: a profile entry wins when the same key exists, while additional global and profile entries remain available together.
The official CKEditor API also supports mode: "automatic". However, it requires an actual JavaScript function in callback, which cannot be stored as JSON. This profile field is therefore intended for manual decorators. The profile provides the blank_to_external option for automatically opening external links in a new tab.
Official API reference: CKEditor LinkConfig#decorators and LinkDecoratorManualDefinition.
Defines custom mention feeds.
[
{
"marker": "@",
"minimumCharacters": 1,
"feed": ["@support", "@sales", "@redaktion", "@admin"]
},
{
"marker": "#",
"minimumCharacters": 1,
"feed": ["#news", "#release", "#event", "#faq"]
}
]Sprog replacements are JSON-based and are exposed via { mention marker.
[
{ "id": "{{company}}", "text": "Friends Of REDAXO" },
{ "id": "{{support_mail}}", "text": "support@example.org" },
{ "id": "{{hotline}}", "text": "+49 000 123456" }
]Defines explicit image resize options used in image toolbar.
[
{ "name": "resizeImage:original", "label": "Original", "value": null },
{ "name": "resizeImage:25", "label": "25%", "value": "25" },
{ "name": "resizeImage:50", "label": "50%", "value": "50" },
{ "name": "resizeImage:75", "label": "75%", "value": "75" }
]Note: the addon normalizes names internally for profile output.
Adds additional typing transformations.
[
{ "from": "->", "to": "→" },
{ "from": "<-", "to": "←" },
{ "from": "(c)", "to": "©" },
{ "from": "(r)", "to": "®" }
]Allow additional elements/attributes/classes/styles.
[
{
"name": "regex(/^(section|article|div)$/)",
"attributes": true,
"classes": true,
"styles": true
},
{
"name": "a",
"attributes": ["target", "rel", "data-bs-toggle", "data-bs-target"],
"classes": ["btn", "btn-primary", "btn-outline-secondary"],
"styles": false
}
]Disallow specific patterns even if allowed elsewhere.
[
{
"name": "script",
"attributes": true,
"classes": true,
"styles": true
},
{
"name": "*",
"attributes": ["on.*"]
}
]Advanced raw merge into generated profile JSON. Use with care.
{
"removePlugins": ["Autoformat"],
"heading": {
"options": [
{ "model": "paragraph", "title": "Paragraph", "class": "ck-heading_paragraph" },
{ "model": "heading2", "view": "h2", "title": "H2", "class": "ck-heading_heading2" }
]
}
}Tip: if you define removePlugins here, it is merged with existing remove list.
- Always use valid JSON (double quotes, no trailing commas).
- Start with small JSON snippets and test in one profile first.
- If a profile fails to behave as expected, open profile preview and inspect generated JSON.
Templates are no longer part of the active workflow. Use snippets for reusable editor content blocks.
Recommended workflow:
- Create snippets in
Profiles > Customise > Snippets. - Assign snippets to one or more profiles.
- Add
snippetsbutton to profile toolbar.
Profiles > Export exports selected profiles including linked dependencies.
Export payload includes:
profilesstyle_groupsstylessnippets
Profiles > Import supports:
- New bundle format (profiles + dependencies)
- Legacy profile-only format
Import performs ID-based upsert for bundled tables and then profile import.
The same bundles can be written and read without the backend, which is what a versioned profile set in a project repository or a deployment step needs:
# all profiles, indented
redaxo/bin/console cke5:export redaxo/data/addons/project/cke5_profiles.json --pretty
# a selection
redaxo/bin/console cke5:export bundle.json content hero
# import; an existing profile of the same name is kept unless it is named
redaxo/bin/console cke5:import bundle.json --overwrite=content --overwrite=hero
redaxo/bin/console cke5:import bundle.json --overwrite-allcke5:export produces the same payload as the export page, and cke5:import
regenerates cke5profiles.js afterwards — without that the editor keeps using
the profile definitions it already has.
CKEditor 5 > Config provides:
- License key configuration
- Upload/replace of editor runtime files (
.js,.js.map) - Upload/replace translation files (
.js)
Default runtime path is modern build under:
assets/addons/cke5/vendor/ckeditor5-modern/
Programmatically create an expert profile:
use Cke5\Creator\Cke5ProfilesApi;
$definition = json_encode([
'toolbar' => [
'items' => ['heading', '|', 'bold', 'italic', 'link', 'snippets', 'undo', 'redo'],
],
], JSON_UNESCAPED_UNICODE);
Cke5ProfilesApi::addProfile(
'project_expert',
'Project expert profile',
$definition,
null
);You can combine:
- static custom CSS in addon/project assets
- generated CSS from style/style-group definitions
- optional external CSS paths configured per style/style group
The addon regenerates backend CSS artifacts when style/style-group data changes.
For build sources, runtime plugin architecture, and external plugin integration, see:
PLUGIN_DEVELOPMENT.md
- Check that textarea has class
cke5-editor. - Verify
data-profileexists. - Verify
cke5profiles.jsis generated and loaded.
- Ensure profile image toolbar contains
linkImage. - Ensure build contains
LinkImageplugin. - Hard-reload backend after JS updates.
- Verify configured translation path and files in Config page.
- Ensure profile/user language maps to available CKEditor translation files.
Project Lead
Thanks to all Contributors who have shaped CKEditor 5 for REDAXO over the years!
Top Contributors
| Contributor | |
|---|---|
| 1 | crydotsnake |
| 2 | interweave-media |
| 3 | staabm |
| 4 | nandes2062 |
| 5 | TobiasKrais |
| 6 | schuer |
| 7 | marcohanke |
| 8 | eaCe |
A big thank you also to aeberhard, Bio-GitHub, dergel, V-Simos, VIEWSION, ynamite and ischfr.
Image credits
Demo content image: Frankfurt am Main Skyline by Leonhard_Niederwimmer on Pixabay — free to use under the Pixabay Content License.
Friends of REDAXO logo: friendsofredaxo.github.io — © Friends of REDAXO.
A project by Friends Of REDAXO