diff --git a/.gitignore b/.gitignore
index 7579f74311d35aae05dd0f0a54537ea7a0034e89..15188ce87a7dd11fd45c479d4f8a41c5bd026690 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
 vendor
 composer.lock
+node_modules
+package.lock
diff --git a/entity_embed.libraries.yml b/entity_embed.libraries.yml
index c92858a6a8d842734c914bea60058ff4925561f8..75a5fc7b096207534e089f9b7cf1f4ecef43c496 100644
--- a/entity_embed.libraries.yml
+++ b/entity_embed.libraries.yml
@@ -17,3 +17,20 @@ caption:
       css/entity_embed.filter.caption.css: {}
   dependencies:
     - filter/caption
+
+# CKEditor 5
+entity_embed:
+  js:
+    js/build/drupalentity.js: { minified: true }
+  dependencies:
+    - core/ckeditor5
+    - core/drupal
+
+admin.entity_embed:
+  js:
+    js/entity_embed.set_dynamic_icons.js: {}
+  dependencies:
+    - core/drupal
+    - core/jquery
+    - core/once
+    - core/drupalSettings
diff --git a/entity_embed.module b/entity_embed.module
index 89e28c09bac388ed30e4ff99a4e5c31f93e51573..1903afbdfe8cd06f60d4af291bf5eeaa6a8d26e5 100644
--- a/entity_embed.module
+++ b/entity_embed.module
@@ -5,11 +5,18 @@
  * Framework for allowing entities to be embedded in CKEditor.
  */
 
+use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Entity\EntityInterface;
 use Drupal\Component\Serialization\Json;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\embed\EmbedButtonInterface;
