CKEditor 5 Roundtrip
This module provides plugins to fix issues where CKEditor 5 incorrectly modifies or removes certain HTML elements when switching between editing modes or saving content. It preserves script tags, empty anchor targets, and legacy HTML elements, while also cleaning up unwanted attributes and classes that are added by CKEditor or Drupal core.
Fixes HTML content loss and mutation that occurs when editors toggle between Source and WYSIWYG mode in Drupal's CKEditor 5. CKEditor 5's General HTML Support (GHS) layer and its model normalisation rules silently discard or rewrite certain HTML elements on every toggle — or on every Save.
This module provides 11 independent, opt-in plugins. Each plugin is a documented workaround for a named, tracked defect in Drupal core or CKEditor 5 upstream. The relevant issue links are included so you can monitor upstream progress and retire a plugin when the fix lands in core.
Development of this module involved AI-assisted tooling. All code has been reviewed and tested by the maintainer.
Plugins
ScriptRoundtrip (ckeditor5_roundtrip_script)
Prevents <script> tags from being stripped after a Source → WYSIWYG toggle. Also prevents JS operators (<, >, &&) from being entity-encoded when the script body passes through the DOM parser.
Upstream: drupal.org #3570973, ckeditor5 #10891
⚠ Trusted editors / Full HTML formats only.
ScriptContainerRoundtrip (ckeditor5_roundtrip_script_container)
Preserves <div><script>…</script></div> ad-tag and tracking-pixel patterns. Without this plugin, ScriptRoundtrip consumes the inner <script> leaving the parent <div> childless; CKEditor's model post-fixers then discard the container along with all its data-*, id, and other attributes.
⚠ Trusted editors / Full HTML formats only.
AnchorRoundtrip (ckeditor5_roundtrip_anchor)
Prevents empty <a id="top"></a> and <a name="section"></a> anchor targets from disappearing on toggle. CKEditor's model cannot represent empty inline elements and silently discards them, breaking in-page links.
Upstream: drupal.org #3337298, ckeditor5 #9888
ListIdsRoundtrip (ckeditor5_roundtrip_listids)
Strips the data-list-item-id tracking attributes that CKEditor 5's DocumentList plugin injects onto every <li> before saving. These are editor internals not intended for storage.
Upstream: ckeditor5 #18407, ckeditor5 #19006
TableRoundtrip (ckeditor5_roundtrip_table)
Removes the class="table" Bootstrap class that Drupal core's PlainTableOutput injects on <table> elements. Sites not using Bootstrap receive unintended styling from this injected class.
Upstream: ckeditor5 #11379, #11394, #13164
TableRowspanRoundtrip (ckeditor5_roundtrip_table_rowspan)
Preserves rowspan and colspan attributes on <td>/<th> without requiring the Merge table cells toolbar button. Without this plugin, GHS has no allow-rule for these attributes when the merge UI is absent and silently strips them, collapsing multi-row and multi-column cells.
LegacyHtmlRoundtrip (ckeditor5_roundtrip_legacy)
Prevents <center> from gaining a spurious <p> wrapper on toggle, and preserves attributes on <font> elements. Both are HTML4 elements unknown to CKEditor 5 that are mutated or stripped without explicit registration.
Upstream: drupal.org #3268306, #3418322, ckeditor5 #16203
InlineStyleRoundtrip (ckeditor5_roundtrip_inline_style)
Prevents font-weight:bold and font-style:italic from being converted to <strong> and <em> respectively. CKEditor 5's Bold and Italic plugins consume these CSS properties at normal upcast priority and discard the original inline style declaration.
ListStyleRoundtrip (ckeditor5_roundtrip_liststyle)
Preserves CSS list-style-type on <ol>/<ul> elements instead of converting it to the HTML4 type attribute. Also prevents adjacent <ol> elements with distinct list styles from being merged into one by CKEditor's list normalisation pass.
StyleRoundtrip (ckeditor5_roundtrip_style)
Preserves block <style> elements at their original document position. GHS inserts <style> at model position 0 (the start of $root) rather than at its correct structural position during upcast — every toggle silently hoists the block to the top of the content.
AlignmentRoundtrip (ckeditor5_roundtrip_alignment)
Three fixes in one plugin:
- CSS classes → inline style — Drupal's Alignment plugin emits
class="text-align-center"by default. This plugin overrides the downcast to emitstyle="text-align:center"instead, so alignment works on themes that provide no class-based rules. - Restore
align=on table cells — CKEditor's TableCellProperties plugin convertsalign="center"on<td>/<th>tostyle="text-align:center"with no corresponding downcast. This plugin post-processesgetData()output to restore the attribute, preserving layout in email clients that honouralign=but notstyle=. - Strip injected pixel dimensions — CKEditor's ImageSizeAttributes plugin reads
naturalWidth/naturalHeightafter each image loads and writes them aswidth/heightHTML attributes. This plugin strips those attributes from images already sized via CSS, where the pixel attributes would override the CSS constraints.
Requirements
- Drupal 10.3 or 11.x
- Core modules:
ckeditor5,filter - The Script and Script Container plugins require a text format without
filter_html(Full HTML or equivalent) and should only be enabled for trusted editors.
Installation
composer require drupal/ckeditor5_roundtrip drush en ckeditor5_roundtrip
Then enable individual plugins at Administration → Configuration → Content authoring → Text formats and editors → select a format → Configure → Available plugins.
Related issues
All upstream and Drupal core issues tracked by this module are linked in the README.
Depends on
Dependencies of the latest stable release
No dependencies recorded for this project.
Required by
Tracked projects that depend on this one
No tracked projects depend on this one yet.
Activity
Release Timeline
Releases
| Version | Type | Core | Release date | |
|---|---|---|---|---|
| 1.0.0-beta10 | Pre-release | 10–11 | Sep 7, 2026 | |
| 1.0.0-beta9 | Pre-release | 10–11 | Sep 7, 2026 | |
| 1.0.0-beta8 | Pre-release | 10–11 | Sep 2, 2026 | |
| 1.0.0-beta7 | Pre-release | 10–11 | Sep 2, 2026 | |
| 1.0.0-beta6 | Pre-release | 10–11 | Aug 21, 2026 | |
| 1.0.0-beta5 | Pre-release | 10–11 | Aug 20, 2026 | |
| 1.0.0-beta4 | Pre-release | 10–11 | Aug 17, 2026 | |
| 1.0.0-beta3 | Pre-release | 10–11 | Aug 17, 2026 | |
| 1.0.0-beta2 | Pre-release | 10–11 | Aug 14, 2026 | |
| 1.0.0-beta1 | Pre-release | 10–11 | Aug 14, 2026 | |
| 1.0.0-alpha6 | Pre-release | 10–11 | Aug 14, 2026 | |
| 1.0.0-alpha5 | Pre-release | 10–11 | Aug 14, 2026 | |
| 1.0.0-alpha4 | Pre-release | 10–11 | Aug 14, 2026 | |
| 1.0.0-alpha3 | Pre-release | 10–11 | Aug 14, 2026 | |
| 1.0.0-alpha2 | Pre-release | 10–11 | Aug 13, 2026 | |
| 1.0.0-alpha1 | Pre-release | 10–11 | Aug 13, 2026 |