Update to CKEditor 5 v49.x
Let an AI coding agent do the update for you. The official ckeditor-update skill walks your agent through every update guide between your version and the target one and applies the changes to your code. Install the CKEditor 5 skills:
npx skills add ckeditor/skillsCopy codeThen ask your agent, for example: “Update CKEditor 5 to the latest version” or name the version you want. Review the changes before you commit them. See the Using CKEditor 5 with AI coding agents guide for setup options and supported agents.
Version 49 ships a refreshed default editor theme built on a reorganized set of design tokens (CSS variables). The default look changed, and many token names were renamed or restructured. Overriding the old names continues to work through backward-compatible fallbacks, but reading old names or depending on specific old values may need adjustments.
If you customized the editor’s appearance, start with the Migrating to the refreshed theme guide, which covers what changed, the backward-compatibility mechanisms, and how to restore the previous look. To work with the new tokens, see the Theme customization and Theme token naming guides.
The Watchdog is gone in v49. That removes the @ckeditor/ckeditor5-watchdog package, the EditorWatchdog and ContextWatchdog classes, and the static fields that exposed them on every editor class. Nothing restarts a crashed editor anymore, and no editor content is saved or restored for you. To learn why we removed it, see the announcement on GitHub.
In its place, onEditorError() reports the errors that escape a running editor, together with the editor or context they were attributed to, and your application decides what happens next. The error handling guide covers the options; the migrating from the Watchdog guide covers the move in plain JavaScript and in each of the framework integrations.
This changes the default behavior for everyone using the React, Vue, or Angular integrations, not only for those who configured a watchdog. All three attached one on your behalf. An editor that used to be rebuilt after a crash now stays as it is, but its state may no longer be consistent, so handle the error as described in the Error handling guide. The integrations also now require CKEditor 5 in version 49 or higher.
ActionsRecorder was not removed with the package. It moved to @ckeditor/ckeditor5-core, so change the specifier if you imported it from the Watchdog package. Importing it from ckeditor5 keeps working unchanged.
CKEditor 5 can now run inside an open shadow root. Selection, focus, positioning, scrolling, drag and drop, and the floating user interface all work there, and so do the premium features. Closed shadow roots are not supported.
A shadow root is a separate styling boundary, so two things do not happen by themselves:
- The editor style sheets have to be loaded into every tree that holds the editor user interface: the shadow root the editor lives in, and the tree its floating user interface mounts in.
- CSS variables have to be overridden on every shadow host that holds the editor user interface, including the host of the overlay container, instead of on
:root. The editor style sheets now declare their variables on both:rootand:host, so they resolve in both places. If your custom style sheet declares variables of its own, declare them on both selectors as well.
By default, balloons, dialogs, and tooltips mount in the tree the editing root lives in. Inside a shadow root, the shadow host or one of its ancestors may then clip or misplace them, for example when it has overflow: hidden or position: relative. That is why an editor in a shadow root logs the ui-overlay-container-not-configured warning. The new config.ui.overlayContainer option lets you mount them elsewhere, and setting it silences the warning. We recommend a dedicated shadow root at the end of <body>, with the editor style sheets adopted into it. The Where the floating user interface mounts section of the Shadow DOM guide shows how to set it up.
Some features with a floating user interface of their own accept a container option as well: config.sidebar.overlayContainer, config.presenceList.overlayContainer, and config.ai.overlayContainer. When they are not set, these features use config.ui.overlayContainer. In a setup with a Context, the sidebar and the presence list read these options from the context configuration, so set config.ui.overlayContainer there as well.
The Shadow DOM guide covers the setup in detail. Each framework integration guide has a section on using the editor inside a shadow root:
Some changes apply even if you do not use a shadow root:
- The body collection is attached later, and once per mount target. There is one
.ck-body-wrapperelement per mount target now, not one per page. Editors in the light DOM without an overlay container still share the one in<body>, but an editor in a shadow root, or one withconfig.ui.overlayContainerset, uses a wrapper in that root or container. The wrapper is not in the DOM until the editing root is connected to the document. Instead ofdocument.querySelector( '.ck-body-wrapper' ), readeditor.ui.view.body.bodyCollectionContainer. It is the editor’s own.ck-bodyelement inside the wrapper, and it is available as soon as the editor is created. - Page-level rules moved out of the theme style sheets. The editor now adopts the rules behind the
ck-fullscreen-scroll-lockedandck-dialog-scroll-lockedclasses into the document at runtime. They come later in the cascade than the theme style sheets, so overriding them may need higher specificity. - The fullscreen mode has a new default container. When
config.fullscreen.containeris not set, the fullscreen mode mounts inconfig.ui.overlayContainer, then in the shadow root the editor lives in, and only then in<body>. The option no longer has a default value, soeditor.config.get( 'fullscreen.container' )returnsundefinedwhen you do not set it. Whenconfig.fullscreen.containerpoints at an element of your layout, the fullscreen mode fills that element instead of covering the viewport. Its wrapper then gets the newck-fullscreen__main-wrapper_custom-containerclass, which custom CSS may need to target.
Native DOM APIs such as document.activeElement, window.getSelection(), and Node#contains() give wrong answers inside a shadow root, and nothing throws. Custom plugins that use them should switch to the shadow-aware helpers exported from the ckeditor5 package. The Shadow-aware helpers section of the deep dive guide explains what each one replaces and when to use it:
- Focus, selection, and hit-testing:
getActiveElement(),getSelection(),containsNode(), andgetElementFromPoint(). - Walking up the tree:
getParentNode()andgetParentElement()for structure,getLayoutParentNode()andgetLayoutParentElement()for geometry. - Shadow roots:
getShadowRoots(),isShadowRoot(),isShadowHostOf(),ShadowRootRegistry, andlistenToShadowRoots(). - Floating user interface and styles:
getOverlayMountRoot()andadoptGlobalStyleSheet().
Floating user interface that lives outside an editor uses OverlayHost instead. The Floating UI renders unstyled, or is clipped by its own container section of the deep dive guide shows how to set it up.
The Porting an existing feature section of the deep dive guide walks you through updating an existing feature, including the ESLint rules that find the code to change.
If you display the CKEditor AI user interface in the 'custom' container type, the floating user interface of the AI features — their balloons, dialogs, and dropdowns — is no longer mounted in document.body by each AI feature separately. It is mounted in the DOM tree the AI user interface lives in, and in this type only your integration knows which tree that is, so set AITabs#container to the container you placed the AI user interface in.
Left unset, the floating user interface falls back to document.body, which may leave it unstyled if the AI user interface runs inside a shadow root. When config.ui.overlayContainer is set, the floating user interface mounts in it instead, whatever AITabs#container holds.
.then( editor => {
const tabsPlugin = editor.plugins.get( 'AITabs' );
for ( const id of tabsPlugin.view.getTabIds() ) {
const tab = tabsPlugin.view.getTab( id );
// Display tab button and panel in a custom container.
myButtonsContainer.appendChild( tab.button.element );
myPanelContainer.appendChild( tab.panel.element );
}
// Tells the AI features which DOM tree their floating UI belongs in.
tabsPlugin.container = myPanelContainer;
} );Copy codeAlternatively, point the new config.ai.overlayContainer option at a container of your own. It takes precedence over the container above, and it is the better choice when the container you placed the AI user interface in is positioned, scrollable, or clipping — a floating UI placed inside such a container drifts away from what it is pinned to as the page scrolls:
ClassicEditor
.create( {
attachTo: document.querySelector( '#editor' ),
ai: {
container: { type: 'custom' },
// A container of your own, at the end of `<body>`, with nothing above it to clip or shift it.
overlayContainer: document.querySelector( '#ai-overlay-container' )
}
} );Copy codeSee the overlay UI container section of the integration guide for details, including what to point it at when the AI user interface runs inside a shadow root.
-
ckeditor5, core, utils: The
@ckeditor/ckeditor5-watchdogpackage was removed, and with it the automatic restart of a crashed editor. An editor that crashes now stays as it is instead of being rebuilt from the data it had before, but its state may no longer be consistent.- The
EditorWatchdogandContextWatchdogclasses are gone, as are theWatchdogbase class and theWatchdogConfigtype. They are no longer re-exported fromckeditor5. - The
Editor.EditorWatchdogandEditor.ContextWatchdogstatic fields were removed from every editor class. - Use
onEditorError()to observe errors instead. It reports the error together with the editor or context it came from and returns a function that unregisters the callback. The same function is reachable asEditor.onEditorError()andContext.onEditorError(). - If you used
ContextWatchdogto share a context between editors, create theContextyourself and pass it in the editor configuration. You now have to destroy the context yourself, whichContextWatchdogused to do for you.
- The
-
core:
ActionsRecordermoved from@ckeditor/ckeditor5-watchdogto@ckeditor/ckeditor5-core. TheActionsRecorderConfig,ActionsRecorderEntry,ActionsRecorderEntryEditorSnapshot,ActionsRecorderErrorCallback,ActionsRecorderFilterCallback, andActionsRecorderMaxEntriesCallbacktypes moved with it, as did theconfig.actionsRecorderdeclaration. Code importing fromckeditor5needs no change. Code importing from@ckeditor/ckeditor5-watchdoghas to import from@ckeditor/ckeditor5-coreinstead. -
ui: The
TooltipManagerconstructor is private now. As before, there is one shared instance per page. Replacenew TooltipManager( editor )withTooltipManager.for( locale ), andtooltipManager.destroy( editor )withtooltipManager.release(). Callrelease()once for everyfor()call: the instance counts its holders and is destroyed when the last one releases it. The manager is no longer tied to editors. Each editor registers its own body collection withTooltipManager#registerBodyCollection(), and UI that renders in a body collection of its own, outside an editor, has to do the same to display tooltips. Such UI unregisters its collection withTooltipManager#unregisterBodyCollection()when it is destroyed. Code that only readseditor.ui.tooltipManagerneeds no change. -
ui: The
BodyCollection#detachFromDom()method is renamed toBodyCollection#destroy(). It still destroys the views and removes their container from the DOM. ReplacedetachFromDom()calls withdestroy(). If your code calleddetachFromDom()and thendestroy(), keep onlydestroy(). To remove the container from the DOM without destroying the views, so that you can attach it again later, use the newBodyCollection#unmountFromDom()method.
- ai:
AITabs#containeris an observable property of theHTMLElement | ShadowRoot | nulltype now. With the'overlay'container type, it holds a shadow root when the editor lives in one. Update code that expects anHTMLElementthere. See Changes to using CKEditor AI features in the custom UI mode. - engine: The
ViewRenderer#domDocumentsproperty is removed. There is no public replacement. - ui: The
clickOutsideHandler()function no longer accepts thelistenerOptionsoption. Remove it from the call. The function sets the priority and capture mode of its listeners itself. - uploadcare: The
uc-configanduc-upload-ctx-providerelements are added to the editor body collection instead ofdocument.body. UseUploadcareEditing#configElementandUploadcareEditing#ctxElementinstead of looking them up in the document. - utils: The
getCommonAncestor()DOM utility is removed. Walk the ancestors of both nodes withgetParentNode()and compare the chains. For the editor tree, use thegetCommonAncestor()methods of the model and view. - utils: The
getPositionedAncestor()function returnsnullfor an element that is not connected to a document. Previously, it only required the element to have a parent. It also behaves differently in these cases:- It returns
<body>when a style such asposition: relativeortransformmakes the body the containing block. Previously, it returnednullfor the main document’s<body>in every case. - For an element inside an iframe, it returns
nullinstead of the iframe’s static<body>, as it already did in the main document. - For an element assigned to a
<slot>, it looks for the positioned ancestor in the shadow tree that renders the element.
- It returns