+use Drupal\embed\Entity\EmbedButton;
+use Drupal\entity_embed\Event\EmbedButtonEvent;
 
 /**
  * Implements hook_help().
@@ -298,3 +305,57 @@ function entity_embed_field_widget_single_element_form_alter(&$element, FormStat
     $element['#attributes']['data-entity_embed-host-entity-langcode'] = $items->getEntity()->language()->getId();
   }
 }
+
+/**
+ * Implements hook_ENTITY_TYPE_presave().
+ */
+function entity_embed_embed_button_presave(EntityInterface $entity) {
+  // Invalidate the CKEditor 5 plugin cache, so new toolbar items will appear
+  // based on which Embed Buttons are configured.
+  /** @var \Drupal\ckeditor5\Plugin\CKEditor5PluginManagerInterface $ckeditor5_plugin_manager */
+  if (\Drupal::hasService('plugin.manager.ckeditor5.plugin')) {
+    $ckeditor5_plugin_manager = \Drupal::service('plugin.manager.ckeditor5.plugin');
+    $ckeditor5_plugin_manager->clearCachedDefinitions();
+    // @see entity_embed_library_info_alter()
+    Cache::invalidateTags(['library_info']);
+  }
+}
+
+/**
+ * Implements hook_library_info_alter().
+ */
+function entity_embed_library_info_alter(&$libraries, $extension) {
+  // Create the drupalSettings dynamically based on embed buttons for the
+  // admin library.
+  if ($extension === 'entity_embed' && isset($libraries['admin.entity_embed'])) {
+    $embed_buttons = \Drupal::entityTypeManager()
+      ->getStorage('embed_button')
+      ->loadByProperties(['type_id' => 'entity']);
+    foreach ($embed_buttons as $embed_button) {
+      $libraries['admin.entity_embed']['drupalSettings']['embedButtons'][$embed_button->id()] = [
+        'id' => $embed_button->id(),
+        'icon' => $embed_button->getIconUrl(),
+      ];
+
+    }
+    $libraries['admin.entity_embed']['drupalSettings']['modulePath'] = \Drupal::service('extension.list.module')->getPath('entity_embed');
+  }
+}
+
+/**
+ * Implements hook_ckeditor4to5upgrade_plugin_info_alter().
+ */
+function entity_embed_ckeditor4to5upgrade_plugin_info_alter(array &$plugin_definitions): void {
+  // Get the user set buttons and add them to the entity embed plugin definition
+  $buttons = [];
+  $embed_buttons = \Drupal::entityTypeManager()
+    ->getStorage('embed_button')
+    ->loadMultiple();
+  foreach ($embed_buttons as $embed_button) {
+    $buttons[] = $embed_button->id();
+  }
+
+  if ($buttons) {
+    $plugin_definitions['entity_embed']["cke4_buttons"] = $buttons;
+  }
+}
diff --git a/js/build/drupalentity.js b/js/build/drupalentity.js
new file mode 100644
index 0000000000000000000000000000000000000000..20d26956355604a6a8cab7b5f92e9dd2b4d944d5
--- /dev/null
+++ b/js/build/drupalentity.js
@@ -0,0 +1 @@
+!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.CKEditor5=e():(t.CKEditor5=t.CKEditor5||{},t.CKEditor5.drupalentity=e())}(self,(()=>(()=>{var t={"ckeditor5/src/core.js":(t,e,i)=>{t.exports=i("dll-reference CKEditor5.dll")("./src/core.js")},"ckeditor5/src/ui.js":(t,e,i)=>{t.exports=i("dll-reference CKEditor5.dll")("./src/ui.js")},"ckeditor5/src/widget.js":(t,e,i)=>{t.exports=i("dll-reference CKEditor5.dll")("./src/widget.js")},"dll-reference CKEditor5.dll":t=>{"use strict";t.exports=CKEditor5.dll}},e={};function i(r){var n=e[r];if(void 0!==n)return n.exports;var o=e[r]={exports:{}};return t[r](o,o.exports,i),o.exports}i.d=(t,e)=>{for(var r in e)i.o(e,r)&&!i.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:e[r]})},i.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var r={};return(()=>{"use strict";i.d(r,{default:()=>u});var t=i("ckeditor5/src/core.js"),e=i("ckeditor5/src/widget.js");class n extends t.Command{execute(t){const{model:e}=this.editor,i=this.editor.plugins.get("EntityEmbedEditing"),r=Object.entries(i.attrs).reduce(((t,[e,i])=>(t[i]=e,t)),{}),n=Object.keys(t).reduce(((e,i)=>(r[i]&&(e[r[i]]=t[i]),e)),{});e.change((t=>{e.insertContent(function(t,e){return t.createElement("drupalEntity",e)}(t,n))}))}refresh(){const t=this.editor.model,e=t.document.selection,i=t.schema.findAllowedParent(e.getFirstPosition(),"drupalEntity");this.isEnabled=null!==i}}class o extends t.Plugin{static get requires(){return[e.Widget]}init(){this.attrs={alt:"alt",title:"title",dataCaption:"data-caption",dataAlign:"data-align",drupalEntityLangCode:"data-langcode",drupalEntityEntityType:"data-entity-type",drupalEntityEntityUuid:"data-entity-uuid",drupalEntityEmbedButton:"data-embed-button",drupalEntityEmbedDisplay:"data-entity-embed-display",drupalEntityEmbedDisplaySettings:"data-entity-embed-display-settings"};const t=this.editor.config.get("entityEmbed");t&&(this.options=t,this.labelError=Drupal.t("Preview failed"),this.previewError=`\n      <p>${Drupal.t("An error occurred while trying to preview the embedded content. Please save your work and reload this page.")}<p>\n    `,this._defineSchema(),this._defineConverters(),this.editor.commands.add("insertEntityEmbed",new n(this.editor)))}_defineSchema(){this.editor.model.schema.register("drupalEntity",{isObject:!0,isContent:!0,isBlock:!0,allowWhere:"$block",allowAttributes:Object.keys(this.attrs)}),this.editor.editing.view.domConverter.blockElements.push("drupal-entity")}_defineConverters(){const{conversion:t}=this.editor;t.for("upcast").elementToElement({model:"drupalEntity",view:{name:"drupal-entity"}}),t.for("dataDowncast").elementToElement({model:"drupalEntity",view:{name:"drupal-entity"}}),t.for("editingDowncast").elementToElement({model:"drupalEntity",view:(t,{writer:i})=>{const r=i.createContainerElement("figure",{class:"drupal-entity"});return i.setCustomProperty("drupalEntity",!0,r),(0,e.toWidget)(r,i,{label:Drupal.t("Entity Embed widget")})}}).add((t=>(t.on("attribute:drupalEntityEntityUuid:drupalEntity",((t,e,i)=>{const r=i.writer,n=e.item,o=i.mapper.toViewElement(e.item);let a=this._getPreviewContainer(o.getChildren());if(a){if("ready"!==a.getAttribute("data-drupal-entity-preview"))return;r.setAttribute("data-drupal-entity-preview","loading",a)}else a=r.createRawElement("div",{"data-drupal-entity-preview":"loading"}),r.insert(r.createPositionAt(o,0),a);this._loadPreview(n).then((({label:t,preview:e})=>{a&&this.editor.editing.view.change((i=>{const r=i.createRawElement("div",{"data-drupal-entity-preview":"ready","aria-label":t},(t=>{t.innerHTML=e}));i.insert(i.createPositionBefore(a),r),i.remove(a)}))}))})),t))),Object.keys(this.attrs).forEach((e=>{const i={model:{key:e,name:"drupalEntity"},view:{name:"drupal-entity",key:this.attrs[e]}};t.for("dataDowncast").attributeToAttribute(i),t.for("upcast").attributeToAttribute(i)}))}async _loadPreview(t){const e={text:this._renderElement(t)},i=await fetch(Drupal.url("embed/preview/"+this.options.format+"?"+new URLSearchParams(e)),{headers:{"X-Drupal-EmbedPreview-CSRF-Token":this.options.previewCsrfToken}});if(i.ok){return{label:Drupal.t("Entity Embed widget"),preview:await i.text()}}return{label:this.labelError,preview:this.previewError}}_renderElement(t){const e=this.editor.model.change((e=>{const i=e.createDocumentFragment(),r=e.cloneElement(t,!1);return e.append(r,i),i}));return this.editor.data.stringify(e)}_getPreviewContainer(t){for(const e of t){if(e.hasAttribute("data-drupal-entity-preview"))return e;if(e.childCount){const t=this._getPreviewContainer(e.getChildren());if(t)return t}}return null}static get pluginName(){return"EntityEmbedEditing"}}var a=i("ckeditor5/src/ui.js");class d extends t.Plugin{static get requires(){return[e.WidgetToolbarRepository]}init(){const e=this.editor,i=e.plugins.get("EntityEmbedEditing"),r=e.config.get("entityEmbed"),{dialogSettings:n={}}=r;e.ui.componentFactory.add("entityEmbedEdit",(o=>{let d=new a.ButtonView(o);return d.set({label:e.t("Edit"),icon:t.icons.pencil,tooltip:!0}),this.listenTo(d,"execute",(t=>{let o=e.model.document.selection.getSelectedElement(),a=Drupal.url("entity-embed/dialog/"+r.format+"/"+o.getAttribute("drupalEntityEmbedButton")),d={};for(let[t,e]of o.getAttributes()){let r=i.attrs[t];r&&(d[r]=e)}this._openDialog(a,d,(({attributes:t})=>{e.execute("insertEntityEmbed",t),e.editing.view.focus()}),n)})),d}))}afterInit(){const{editor:t}=this;if(!t.plugins.has("WidgetToolbarRepository"))return;t.plugins.get("WidgetToolbarRepository").register("entityEmbed",{items:["entityEmbedEdit"],getRelatedElement(t){const i=t.getSelectedElement();return i&&(0,e.isWidget)(i)&&i.getCustomProperty("drupalEntity")?i:null}})}_openDialog(t,e,i,r){const n=r.dialogClass?r.dialogClass.split(" "):[];n.push("ui-dialog--narrow"),r.dialogClass=n.join(" "),r.autoResize=window.matchMedia("(min-width: 600px)").matches,r.width="auto";Drupal.ajax({dialog:r,dialogType:"modal",selector:".ckeditor5-dialog-loading-link",url:t,progress:{type:"fullscreen"},submit:{editor_object:e}}).execute(),Drupal.ckeditor5.saveCallback=i}static get pluginName(){return"EntityEmbedToolbar"}}class s extends t.Plugin{static get requires(){return["Widget"]}init(){const t=this.editor,e=t.commands.get("insertEntityEmbed"),i=t.config.get("entityEmbed");t.editing.view.document;if(!i)return;const{dialogSettings:r={}}=i,n=i.buttons;Object.keys(n).forEach((o=>{let d=Drupal.url("entity-embed/dialog/"+i.format+"/"+o);t.ui.componentFactory.add(o,(i=>{let s=n[o],l=new a.ButtonView(i),u=null;if(s.icon.endsWith("svg")){let t=new XMLHttpRequest;t.open("GET",s.icon,!1),t.send(null),u=t.response}return l.set({label:s.label,icon:u??'<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="16px" height="16px" viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">  <image id="image0" width="16" height="16" x="0" y="0"\n    href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAALGPC/xhBQAAAAJiS0dE\nAP+Hj8y/AAAAB3RJTUUH5gkNAywZrK+VpAAAAElJREFUKM9jYKAeuMrwHwUuhwizoCj6xfALzv6J\nzYRKTIOZCNmMaoUpQzKcvZnhFX5HWmIz4SrDDTj7LbUcSVABbkfeJ9kEcgEApvsllE2X4VkAAAAl\ndEVYdGRhdGU6Y3JlYXRlADIwMjItMDktMTNUMDE6NDQ6MjUrMDI6MDCMUacyAAAAJXRFWHRkYXRl\nOm1vZGlmeQAyMDIyLTA5LTEzVDAxOjQ0OjI1KzAyOjAw/QwfjgAAAABJRU5ErkJggg==" />\n</svg>\n',tooltip:!0}),l.bind("isOn","isEnabled").to(e,"value","isEnabled"),this.listenTo(l,"execute",(()=>Drupal.ckeditor5.openDialog(d,(({attributes:e})=>{t.execute("insertEntityEmbed",e)}),r))),l}))}))}static get pluginName(){return"EntityEmbedUI"}}class l extends t.Plugin{static get requires(){return[o,s,d]}static get pluginName(){return"EntityEmbed"}}const u={EntityEmbed:l}})(),r=r.default})()));
\ No newline at end of file
diff --git a/js/ckeditor5_plugins/drupalentity/entity.svg b/js/ckeditor5_plugins/drupalentity/entity.svg
new file mode 100644
index 0000000000000000000000000000000000000000..5c6a180d1cb92d3b51907f963854883cd0069070
--- /dev/null
+++ b/js/ckeditor5_plugins/drupalentity/entity.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="16px" height="16px" viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">  <image id="image0" width="16" height="16" x="0" y="0"
+    href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAALGPC/xhBQAAAAJiS0dE
+AP+Hj8y/AAAAB3RJTUUH5gkNAywZrK+VpAAAAElJREFUKM9jYKAeuMrwHwUuhwizoCj6xfALzv6J
+zYRKTIOZCNmMaoUpQzKcvZnhFX5HWmIz4SrDDTj7LbUcSVABbkfeJ9kEcgEApvsllE2X4VkAAAAl
+dEVYdGRhdGU6Y3JlYXRlADIwMjItMDktMTNUMDE6NDQ6MjUrMDI6MDCMUacyAAAAJXRFWHRkYXRl
+Om1vZGlmeQAyMDIyLTA5LTEzVDAxOjQ0OjI1KzAyOjAw/QwfjgAAAABJRU5ErkJggg==" />
+</svg>
diff --git a/js/ckeditor5_plugins/drupalentity/src/command.js b/js/ckeditor5_plugins/drupalentity/src/command.js
new file mode 100644
index 0000000000000000000000000000000000000000..362e7d46ce8cc3845695eed7b6e4bd64225ce4dd
--- /dev/null
+++ b/js/ckeditor5_plugins/drupalentity/src/command.js
@@ -0,0 +1,52 @@
+import {Command} from 'ckeditor5/src/core';
+
+export default class InsertEntityEmbedCommand extends Command {
+
+  execute(attributes) {
+    const { model } = this.editor;
+    const entityEmbedEditing = this.editor.plugins.get('EntityEmbedEditing');
+
+    // Create object that contains supported data-attributes in view data by
+    // flipping `EntityEmbedEditing.attrs` object (i.e. keys from object become
+    // values and values from object become keys).
+    const dataAttributeMapping = Object.entries(entityEmbedEditing.attrs).reduce(
+      (result, [key, value]) => {
+        result[value] = key;
+        return result;
+      },
+      {},
+    );
+
+    // \Drupal\entity_embed\Form\EntityEmbedDialog returns data in keyed by
+    // data-attributes used in view data. This converts data-attribute keys to
+    // keys used in model.
+    const modelAttributes = Object.keys(attributes).reduce(
+      (result, attribute) => {
+        if (dataAttributeMapping[attribute]) {
+          result[dataAttributeMapping[attribute]] = attributes[attribute];
+        }
+        return result;
+      },
+      {},
+    );
+
+    model.change((writer) => {
+      model.insertContent(createEntityEmbed(writer, modelAttributes));
+    });
+  }
+
+  refresh() {
+    const model = this.editor.model;
+    const selection = model.document.selection;
+    const allowedIn = model.schema.findAllowedParent(
+      selection.getFirstPosition(),
+      'drupalEntity',
+    );
+    this.isEnabled = allowedIn !== null;
+  };
+
+}
+
+function createEntityEmbed(writer, attributes) {
+  return writer.createElement('drupalEntity', attributes);
+}
diff --git a/js/ckeditor5_plugins/drupalentity/src/editing.js b/js/ckeditor5_plugins/drupalentity/src/editing.js
new file mode 100644
index 0000000000000000000000000000000000000000..09f541a875eb24626ec50eef5221b03cb6413c70
--- /dev/null
+++ b/js/ckeditor5_plugins/drupalentity/src/editing.js
@@ -0,0 +1,288 @@
+import { Plugin } from 'ckeditor5/src/core';
+import { Widget, toWidget } from 'ckeditor5/src/widget';
+import InsertEntityEmbedCommand from './command';
+
+export default class EntityEmbedEditing extends Plugin {
+
+  /**
+   * @inheritdoc
+   */
+  static get requires() {
+    return [Widget];
+  }
+
+  /**
+   * @inheritdoc
+   */
+  init() {
+    this.attrs = {
+      alt: 'alt',
+      title: 'title',
+      dataCaption: 'data-caption',
+      dataAlign: 'data-align',
+      drupalEntityLangCode: 'data-langcode',
+      drupalEntityEntityType: 'data-entity-type',
+      drupalEntityEntityUuid: 'data-entity-uuid',
+      drupalEntityEmbedButton: 'data-embed-button',
+      drupalEntityEmbedDisplay: 'data-entity-embed-display',
+      drupalEntityEmbedDisplaySettings: 'data-entity-embed-display-settings',
+    };
+    const options = this.editor.config.get('entityEmbed');
+    if (!options) {
+      throw new Error('Error on initializing entityEmbed plugin: entityEmbed config is required.');
+    }
+    this.options = options;
+    this.labelError = Drupal.t('Preview failed');
+    this.previewError =`
+      <p>${Drupal.t(
+        'An error occurred while trying to preview the embedded content. Please save your work and reload this page.',
+      )}<p>
+    `;
+
+    this._defineSchema();
+    this._defineConverters();
+    this.editor.commands.add(
+      'insertEntityEmbed',
+      new InsertEntityEmbedCommand(this.editor),
+    );
+  }
+
+  /**
+   * Registers drupalEntity as a block element in the DOM.
+   *
+   * @private
+   */
+  _defineSchema() {
+    const schema = this.editor.model.schema;
+
+    schema.register('drupalEntity', {
+      isObject: true,
+      isContent: true,
+      isBlock: true,
+      allowWhere: '$block',
+      allowAttributes: Object.keys(this.attrs),
+    });
+    this.editor.editing.view.domConverter.blockElements.push('drupal-entity');
+  }
+
+  /**
+   * Defines handling of drupalEntity elements.
+   *
+   * @private
+   */
+  _defineConverters() {
+    const {conversion} = this.editor;
+
+    conversion
+      .for('upcast')
+      .elementToElement({
+        model: 'drupalEntity',
+        view: {
+          name: 'drupal-entity',
+        },
+      });
+
+    conversion
+      .for('dataDowncast')
+      .elementToElement({
+        model: 'drupalEntity',
+        view: {
+          name: 'drupal-entity',
+        },
+      });
+
+    // Convert the <drupalEntity> model into an editable <drupal-entity> widget.
+    conversion
+      .for('editingDowncast')
+      .elementToElement({
+        model: 'drupalEntity',
+        view: (modelElement, { writer }) => {
+          const container = writer.createContainerElement('figure', {
+            class: 'drupal-entity',
+          });
+          writer.setCustomProperty('drupalEntity', true, container);
+
+          return toWidget(container, writer, {
+            label: Drupal.t('Entity Embed widget'),
+          })
+        },
+      })
+      .add((dispatcher) => {
+        const converter = (event, data, conversionApi) => {
+          const viewWriter = conversionApi.writer;
+          const modelElement = data.item;
+          const container = conversionApi.mapper.toViewElement(data.item);
+
+          let drupalEntity = this._getPreviewContainer(container.getChildren());
+          // Use existing container if it exists, create on if it does not.
+          if (drupalEntity) {
+            // Stop processing if a preview is already loading.
+            if (drupalEntity.getAttribute('data-drupal-entity-preview') !== 'ready') {
+              return;
+            }
+            // Preview was ready meaning that a new preview can be loaded.
+            // Change the attribute to loading to prepare for the loading of
+            // the updated preview. Preview is kept intact so that it remains
+            // interactable in the UI until the new preview has been rendered.
+            viewWriter.setAttribute(
+              'data-drupal-entity-preview',
+              'loading',
+              drupalEntity,
+            );
+          }
+          else {
+            drupalEntity = viewWriter.createRawElement('div', {
+              'data-drupal-entity-preview': 'loading',
+            });
+            viewWriter.insert(viewWriter.createPositionAt(container, 0), drupalEntity);
+          }
+
+          this._loadPreview(modelElement).then(({ label, preview }) => {
+            if (!drupalEntity) {
+              // Nothing to do if associated preview wrapped no longer exist.
+              return;
+            }
+            // CKEditor 5 doesn't support async view conversion. Therefore, once
+            // the promise is fulfilled, the editing view needs to be modified
+            // manually.
+            this.editor.editing.view.change((writer) => {
+              const drupalEntityPreview = writer.createRawElement(
+                'div',
+                {'data-drupal-entity-preview': 'ready', 'aria-label': label},
+                (domElement) => {
+                  domElement.innerHTML = preview;
+                },
+              );
+              // Insert the new preview before the previous preview element to
+              // ensure that the location remains same even if it is wrapped
+              // with another element.
+              writer.insert(writer.createPositionBefore(drupalEntity), drupalEntityPreview);
+              writer.remove(drupalEntity);
+            });
+          });
+        }
+
+        dispatcher.on('attribute:drupalEntityEntityUuid:drupalEntity', converter);
+
+        return dispatcher;
+      });
+
+    // Set attributeToAttribute conversion for all supported attributes.
+    Object.keys(this.attrs).forEach((modelKey) => {
+      const attributeMapping = {
+        model: {
+          key: modelKey,
+          name: 'drupalEntity',
+        },
+        view: {
+          name: 'drupal-entity',
+          key: this.attrs[modelKey],
+        },
+      };
+      // Attributes should be rendered only in dataDowncast to avoid having
+      // unfiltered data-attributes on the Drupal Entity widget.
+      conversion.for('dataDowncast').attributeToAttribute(attributeMapping);
+      conversion.for('upcast').attributeToAttribute(attributeMapping);
+    });
+  }
+
+  /**
+   * Loads the preview.
+   *
+   * @param modelElement
+   *   The model element which preview should be loaded.
+   * @returns {Promise<{preview: string, label: *}|{preview: *, label: string}>}
+   *   A promise that returns an object.
+   *
+   * @private
+   *
+   * @see DrupalMediaEditing::_fetchPreview().
+   */
+  async _loadPreview(modelElement) {
+    const query = {
+      text: this._renderElement(modelElement),
+    };
+
+    const response = await fetch(
+      Drupal.url('embed/preview/' + this.options.format + '?' + new URLSearchParams(query)),
+      {
+        headers: {
+          'X-Drupal-EmbedPreview-CSRF-Token':
+          this.options.previewCsrfToken,
+        },
+      },
+    );
+
+    if (response.ok) {
+      const label = Drupal.t('Entity Embed widget');
+      const preview = await response.text();
+      return { label, preview };
+    }
+
+    return { label: this.labelError, preview: this.previewError };
+  }
+
+  /**
+   * Renders the model element.
+   *
+   * @param modelElement
+   *   The drupalMedia model element to be converted.
+   * @returns {*}
+   *   The model element converted into HTML.
+   *
+   * @private
+   */
+  _renderElement(modelElement) {
+    // Create model document fragment which contains the model element so that
+    // it can be stringified using the dataDowncast.
+    const modelDocumentFragment = this.editor.model.change((writer) => {
+      const modelDocumentFragment = writer.createDocumentFragment();
+      // Create shallow clone of the model element to ensure that the original
+      // model element remains untouched and that the caption is not rendered
+      // into the preview.
+      const clonedModelElement = writer.cloneElement(modelElement, false);
+      writer.append(clonedModelElement, modelDocumentFragment);
+
+      return modelDocumentFragment;
+    });
+
+    return this.editor.data.stringify(modelDocumentFragment);
+  }
+
+  /**
+   * Gets the preview container element.
+   *
+   * @param children
+   *   The child elements.
+   * @returns {null|*}
+   *    The preview child element if available.
+   *
+   * @private
+   */
+  _getPreviewContainer(children) {
+    for (const child of children) {
+      if (child.hasAttribute('data-drupal-entity-preview')) {
+        return child;
+      }
+
+      if (child.childCount) {
+        const recursive = this._getPreviewContainer(child.getChildren());
+        // Return only if preview container was found within this element's
+        // children.
+        if (recursive) {
+          return recursive;
+        }
+      }
+    }
+
+    return null;
+  }
+
+  /**
+   * @inheritdoc
+   */
+  static get pluginName() {
+    return 'EntityEmbedEditing';
+  }
+
+}
diff --git a/js/ckeditor5_plugins/drupalentity/src/entityembed.js b/js/ckeditor5_plugins/drupalentity/src/entityembed.js
new file mode 100644
index 0000000000000000000000000000000000000000..65fad2aa921157baa1c1aa4a9d63355393043ea9
--- /dev/null
+++ b/js/ckeditor5_plugins/drupalentity/src/entityembed.js
@@ -0,0 +1,19 @@
+import EntityEmbedEditing from './editing';
+import EntityEmbedToolbar from './toolbar';
+import EntityEmbedUI from './ui';
+import { Plugin } from 'ckeditor5/src/core';
+
+export default class EntityEmbed extends Plugin {
+
+  static get requires() {
+    return [EntityEmbedEditing, EntityEmbedUI, EntityEmbedToolbar];
+  }
+
+  /**
+   * @inheritdoc
+   */
+  static get pluginName() {
+    return 'EntityEmbed';
+  }
+
+}
diff --git a/js/ckeditor5_plugins/drupalentity/src/index.js b/js/ckeditor5_plugins/drupalentity/src/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..f7197a1672af3f0f4ab868cb640e362caecad857
--- /dev/null
+++ b/js/ckeditor5_plugins/drupalentity/src/index.js
@@ -0,0 +1,9 @@
+/**
+ * @module entity-embed
+ */
+
+import EntityEmbed from './entityembed';
+
+export default {
+  EntityEmbed,
+};
diff --git a/js/ckeditor5_plugins/drupalentity/src/toolbar.js b/js/ckeditor5_plugins/drupalentity/src/toolbar.js
new file mode 100644
index 0000000000000000000000000000000000000000..dc0c27fb038f02a0262aeec84424d14253029a1d
--- /dev/null
+++ b/js/ckeditor5_plugins/drupalentity/src/toolbar.js
@@ -0,0 +1,135 @@
+import { Plugin, icons } from 'ckeditor5/src/core';
+import { isWidget, WidgetToolbarRepository } from 'ckeditor5/src/widget';
+import { ButtonView } from "ckeditor5/src/ui";
+
+export default class EntityEmbedToolbar extends Plugin {
+
+  /**
+   * @inheritdoc
+   */
+  static get requires() {
+    return [WidgetToolbarRepository];
+  }
+
+  /**
+   * @inheritdoc
+   */
+  init() {
+    const editor = this.editor;
+    const entityEmbedEditing = editor.plugins.get('EntityEmbedEditing');
+    const options = editor.config.get('entityEmbed');
+    const { dialogSettings = {} } = options;
+
+    editor.ui.componentFactory.add('entityEmbedEdit', (locale) => {
+      let buttonView = new ButtonView(locale);
+
+      buttonView.set({
+        label: editor.t('Edit'),
+        icon: icons.pencil,
+        tooltip: true,
+      })
+
+      this.listenTo(buttonView, 'execute', (eventInfo) => {
+        let element = editor.model.document.selection.getSelectedElement();
+        let libraryURL = Drupal.url('entity-embed/dialog/' + options.format + '/' + element.getAttribute('drupalEntityEmbedButton'));
+
+        let existingValues = {};
+
+        for (let [key, value] of element.getAttributes()) {
+          let attribute = entityEmbedEditing.attrs[key]
+          if (attribute) {
+            existingValues[attribute] = value
+          }
+        }
+
+        // Open a dialog to select entity to embed.
+        this._openDialog(
+          libraryURL,
+          existingValues,
+          ({ attributes }) => {
+            editor.execute('insertEntityEmbed', attributes);
+            editor.editing.view.focus();
+          },
+          dialogSettings,
+        )
+      });
+
+      return buttonView;
+    })
+  }
+
+  /**
+   * @inheritdoc
+   */
+  afterInit() {
+    const { editor } = this
+    if (!editor.plugins.has('WidgetToolbarRepository')) {
+      return;
+    }
+    const widgetToolbarRepository = editor.plugins.get('WidgetToolbarRepository')
+
+    widgetToolbarRepository.register('entityEmbed', {
+      items: ['entityEmbedEdit'],
+      getRelatedElement(selection) {
+        const viewElement = selection.getSelectedElement()
+        if (!viewElement) {
+          return null
+        }
+        if (!isWidget(viewElement)) {
+          return null
+        }
+        if (!viewElement.getCustomProperty('drupalEntity')) {
+          return null
+        }
+
+        return viewElement
+      },
+    })
+  }
+
+  /**
+   * @param {string} url
+   *   The URL that contains the contents of the dialog.
+   * @param {object} existingValues
+   *   Existing values that will be sent via POST to the url for the dialog
+   *   contents.
+   * @param {function} saveCallback
+   *   A function to be called upon saving the dialog.
+   * @param {object} dialogSettings
+   *   An object containing settings to be passed to the jQuery UI.
+   */
+  _openDialog(url, existingValues, saveCallback, dialogSettings) {
+    // Add a consistent dialog class.
+    const classes = dialogSettings.dialogClass
+      ? dialogSettings.dialogClass.split(' ')
+      : [];
+    classes.push('ui-dialog--narrow');
+    dialogSettings.dialogClass = classes.join(' ');
+    dialogSettings.autoResize =
+      window.matchMedia('(min-width: 600px)').matches;
+    dialogSettings.width = 'auto';
+
+    const ckeditorAjaxDialog = Drupal.ajax({
+      dialog: dialogSettings,
+      dialogType: 'modal',
+      selector: '.ckeditor5-dialog-loading-link',
+      url,
+      progress: { type: 'fullscreen' },
+      submit: {
+        editor_object: existingValues,
+      },
+    });
+    ckeditorAjaxDialog.execute();
+
+    // Store the save callback to be executed when this dialog is closed.
+    Drupal.ckeditor5.saveCallback = saveCallback;
+  }
+
+  /**
+   * @inheritdoc
+   */
+  static get pluginName() {
+    return 'EntityEmbedToolbar';
+  }
+
+}
diff --git a/js/ckeditor5_plugins/drupalentity/src/ui.js b/js/ckeditor5_plugins/drupalentity/src/ui.js
new file mode 100644
index 0000000000000000000000000000000000000000..16a7cc566533940372f02c6f2bbb30bb565a67d1
--- /dev/null
+++ b/js/ckeditor5_plugins/drupalentity/src/ui.js
@@ -0,0 +1,79 @@
+/**
+ * @file Registers the entity embed button(s) to the CKEditor instance(s) and binds functionality to it/them.
+ */
+
+import { Plugin } from 'ckeditor5/src/core';
+import { ButtonView } from 'ckeditor5/src/ui';
+import defaultIcon from '../entity.svg';
+
+export default class EntityEmbedUI extends Plugin {
+
+  /**
+   * @inheritdoc
+   */
+  static get requires() {
+    return ['Widget'];
+  }
+
+  /**
+   * @inheritdoc
+   */
+  init() {
+    const editor = this.editor;
+    const command = editor.commands.get('insertEntityEmbed');
+    const options = editor.config.get('entityEmbed');
+    const viewDocument = editor.editing.view.document;
+    if (!options) {
+      return;
+    }
+    const { dialogSettings = {} } = options;
+    const embed_buttons = options.buttons;
+
+    // Register each embed button to the toolbar based on configuration.
+    Object.keys(embed_buttons).forEach(id => {
+      let libraryURL = Drupal.url('entity-embed/dialog/' + options.format + '/' + id);
+      // Add each button to the toolbar.
+      editor.ui.componentFactory.add(id, (locale) => {
+        let button = embed_buttons[id];
+        let buttonView = new ButtonView(locale);
+        // Set the icon to the SVG from config, or set it to the default icon.
+        // If the uploaded icon is an SVG, load it or use the default icon otherwise.
+        let icon = null;
+        if (button.icon.endsWith('svg')) {
+          let XMLrequest = new XMLHttpRequest();
+          XMLrequest.open("GET", button.icon, false);
+          XMLrequest.send(null);
+          icon = XMLrequest.response;
+        }
+
+        buttonView.set({
+          label: button.label,
+          icon: icon ?? defaultIcon,
+          tooltip: true,
+        });
+        buttonView.bind('isOn', 'isEnabled').to(command, 'value', 'isEnabled');
+
+        this.listenTo(buttonView, 'execute', () =>
+          // Open a dialog to select entity to embed.
+          Drupal.ckeditor5.openDialog(
+            libraryURL,
+            ({ attributes }) => {
+              editor.execute('insertEntityEmbed', attributes);
+            },
+            dialogSettings,
+          ),
+        );
+
+        return buttonView;
+      })
+    });
+  }
+
+  /**
+   * @inheritdoc
+   */
+  static get pluginName() {
+    return 'EntityEmbedUI';
+  }
+
+}
diff --git a/js/entity_embed.set_dynamic_icons.js b/js/entity_embed.set_dynamic_icons.js
new file mode 100644
index 0000000000000000000000000000000000000000..cfca00e2ed3c7bb91703d6eb5cf5ff2d0fd99934
--- /dev/null
+++ b/js/entity_embed.set_dynamic_icons.js
@@ -0,0 +1,18 @@
+/* This script is responsible for setting the correct image(s) on the admin
+ * toolbar, as we cannot build the JS for it because we do not know how
+ * many entity embed buttons are there in the system. The number of buttons
+ * created are based on the number of embed buttons.
+ */
+(function ($, Drupal, drupalSettings, once) {
+  Drupal.behaviors.entityEmbedSetDynamicIcons = {
+    attach: function (context) {
+      // Get the available Embed Buttons from Drupal.
+      Object.values(drupalSettings.embedButtons || {}).forEach(function (button) {
+        // Iterate through the embed buttons and set the corresponding background image.
+        let selector = '.ckeditor5-toolbar-button-' + button.id;
+        let iconUrl = button.icon.endsWith('svg') ? button.icon : '/' + drupalSettings.modulePath + '/js/ckeditor5_plugins/drupalentity/entity.svg';
+        $(once('entityEmbedSetDynamicIcons', selector, context)).css('background-image', 'url(' + iconUrl + ')');
+      });
+    },
+  }
+})(jQuery, Drupal, drupalSettings, once);
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..26905289797a2ddc96a849c149a2140670bf1cc8
--- /dev/null
+++ b/package.json
@@ -0,0 +1,19 @@
+{
+  "name": "drupal-entity-embed",
+  "version": "1.0.0",
+  "description": "Drupal Entity Embed CKEditor 5 integration",
+  "author": "",
+  "license": "GPL-2.0-or-later",
+  "scripts": {
+    "watch": "webpack --mode development --watch",
+    "build": "webpack"
+  },
+  "devDependencies": {
+    "@ckeditor/ckeditor5-dev-utils": "^30.0.0",
+    "ckeditor5": "~34.1.0",
+    "raw-loader": "^4.0.2",
+    "terser-webpack-plugin": "^5.2.0",
+    "webpack": "^5.51.1",
+    "webpack-cli": "^4.4.0"
+  }
+}
diff --git a/src/Plugin/CKEditor4To5Upgrade/EntityEmbed.php b/src/Plugin/CKEditor4To5Upgrade/EntityEmbed.php
new file mode 100644
index 0000000000000000000000000000000000000000..1a40ea15d358e2109cc1bb2d7489e10c2c4b329b
--- /dev/null
+++ b/src/Plugin/CKEditor4To5Upgrade/EntityEmbed.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace Drupal\entity_embed\Plugin\CKEditor4To5Upgrade;
+
+use Drupal\ckeditor5\HTMLRestrictions;
+use Drupal\ckeditor5\Plugin\CKEditor4To5UpgradePluginInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Plugin\PluginBase;
+use Drupal\filter\FilterFormatInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides the CKEditor 4 to 5 upgrade path for entity embed buttons.
+ *
+ * @CKEditor4To5Upgrade(
+ *   id = "entity_embed",
+ *   cke4_buttons = {
+ *   },
+ *   cke4_plugin_settings = {
+ *   },
+ *   cke5_plugin_elements_subset_configuration = {
+ *   }
+ * )
+ *
+ * @internal
+ *   Plugin classes are internal.
+ */
+class EntityEmbed extends PluginBase implements CKEditor4To5UpgradePluginInterface, ContainerFactoryPluginInterface {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * EntityEmbed constructor.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param \Drupal\ckeditor5\Plugin\CKEditor4To5UpgradePluginInterface $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The Entity Type Manager service.
+   */
+  public function __construct(array $configuration,
+    string $plugin_id,
+    CKEditor4To5UpgradePluginInterface $plugin_definition,
+    EntityTypeManagerInterface $entity_type_manager
+  ) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function mapCKEditor4ToolbarButtonToCKEditor5ToolbarItem(string $cke4_button, HTMLRestrictions $text_format_html_restrictions): ?array {
+    $buttons = [];
+
+    $embed_buttons = $this->entityTypeManager
+      ->getStorage('embed_button')
+      ->loadByProperties(['type_id' => 'entity']);
+    foreach ($embed_buttons as $embed_button) {
+      $buttons[] = $embed_button->id();
+    }
+    foreach ($buttons as $button) {
+      if ($cke4_button == $button) {
+        return [$button];
+      }
+    }
+
+    throw new \OutOfBoundsException();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function mapCKEditor4SettingsToCKEditor5Configuration(string $cke4_plugin_id, array $cke4_plugin_settings): ?array {
+    throw new \OutOfBoundsException();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function computeCKEditor5PluginSubsetConfiguration(string $cke5_plugin_id, FilterFormatInterface $text_format): ?array {
+    throw new \OutOfBoundsException();
+  }
+
+}
diff --git a/src/Plugin/CKEditor5Plugin/DrupalEntity.php b/src/Plugin/CKEditor5Plugin/DrupalEntity.php
new file mode 100644
index 0000000000000000000000000000000000000000..9d27cefb54291f147e9ed9b4f8cc2b19a010bade
--- /dev/null
+++ b/src/Plugin/CKEditor5Plugin/DrupalEntity.php
@@ -0,0 +1,130 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\entity_embed\Plugin\CKEditor5Plugin;
+
+use Drupal\ckeditor5\Plugin\CKEditor5PluginDefault;
+use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Access\CsrfTokenGenerator;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\editor\EditorInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * @CKEditor5Plugin(
+ *   id = "entity_embed_drupalentity",
+ *   ckeditor5 = @CKEditor5AspectsOfCKEditor5Plugin(
+ *     plugins = {"drupalentity.EntityEmbed"},
+ *     config = {},
+ *   ),
+ *   drupal = @DrupalAspectsOfCKEditor5Plugin(
+ *     deriver = "Drupal\entity_embed\Plugin\CKEditor5Plugin\DrupalEntityDeriver",
+ *     library = "entity_embed/entity_embed",
+ *     admin_library = "entity_embed/admin.entity_embed",
+ *     elements = {
+ *       "<drupal-entity>",
+ *       "<drupal-entity alt title data-align data-caption data-entity-embed-display data-entity-embed-display-settings data-entity-uuid>",
+ *       "<drupal-entity data-langcode>",
+ *     },
+ *     conditions = {
+ *       "filter" = "entity_embed",
+ *     },
+ *   )
+ * )
+ */
+class DrupalEntity extends CKEditor5PluginDefault implements ContainerFactoryPluginInterface {
+
+  /**
+   * The CSRF Token generator.
+   *
+   * @var \Drupal\Core\Access\CsrfTokenGenerator
+   */
+  protected $csrfTokenGenerator;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * DrupalEntity constructor.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param \Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token_generator
+   *   The CSRF Token generator service.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The Entity Type Manager service.
+   */
+  public function __construct(
+    array $configuration,
+    string $plugin_id,
+    CKEditor5PluginDefinition $plugin_definition,
+    CsrfTokenGenerator $csrf_token_generator,
+    EntityTypeManagerInterface $entity_type_manager
+  ) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->csrfTokenGenerator = $csrf_token_generator;
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('csrf_token'),
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDynamicPluginConfig(array $static_plugin_config, EditorInterface $editor): array {
+    // Register embed buttons as individual buttons on admin pages.
+    $dynamic_plugin_config = $static_plugin_config;
+    $embed_buttons = $this
+      ->entityTypeManager
+      ->getStorage('embed_button')
+      ->loadByProperties(['type_id' => 'entity']);
+    $buttons = [];
+    /** @var \Drupal\embed\EmbedButtonInterface $embed_button */
+    foreach ($embed_buttons as $embed_button) {
+      $id = $embed_button->id();
+      $label = Html::escape($embed_button->label());
+      $buttons[$id] = [
+        'id' => $id,
+        'name' => $label,
+        'label' => $label,
+        'icon' => $embed_button->getIconUrl(),
+      ];
+    }
+    // Add configured embed buttons and pass it to the UI.
+    $dynamic_plugin_config['entityEmbed'] = [
+      'buttons' => $buttons,
+      'format' => $editor->getFilterFormat()->id(),
+      'dialogSettings' => [
+        'dialogClass' => 'entity-select-dialog',
+        'height' => 'auto',
+        'width' => 'auto',
+      ],
+      'previewCsrfToken' => $this->csrfTokenGenerator->get('X-Drupal-EmbedPreview-CSRF-Token'),
+    ];
+
+    return $dynamic_plugin_config;
+  }
+
+}
diff --git a/src/Plugin/CKEditor5Plugin/DrupalEntityDeriver.php b/src/Plugin/CKEditor5Plugin/DrupalEntityDeriver.php
new file mode 100644
index 0000000000000000000000000000000000000000..ffbe8e7668c45a61ced264ae91e6d500417c8001
--- /dev/null
+++ b/src/Plugin/CKEditor5Plugin/DrupalEntityDeriver.php
@@ -0,0 +1,74 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\entity_embed\Plugin\CKEditor5Plugin;
+
+use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+class DrupalEntityDeriver extends DeriverBase implements ContainerDeriverInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $embedButtonStorage;
+
+  /**
+   * Constructs a new DrupalEntityDeriver object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->embedButtonStorage = $entity_type_manager->getStorage('embed_button');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, $base_plugin_id) {
+    return new static(
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDerivativeDefinitions($base_plugin_definition) {
+    assert($base_plugin_definition instanceof CKEditor5PluginDefinition);
+    /** @var \Drupal\embed\EmbedButtonInterface $embed_button */
+    foreach ($this->embedButtonStorage->loadMultiple() as $embed_button) {
+      $embed_button_id = $embed_button->id();
+      $embed_button_label = Html::escape($embed_button->label());
+      $plugin_id = "entity_embed_{$embed_button_id}";
+      $definition = $base_plugin_definition->toArray();
+      $definition['id'] .= $embed_button_id;
+      $definition['drupal']['label'] = $this->t('Entity Embed - @label', ['@label' => $embed_button_label])->render();
+      $definition['drupal']['toolbar_items'] = [
+        $embed_button_id => [
+          'label' => $embed_button_label,
+        ],
+      ];
+      $definition['drupal']['elements'][] = '<drupal-entity data-embed-button="' . $embed_button_id . '">';
+      $definition['drupal']['elements'][] = '<drupal-entity data-entity-type="' . $embed_button->getTypeSetting('entity_type') . '">';
+      $this->derivatives[$plugin_id] = new CKEditor5PluginDefinition($definition);
+    }
+
+    return $this->derivatives;
+  }
+
+}
diff --git a/src/Plugin/Derivative/DrupalEntityDeriver.php b/src/Plugin/Derivative/DrupalEntityDeriver.php
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/modules/entity_embed_test/entity_embed_test.info.yml b/tests/modules/entity_embed_test/entity_embed_test.info.yml
index d7ffa6d6ec150c0cca8c05877ba26978a13c9e51..bb01b8984b6d383c18065c22b31532ec2c790c9c 100644
--- a/tests/modules/entity_embed_test/entity_embed_test.info.yml
+++ b/tests/modules/entity_embed_test/entity_embed_test.info.yml
@@ -3,12 +3,13 @@ type: module
 description: 'Support module for the Entity Embed module tests.'
 package: Testing
 dependencies:
+ - drupal:ckeditor
+ - drupal:ckeditor5
  - drupal:file
  - drupal:image
  - drupal:node
  - drupal:text
  - drupal:media
- - drupal:ckeditor
  - drupal:editor
  - embed:embed
  - entity_embed:entity_embed
diff --git a/tests/modules/entity_embed_translation_test/entity_embed_translation_test.info.yml b/tests/modules/entity_embed_translation_test/entity_embed_translation_test.info.yml
index e28ea6da3059981031989425437af58ba705d0c0..c33409cd5f980f7a9f27b9c29ac1ee76a020584c 100644
--- a/tests/modules/entity_embed_translation_test/entity_embed_translation_test.info.yml
+++ b/tests/modules/entity_embed_translation_test/entity_embed_translation_test.info.yml
@@ -4,12 +4,13 @@ description: 'Aids in testing translation within entity embed'
 package: Testing
 dependencies:
  - drupal:content_translation
+ - drupal:ckeditor
+ - drupal:ckeditor5
  - drupal:file
  - drupal:image
  - drupal:node
  - drupal:text
  - drupal:media
- - drupal:ckeditor
  - drupal:editor
  - embed:embed
  - entity_embed:entity_embed
diff --git a/tests/src/Kernel/UpgradePathTest.php b/tests/src/Kernel/UpgradePathTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..9d8b24ca81311c1017df40467075120568ec2852
--- /dev/null
+++ b/tests/src/Kernel/UpgradePathTest.php
@@ -0,0 +1,242 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Tests\entity_embed\Kernel;
+
+use Drupal\editor\Entity\Editor;
+use Drupal\filter\Entity\FilterFormat;
+use Drupal\Tests\ckeditor5\Kernel\SmartDefaultSettingsTest;
+
+/**
+ * @covers \Drupal\linkit\Plugin\CKEditor4To5Upgrade\\Drupal\entity_embed\Plugin\CKEditor4To5Upgrade\EntityEmbed
+ * @group entity_embed
+ * @group ckeditor5
+ * @requires module ckeditor5
+ * @internal
+ */
+class UpgradePathTest extends SmartDefaultSettingsTest {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'entity_embed',
+    // Because modules/contrib/entity_embed/config/optional/embed.button.node.yml
+    // will only then get installed.
+    'embed',
+    'node',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->installConfig(['entity_embed']);
+
+    $filter_config_bad_filter_html = [
+      'filter_html' => [
+        'status' => 1,
+        'settings' => [
+          'allowed_html' => '<p> <br> <strong>',
+        ],
+      ],
+    ];
+    $filter_config_entity_embed_off = [
+      'entity_embed' => [
+        'status' => 0,
+      ],
+    ];
+    $filter_config_entity_embed_on = [
+      'entity_embed' => [
+        'status' => 1,
+      ],
+    ];
+    FilterFormat::create([
+      'format' => 'entity_embed_disabled',
+      'name' => 'Entity Embed disabled',
+      'filters' => $filter_config_bad_filter_html + $filter_config_entity_embed_off,
+    ])->setSyncing(TRUE)->save();
+    FilterFormat::create([
+      'format' => 'entity_embed_enabled_misconfigured_format_filter_html',
+      'name' => 'Entity Embed enabled on a misconfigured format (filter_html wrong)',
+      'filters' => $filter_config_bad_filter_html + $filter_config_entity_embed_on ,
+    ])->setSyncing(TRUE)->save();
+    FilterFormat::create([
+      'format' => 'entity_embed_enabled_misconfigured_format_missing_entity_embed',
+      'name' => 'Entity Embed enabled on a misconfigured format (entity_embed missing)',
+      'filters' => $filter_config_bad_filter_html + $filter_config_entity_embed_off,
+    ])->setSyncing(TRUE)->save();
+    FilterFormat::create([
+      'format' => 'entity_embed_enabled',
+      'name' => 'Entity Embed enabled on a well-configured format',
+      'filters' => [
+        'filter_html' => [
+          'status' => 1,
+          'settings' => [
+            'allowed_html' => '<p> <br> <strong> <drupal-entity data-entity-type data-entity-uuid data-entity-embed-display data-entity-embed-display-settings data-align data-caption data-embed-button data-langcode alt title>',
+          ],
+        ],
+      ] + $filter_config_entity_embed_on,
+    ])->setSyncing(TRUE)->save();
+
+    $generate_editor_settings = function (bool $node_embed_button_in_toolbar) {
+      return [
+        'toolbar' => [
+          'rows' => [
+            0 => [
+              [
+                'name' => 'Basic Formatting',
+                'items' => [
+                  'Bold',
+                  'Format',
+                ],
+              ],
+              [
+                'name' => 'Embedding',
+                'items' => $node_embed_button_in_toolbar
+                  ? [
+                    'node',
+                  ]
+                  : [],
+              ],
+            ],
+          ],
+        ],
+        'plugins' => [],
+      ];
+    };
+
+    Editor::create([
+      'format' => 'entity_embed_disabled',
+      'editor' => 'ckeditor',
+      'settings' => $generate_editor_settings(FALSE),
+    ])->setSyncing(TRUE)->save();
+    Editor::create([
+      'format' => 'entity_embed_enabled_misconfigured_format_filter_html',
+      'editor' => 'ckeditor',
+      'settings' => $generate_editor_settings(TRUE),
+    ])->setSyncing(TRUE)->save();
+    Editor::create([
+      'format' => 'entity_embed_enabled_misconfigured_format_missing_entity_embed',
+      'editor' => 'ckeditor',
+      'settings' => $generate_editor_settings(TRUE),
+    ])->setSyncing(TRUE)->save();
+    Editor::create([
+      'format' => 'entity_embed_enabled',
+      'editor' => 'ckeditor',
+      'settings' => $generate_editor_settings(TRUE),
+    ])->setSyncing(TRUE)->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function provider() {
+    $expected_ckeditor5_toolbar = [
+      'items' => [
+        'bold',
+        '|',
+        'node',
+      ],
+    ];
+
+    yield "entity_embed disabled" => [
+      'format_id' => 'entity_embed_disabled',
+      'filters_to_drop' => [],
+      'expected_ckeditor5_settings' => [
+        'toolbar' => [
+          'items' => [
+            'bold',
+          ],
+        ],
+        'plugins' => [],
+      ],
+      'expected_superset' => '',
+      'expected_fundamental_compatibility_violations' => [],
+      'expected_db_logs' => [],
+      'expected_messages' => [],
+    ];
+
+    yield "entity_embed enabled on a misconfigured text format: filter_html wrong" => [
+      'format_id' => 'entity_embed_enabled_misconfigured_format_filter_html',
+      'filters_to_drop' => [],
+      'expected_ckeditor5_settings' => [
+        'toolbar' => $expected_ckeditor5_toolbar,
+        'plugins' => [],
+      ],
+      'expected_superset' => '<drupal-entity alt title data-align data-caption data-entity-embed-display data-entity-embed-display-settings data-entity-uuid data-langcode data-embed-button="node" data-entity-type="node">',
+      'expected_fundamental_compatibility_violations' => [],
+      'expected_db_logs' => [],
+      'expected_messages' => [
+        'warning' => [
+          'Updating to CKEditor 5 added support for some previously unsupported tags/attributes. A plugin introduced support for the following:  The tag <em class="placeholder">&lt;drupal-entity&gt;</em>; These attributes: <em class="placeholder"> alt (for &lt;drupal-entity&gt;), title (for &lt;drupal-entity&gt;), data-align (for &lt;drupal-entity&gt;), data-caption (for &lt;drupal-entity&gt;), data-entity-embed-display (for &lt;drupal-entity&gt;), data-entity-embed-display-settings (for &lt;drupal-entity&gt;), data-entity-uuid (for &lt;drupal-entity&gt;), data-langcode (for &lt;drupal-entity&gt;), data-embed-button (for &lt;drupal-entity&gt;), data-entity-type (for &lt;drupal-entity&gt;)</em>; Additional details are available in your logs.',
+        ],
+      ],
+    ];
+
+    yield "entity_embed enabled on a misconfigured text format: entity_embed off" => [
+      'format_id' => 'entity_embed_enabled_misconfigured_format_missing_entity_embed',
+      'filters_to_drop' => [],
+      'expected_ckeditor5_settings' => [
+        'toolbar' => $expected_ckeditor5_toolbar,
+        'plugins' => [],
+      ],
+      'expected_superset' => '',
+      'expected_fundamental_compatibility_violations' => [],
+      'expected_db_logs' => [],
+      'expected_messages' => [],
+      'expected_post_filter_drop_fundamental_compatibility_violations' => NULL,
+      'expected_post_update_text_editor_violations' => [
+        'settings.toolbar.items.2' => 'The <em class="placeholder">Node</em> toolbar item requires the <em class="placeholder">Display embedded entities</em> filter to be enabled.',
+      ]
+    ];
+
+    yield "entity_embed enabled on a well-configured text format" => [
+      'format_id' => 'entity_embed_enabled',
+      'filters_to_drop' => [],
+      'expected_ckeditor5_settings' => [
+        'toolbar' => [
+          'items' => [
+            'bold',
+            '|',
+            'node',
+            // Added because the CKEditor 4 entity_embed plugin uses attribute
+            // restrictions that are too permissive: it allows any value for the
+            // `data-entity-type` and `data-embed-button` attributes, but it
+            // should have been restricted to be more narrow.
+            'sourceEditing',
+          ],
+        ],
+        'plugins' => [
+          'ckeditor5_sourceEditing' => [
+            'allowed_tags' => [
+              '<drupal-entity data-entity-type data-embed-button>',
+            ],
+          ],
+        ],
+      ],
+      'expected_superset' => '',
+      'expected_fundamental_compatibility_violations' => [],
+      'expected_db_logs' => [
+        'status' => [
+          'As part of migrating to CKEditor 5, it was found that the <em class="placeholder">Entity Embed enabled on a well-configured format</em> text format\'s HTML filters includes plugins that support the following tags, but not some of their attributes. To ensure these attributes remain supported, the following were added to the Source Editing plugin\'s <em>Manually editable HTML tags</em>: &lt;drupal-entity data-entity-type data-embed-button&gt;. The text format must be saved to make these changes active.',
+        ],
+      ],
+      'expected_messages' => [
+        'status' => [
+          'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following:  Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: &lt;drupal-entity data-entity-type data-embed-button&gt;. Additional details are available in your logs.',
+        ],
+      ],
+    ];
+
+    // Verify that none of the core test cases are broken; especially important
+    // for Linkit since it extends the behavior of Drupal core.
+    foreach (parent::provider() as $label => $case) {
+      yield $label => $case;
+    }
+  }
+
+}
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..bd63f7f4689e679dcf4bab8644724539eca750e6
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,65 @@
+const path = require('path');
+const fs = require('fs');
+const webpack = require('webpack');
+const TerserPlugin = require('terser-webpack-plugin');
+
+function getDirectories(srcpath) {
+  return fs
+    .readdirSync(srcpath)
+    .filter((item) => fs.statSync(path.join(srcpath, item)).isDirectory());
+}
+
+module.exports = [];
+// Loop through every subdirectory in src, each a different plugin, and build
+// each one in ./build.
+getDirectories('./js/ckeditor5_plugins').forEach((dir) => {
+  const bc = {
+    mode: 'production',
+    optimization: {
+      minimize: true,
+      minimizer: [
+        new TerserPlugin({
+          terserOptions: {
+            format: {
+              comments: false,
+            },
+          },
+          test: /\.js(\?.*)?$/i,
+          extractComments: false,
+        }),
+      ],
+      moduleIds: 'named',
+    },
+    entry: {
+      path: path.resolve(
+        __dirname,
+        'js/ckeditor5_plugins',
+        dir,
+        'src/index.js',
+      ),
+    },
+    output: {
+      path: path.resolve(__dirname, './js/build'),
+      filename: `${dir}.js`,
+      library: ['CKEditor5', dir],
+      libraryTarget: 'umd',
+      libraryExport: 'default',
+    },
+    plugins: [
+      // It is possible to require the ckeditor5-dll.manifest.json used in
+      // core/node_modules rather than having to install CKEditor 5 here.
+      // However, that requires knowing the location of that file relative to
+      // where your module code is located.
+      new webpack.DllReferencePlugin({
+        manifest: require('./node_modules/ckeditor5/build/ckeditor5-dll.manifest.json'), // eslint-disable-line global-require, import/no-unresolved
+        scope: 'ckeditor5/src',
+        name: 'CKEditor5.dll',
+      }),
+    ],
+    module: {
+      rules: [{ test: /\.svg$/, use: 'raw-loader' }],
+    },
+  };
+
+  module.exports.push(bc);
+});
