diff --git a/core/modules/content_moderation/tests/src/FunctionalJavascript/ContentModerationConfigureEntityTypesFormTest.php b/core/modules/content_moderation/tests/src/FunctionalJavascript/ContentModerationConfigureEntityTypesFormTest.php
new file mode 100644
index 0000000..4c74f2a
--- /dev/null
+++ b/core/modules/content_moderation/tests/src/FunctionalJavascript/ContentModerationConfigureEntityTypesFormTest.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace Drupal\Tests\content_moderation\FunctionalJavascript;
+
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+use Drupal\block_content\Entity\BlockContentType;
+
+/**
+ * AJAX modal tests for the workflow type edit form.
+ *
+ * @group content_moderation
+ */
+class ContentModerationConfigureEntityTypesFormTest extends JavascriptTestBase {
+
+  /**
+   * Modules to install.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'workflows',
+    'content_moderation',
+    'block_content',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    $this->drupalLogin($this->drupalCreateUser(['administer workflows']));
+
+    // Create a custom block type.
+    BlockContentType::create([
+      'id' => 'epic',
+      'label' => 'Epic block',
+      'revision' => FALSE,
+    ])->save();
+  }
+
+  /**
+   * Tests the workflow type edit form 'This workflow applies to:' section.
+   */
+  public function testContentModerationConfigureEntityTypesForm() {
+    $this->drupalGet('admin/config/workflow/workflows/manage/editorial');
+
+    // Open the 'Custom block types' modal and select 'Epic block'.
+    $this->click('[data-drupal-selector="edit-type-settings-entity-types-container-entity-types-block-content-operations"] a');
+    $session = $this->assertSession();
+    $session->assertWaitOnAjaxRequest();
+    $modal = $session->waitForElementVisible('css', '#drupal-modal');
+    $this->assertTrue($modal->isVisible(), 'Modal window found.');
+    // Check the 'Epic block' checkbox.
+    $session->waitForElementVisible('css', '[data-drupal-selector="edit-bundles-epic"]')->check();
+    // Save the form and assert that the modal closes.
+    $save_button = $session->waitForElementVisible('css', '.ui-dialog button:contains(Save)');
+    $this->assertTrue($save_button->isVisible(), 'Save button found.');
+    $save_button->click();
+    $session->assertWaitOnAjaxRequest();
+    $this->assertFalse($modal->isVisible(), 'Modal window closed.');
+    // Assert that 'Epic block' shows in the UI as the selected Custom block
+    // types.
+    $selected_block_content = $session->waitForElementVisible('named', ['id', 'selected-block_content']);
+    $this->assertEquals('Epic block', $selected_block_content->getText());
+
+    // Open the 'Custom block types' modal again and deselect 'Epic block'.
+    $this->click('[data-drupal-selector="edit-type-settings-entity-types-container-entity-types-block-content-operations"] a');
+    $session->assertWaitOnAjaxRequest();
+    $modal = $session->waitForElementVisible('css', '#drupal-modal');
+    $this->assertTrue($modal->isVisible(), 'Modal window found.');
+    // Uncheck the 'Epic block' checkbox.
+    $session->waitForElementVisible('css', '[data-drupal-selector="edit-bundles-epic"]')->uncheck();
+    // Save the form and assert that the modal closes.
+    $save_button = $session->waitForElementVisible('css', '.ui-dialog button:contains(Save)');
+    $this->assertTrue($save_button->isVisible(), 'Save button found.');
+    $save_button->click();
+    $session->assertWaitOnAjaxRequest();
+    $this->assertFalse($modal->isVisible(), 'Modal window closed.');
+    // Assert that 'none' shows in the UI as the selected Custom block types.
+    $selected_block_content = $session->waitForElementVisible('named', ['id', 'selected-block_content']);
+    $this->assertEquals('none', $selected_block_content->getText());
+  }
+
+}
diff --git a/core/modules/workflows/src/Form/DialogFormTrait.php b/core/modules/workflows/src/Form/DialogFormTrait.php
new file mode 100644
index 0000000..266caad
--- /dev/null
+++ b/core/modules/workflows/src/Form/DialogFormTrait.php
@@ -0,0 +1,133 @@
+<?php
+
+namespace Drupal\workflows\Form;
+
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Ajax\RedirectCommand;
+use Drupal\Core\Ajax\ReplaceCommand;
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\CloseDialogCommand;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides a utility trait to simplify rendering forms in a dialog.
+ *
+ * @internal
+ */
+trait DialogFormTrait {
+
+  /**
+   * Adds dialog support to a form.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  protected function buildFormDialog(array &$form, FormStateInterface $form_state) {
+    if ($this->getRequestWrapperFormat() !== 'drupal_modal') {
+      return;
+    }
+
+    $form['actions']['submit']['#ajax'] = [
+      'dialogType' => 'modal',
+      'callback' => '::submitFormDialog',
+    ];
+
+    $form['actions']['cancel'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Cancel'),
+      '#weight' => 100,
+      '#submit' => ['::noSubmit'],
+      '#limit_validation_errors' => [],
+      '#ajax' => [
+        'dialogType' => 'modal',
+        'callback' => '::closeDialog',
+      ],
+    ];
+
+    $form['#attached']['library'][] = 'core/drupal.dialog.ajax';
+
+    // static::submitFormDialog() requires data-drupal-selector to be the same
+    // between the various Ajax requests. A bug in
+    // \Drupal\Core\Form\FormBuilder prevents that from happening unless
+    // $form['#id'] is also the same. Normally, #id is set to a unique HTML ID
+    // via Html::getUniqueId(), but here we bypass that in order to work around
+    // the data-drupal-selector bug. This is okay so long as we assume that this
+    // form only ever occurs once on a page.
+    // @todo Remove this workaround once https://www.drupal.org/node/2897377 is
+    //   fixed.
+    $form['#id'] = Html::getId($form_state->getBuildInfo()['form_id']);
+  }
+
+  /**
+   * Empty submit #ajax submit callback.
+   *
+   * This allows a modal dialog to using ::submitCallback to validate and submit
+   * the form via a single ajax request.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public function noSubmit(array &$form, FormStateInterface $form_state) {
+  }
+
+  /**
+   * Submit form dialog #ajax callback.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return \Drupal\Core\Ajax\AjaxResponse
+   *   An AJAX response that display validation error messages or redirects
+   *   to a URL.
+   */
+  public function submitFormDialog(array &$form, FormStateInterface $form_state) {
+    $response = new AjaxResponse();
+    if ($form_state->hasAnyErrors()) {
+      $form['status_messages'] = [
+        '#type' => 'status_messages',
+        '#weight' => -1000,
+      ];
+      $response->addCommand(new ReplaceCommand('[data-drupal-selector="' . $form['#attributes']['data-drupal-selector'] . '"]', $form));
+    }
+    else {
+      /** @var \Drupal\workflows\WorkflowInterface $workflow */
+      $workflow = $this->workflow ?: $this->getEntity();
+      $response->addCommand(new RedirectCommand($workflow->toUrl('edit-form')->setAbsolute()->toString()));
+    }
+    return $response;
+  }
+
+  /**
+   * Closes dialog #ajax callback.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return \Drupal\Core\Ajax\AjaxResponse
+   *   An AJAX response that displays validation error messages.
+   */
+  public function closeDialog(array &$form, FormStateInterface $form_state) {
+    return (new AjaxResponse())->addCommand(new CloseDialogCommand('#drupal-modal'));
+  }
+
+  /**
+   * Gets the request wrapper format.
+   *
+   * @return string
+   *   The request wrapper format.
+   */
+  protected function getRequestWrapperFormat() {
+    return $this->getRequest()
+      ->get(MainContentViewSubscriber::WRAPPER_FORMAT);
+  }
+
+}
diff --git a/core/modules/workflows/src/Form/WorkflowEditForm.php b/core/modules/workflows/src/Form/WorkflowEditForm.php
index 889b7bb..bdccfa3 100644
--- a/core/modules/workflows/src/Form/WorkflowEditForm.php
+++ b/core/modules/workflows/src/Form/WorkflowEditForm.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\workflows\Form;
 
+use Drupal\Component\Serialization\Json;
 use Drupal\Core\Form\SubformState;
 use Drupal\Core\Plugin\PluginFormFactoryInterface;
 use Drupal\workflows\Entity\Workflow;
@@ -110,20 +111,32 @@ public function form(array $form, FormStateInterface $form_state) {
       );
     }
 
+    $modal_attributes = [
+      'class' => ['use-ajax'],
+      'data-dialog-type' => 'modal',
+      'data-dialog-options' => Json::encode([
+        'width' => 700,
+      ]),
+    ];
     foreach ($states as $state) {
       $links = [
         'edit' => [
           'title' => $this->t('Edit'),
-          'url' => Url::fromRoute('entity.workflow.edit_state_form', ['workflow' => $workflow->id(), 'workflow_state' => $state->id()]),
-        ]
+          'url' => Url::fromRoute('entity.workflow.edit_state_form', [
+            'workflow' => $workflow->id(),
+            'workflow_state' => $state->id(),
+          ]),
+          'attributes' => $modal_attributes,
+        ],
       ];
       if ($this->entity->access('delete-state:' . $state->id())) {
         $links['delete'] = [
           'title' => t('Delete'),
           'url' => Url::fromRoute('entity.workflow.delete_state_form', [
             'workflow' => $workflow->id(),
-            'workflow_state' => $state->id()
+            'workflow_state' => $state->id(),
           ]),
+          'attributes' => $modal_attributes,
         ];
       }
       $form['states_container']['states'][$state->id()] = [
@@ -144,7 +157,12 @@ public function form(array $form, FormStateInterface $form_state) {
       ];
     }
     $form['states_container']['state_add'] = [
-      '#markup' => $workflow->toLink($this->t('Add a new state'), 'add-state-form')->toString(),
+      '#type' => 'link',
+      '#title' => $this->t('Add a new state'),
+      '#url' => Url::fromRoute('entity.workflow.add_state_form', [
+        'workflow' => $workflow->id(),
+      ]),
+      '#attributes' => $modal_attributes,
     ];
 
     $header = [
@@ -175,11 +193,19 @@ public function form(array $form, FormStateInterface $form_state) {
     foreach ($workflow->getTypePlugin()->getTransitions() as $transition) {
       $links['edit'] = [
         'title' => $this->t('Edit'),
-        'url' => Url::fromRoute('entity.workflow.edit_transition_form', ['workflow' => $workflow->id(), 'workflow_transition' => $transition->id()]),
+        'url' => Url::fromRoute('entity.workflow.edit_transition_form', [
+          'workflow' => $workflow->id(),
+          'workflow_transition' => $transition->id(),
+        ]),
+        'attributes' => $modal_attributes,
       ];
       $links['delete'] = [
         'title' => t('Delete'),
-        'url' => Url::fromRoute('entity.workflow.delete_transition_form', ['workflow' => $workflow->id(), 'workflow_transition' => $transition->id()]),
+        'url' => Url::fromRoute('entity.workflow.delete_transition_form', [
+          'workflow' => $workflow->id(),
+          'workflow_transition' => $transition->id(),
+        ]),
+        'attributes' => $modal_attributes,
       ];
       $form['transitions_container']['transitions'][$transition->id()] = [
         '#attributes' => ['class' => ['draggable']],
@@ -205,7 +231,12 @@ public function form(array $form, FormStateInterface $form_state) {
       ];
     }
     $form['transitions_container']['transition_add'] = [
-      '#markup' => $workflow->toLink($this->t('Add a new transition'), 'add-transition-form')->toString(),
+      '#type' => 'link',
+      '#title' => $this->t('Add a new transition'),
+      '#url' => Url::fromRoute('entity.workflow.add_transition_form', [
+        'workflow' => $workflow->id(),
+      ]),
+      '#attributes' => $modal_attributes,
     ];
 
     if ($workflow_type->hasFormClass(WorkflowTypeInterface::PLUGIN_FORM_KEY)) {
@@ -217,6 +248,8 @@ public function form(array $form, FormStateInterface $form_state) {
         ->createInstance($workflow_type, WorkflowTypeInterface::PLUGIN_FORM_KEY)
         ->buildConfigurationForm($form['type_settings'], $subform_state);
     }
+    // Add the AJAX library to the form for dialog support.
+    $form['#attached']['library'][] = 'core/drupal.dialog.ajax';
 
     return $form;
   }
diff --git a/core/modules/workflows/src/Form/WorkflowEntityFormBase.php b/core/modules/workflows/src/Form/WorkflowEntityFormBase.php
new file mode 100644
index 0000000..a99403d
--- /dev/null
+++ b/core/modules/workflows/src/Form/WorkflowEntityFormBase.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\workflows\Form;
+
+use Drupal\Core\Entity\EntityForm;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * A base class for Workflow entity forms.
+ */
+class WorkflowEntityFormBase extends EntityForm {
+
+  use DialogFormTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $workflow_transition = NULL) {
+    $form = parent::buildForm($form, $form_state);
+    $this->buildFormDialog($form, $form_state, TRUE);
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function actions(array $form, FormStateInterface $form_state) {
+    $actions = parent::actions($form, $form_state);
+    unset($actions['delete']);
+    return $actions;
+  }
+
+}
diff --git a/core/modules/workflows/src/Form/WorkflowStateAddForm.php b/core/modules/workflows/src/Form/WorkflowStateAddForm.php
index ab4a4c1..f76351b 100644
--- a/core/modules/workflows/src/Form/WorkflowStateAddForm.php
+++ b/core/modules/workflows/src/Form/WorkflowStateAddForm.php
@@ -2,18 +2,18 @@
 
 namespace Drupal\workflows\Form;
 
-use Drupal\Core\Entity\EntityForm;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Form\SubformState;
 use Drupal\Core\Plugin\PluginFormFactoryInterface;
 use Drupal\workflows\StateInterface;
+use Drupal\workflows\WorkflowInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Class WorkflowStateAddForm.
  */
-class WorkflowStateAddForm extends EntityForm {
+class WorkflowStateAddForm extends WorkflowEntityFormBase {
 
   /**
    * The plugin form factory.
@@ -168,15 +168,10 @@ public function save(array $form, FormStateInterface $form_state) {
   }
 
   /**
-   * {@inheritdoc}
+   * Route title callback.
    */
-  protected function actions(array $form, FormStateInterface $form_state) {
-    $actions['submit'] = [
-      '#type' => 'submit',
-      '#value' => $this->t('Save'),
-      '#submit' => ['::submitForm', '::save'],
-    ];
-    return $actions;
+  public function getTitle(WorkflowInterface $workflow) {
+    return $this->t('Add a new state to the @workflow workflow', ['@workflow' => $workflow->label()]);
   }
 
 }
diff --git a/core/modules/workflows/src/Form/WorkflowStateDeleteForm.php b/core/modules/workflows/src/Form/WorkflowStateDeleteForm.php
index bfca31e..8ec085d 100644
--- a/core/modules/workflows/src/Form/WorkflowStateDeleteForm.php
+++ b/core/modules/workflows/src/Form/WorkflowStateDeleteForm.php
@@ -12,6 +12,8 @@
  */
 class WorkflowStateDeleteForm extends ConfirmFormBase {
 
+  use DialogFormTrait;
+
   /**
    * The workflow entity the state being deleted belongs to.
    *
@@ -82,7 +84,10 @@ public function buildForm(array $form, FormStateInterface $form_state, WorkflowI
       return $form;
     }
 
-    return parent::buildForm($form, $form_state);
+    $form = parent::buildForm($form, $form_state);
+    $this->buildFormDialog($form, $form_state, TRUE);
+
+    return $form;
   }
 
   /**
diff --git a/core/modules/workflows/src/Form/WorkflowStateEditForm.php b/core/modules/workflows/src/Form/WorkflowStateEditForm.php
index a04998d..18c78b9 100644
--- a/core/modules/workflows/src/Form/WorkflowStateEditForm.php
+++ b/core/modules/workflows/src/Form/WorkflowStateEditForm.php
@@ -2,19 +2,18 @@
 
 namespace Drupal\workflows\Form;
 
-use Drupal\Core\Entity\EntityForm;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Form\SubformState;
 use Drupal\Core\Plugin\PluginFormFactoryInterface;
-use Drupal\Core\Url;
 use Drupal\workflows\StateInterface;
+use Drupal\workflows\WorkflowInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Class WorkflowStateEditForm.
  */
-class WorkflowStateEditForm extends EntityForm {
+class WorkflowStateEditForm extends WorkflowEntityFormBase {
 
   /**
    * The ID of the state that is being edited.
@@ -105,45 +104,6 @@ public function form(array $form, FormStateInterface $form_state) {
         ->buildConfigurationForm($form['type_settings'], $subform_state);
     }
 
-    $header = [
-      'label' => $this->t('Transition'),
-      'state' => $this->t('To'),
-      'operations' => $this->t('Operations'),
-    ];
-    $form['transitions'] = [
-      '#type' => 'table',
-      '#header' => $header,
-      '#empty' => $this->t('There are no transitions to or from this state yet.'),
-    ];
-    foreach ($state->getTransitions() as $transition) {
-      $links['edit'] = [
-        'title' => $this->t('Edit'),
-        'url' => Url::fromRoute('entity.workflow.edit_transition_form', [
-          'workflow' => $workflow->id(),
-          'workflow_transition' => $transition->id()
-        ]),
-      ];
-      $links['delete'] = [
-        'title' => t('Delete'),
-        'url' => Url::fromRoute('entity.workflow.delete_transition_form', [
-          'workflow' => $workflow->id(),
-          'workflow_transition' => $transition->id()
-        ]),
-      ];
-      $form['transitions'][$transition->id()] = [
-        'label' => [
-          '#markup' => $transition->label(),
-        ],
-        'state' => [
-          '#markup' => $transition->to()->label(),
-        ],
-        'operations' => [
-          '#type' => 'operations',
-          '#links' => $links,
-        ],
-      ];
-    }
-
     return $form;
   }
 
@@ -207,29 +167,11 @@ public function save(array $form, FormStateInterface $form_state) {
   }
 
   /**
-   * {@inheritdoc}
+   * Route title callback.
    */
-  protected function actions(array $form, FormStateInterface $form_state) {
-    $actions['submit'] = [
-      '#type' => 'submit',
-      '#value' => $this->t('Save'),
-      '#submit' => ['::submitForm', '::save'],
-    ];
-
-    $actions['delete'] = [
-      '#type' => 'link',
-      '#title' => $this->t('Delete'),
-      '#access' => $this->entity->access('delete-state:' . $this->stateId),
-      '#attributes' => [
-        'class' => ['button', 'button--danger'],
-      ],
-      '#url' => Url::fromRoute('entity.workflow.delete_state_form', [
-        'workflow' => $this->entity->id(),
-        'workflow_state' => $this->stateId
-      ])
-    ];
-
-    return $actions;
+  public function getTitle(WorkflowInterface $workflow, $workflow_state) {
+    $title = $this->t('Edit the @state state for the @workflow workflow', ['@state' => $workflow->getTypePlugin()->getState($workflow_state)->label(), '@workflow' => $workflow->label()]);
+    return $title;
   }
 
 }
diff --git a/core/modules/workflows/src/Form/WorkflowTransitionAddForm.php b/core/modules/workflows/src/Form/WorkflowTransitionAddForm.php
index 8452292..5fa4f8a 100644
--- a/core/modules/workflows/src/Form/WorkflowTransitionAddForm.php
+++ b/core/modules/workflows/src/Form/WorkflowTransitionAddForm.php
@@ -2,19 +2,19 @@
 
 namespace Drupal\workflows\Form;
 
-use Drupal\Core\Entity\EntityForm;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Form\SubformState;
 use Drupal\Core\Plugin\PluginFormFactoryInterface;
 use Drupal\workflows\State;
 use Drupal\workflows\TransitionInterface;
+use Drupal\workflows\WorkflowInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Class WorkflowTransitionAddForm.
  */
-class WorkflowTransitionAddForm extends EntityForm {
+class WorkflowTransitionAddForm extends WorkflowEntityFormBase {
 
   /**
    * The plugin form factory.
@@ -194,15 +194,11 @@ public function save(array $form, FormStateInterface $form_state) {
   }
 
   /**
-   * {@inheritdoc}
+   * Route title callback.
    */
-  protected function actions(array $form, FormStateInterface $form_state) {
-    $actions['submit'] = [
-      '#type' => 'submit',
-      '#value' => $this->t('Save'),
-      '#submit' => ['::submitForm', '::save'],
-    ];
-    return $actions;
+  public function getTitle(WorkflowInterface $workflow) {
+    $title = $this->t('Add a new transition to the @workflow workflow', ['@workflow' => $workflow->label()]);
+    return $title;
   }
 
 }
diff --git a/core/modules/workflows/src/Form/WorkflowTransitionDeleteForm.php b/core/modules/workflows/src/Form/WorkflowTransitionDeleteForm.php
index 27add72..4fff53c 100644
--- a/core/modules/workflows/src/Form/WorkflowTransitionDeleteForm.php
+++ b/core/modules/workflows/src/Form/WorkflowTransitionDeleteForm.php
@@ -12,6 +12,8 @@
  */
 class WorkflowTransitionDeleteForm extends ConfirmFormBase {
 
+  use DialogFormTrait;
+
   /**
    * The workflow entity the transition being deleted belongs to.
    *
@@ -83,8 +85,12 @@ public function buildForm(array $form, FormStateInterface $form_state, WorkflowI
     catch (\InvalidArgumentException $e) {
       throw new NotFoundHttpException();
     }
+    $this->transitionId = $this->transition->id();
     $this->workflow = $workflow;
-    return parent::buildForm($form, $form_state);
+
+    $form = parent::buildForm($form, $form_state);
+    $this->buildFormDialog($form, $form_state, TRUE);
+    return $form;
   }
 
   /**
diff --git a/core/modules/workflows/src/Form/WorkflowTransitionEditForm.php b/core/modules/workflows/src/Form/WorkflowTransitionEditForm.php
index 1406025..8bd3c77 100644
--- a/core/modules/workflows/src/Form/WorkflowTransitionEditForm.php
+++ b/core/modules/workflows/src/Form/WorkflowTransitionEditForm.php
@@ -2,20 +2,19 @@
 
 namespace Drupal\workflows\Form;
 
-use Drupal\Core\Entity\EntityForm;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Form\SubformState;
 use Drupal\Core\Plugin\PluginFormFactoryInterface;
-use Drupal\Core\Url;
 use Drupal\workflows\State;
 use Drupal\workflows\TransitionInterface;
+use Drupal\workflows\WorkflowInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Class WorkflowTransitionEditForm.
  */
-class WorkflowTransitionEditForm extends EntityForm {
+class WorkflowTransitionEditForm extends WorkflowEntityFormBase {
 
   /**
    * The ID of the transition that is being edited.
@@ -204,30 +203,11 @@ public function save(array $form, FormStateInterface $form_state) {
   }
 
   /**
-   * {@inheritdoc}
+   * Route title callback.
    */
-  protected function actions(array $form, FormStateInterface $form_state) {
-    $actions['submit'] = [
-      '#type' => 'submit',
-      '#value' => $this->t('Save'),
-      '#submit' => ['::submitForm', '::save'],
-    ];
-
-    $actions['delete'] = [
-      '#type' => 'link',
-      '#title' => $this->t('Delete'),
-      // Deleting a transition is editing a workflow.
-      '#access' => $this->entity->access('edit'),
-      '#attributes' => [
-        'class' => ['button', 'button--danger'],
-      ],
-      '#url' => Url::fromRoute('entity.workflow.delete_transition_form', [
-        'workflow' => $this->entity->id(),
-        'workflow_transition' => $this->transitionId
-      ])
-    ];
-
-    return $actions;
+  public function getTitle(WorkflowInterface $workflow, $workflow_transition) {
+    $title = $this->t('Edit the @transition transition for the @workflow workflow', ['@transition' => $workflow->getTypePlugin()->getTransition($workflow_transition)->label(), '@workflow' => $workflow->label()]);
+    return $title;
   }
 
 }
diff --git a/core/modules/workflows/tests/src/Functional/WorkflowUiTest.php b/core/modules/workflows/tests/src/Functional/WorkflowUiTest.php
index fcbcbd0..b8febd3 100644
--- a/core/modules/workflows/tests/src/Functional/WorkflowUiTest.php
+++ b/core/modules/workflows/tests/src/Functional/WorkflowUiTest.php
@@ -177,7 +177,7 @@ public function testWorkflowCreation() {
     // Delete the transition.
     $workflow = $workflow_storage->loadUnchanged('test');
     $this->assertTrue($workflow->getTypePlugin()->hasTransitionFromStateToState('published', 'published'), 'Can transition from published to published');
-    $this->clickLink('Delete');
+    $this->drupalGet('admin/config/workflow/workflows/manage/test/transition/save_and_publish/delete');
     $this->assertSession()->pageTextContains('Are you sure you want to delete Save and publish from Test?');
     $this->submitForm([], 'Delete');
     $workflow = $workflow_storage->loadUnchanged('test');
diff --git a/core/modules/workflows/tests/src/FunctionalJavascript/WorkflowEditFormTest.php b/core/modules/workflows/tests/src/FunctionalJavascript/WorkflowEditFormTest.php
new file mode 100644
index 0000000..210dc9e
--- /dev/null
+++ b/core/modules/workflows/tests/src/FunctionalJavascript/WorkflowEditFormTest.php
@@ -0,0 +1,208 @@
+<?php
+
+namespace Drupal\Tests\workflows\FunctionalJavascript;
+
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+use Drupal\workflows\Entity\Workflow;
+
+/**
+ * AJAX modal tests for the workflow list page.
+ *
+ * @group content_moderation
+ */
+class WorkflowEditFormTest extends JavascriptTestBase {
+
+  /**
+   * Modules to install.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'workflows',
+    'workflow_type_test',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    // Create a minimal workflow for testing.
+    $workflow = Workflow::create(['id' => 'test', 'type' => 'workflow_type_test']);
+    $workflow->getTypePlugin()
+      ->addState('draft', 'Draft')
+      ->addState('published', 'Published')
+      ->addState('archived', 'Archived')
+      ->addTransition('publish', 'Publish', ['draft', 'published'], 'published')
+      ->addTransition('create_new_draft', 'Create New Draft', ['published'], 'draft')
+      ->addTransition('restore', 'Restore', ['archived'], 'published');
+    $workflow->save();
+
+    $this->drupalLogin($this->drupalCreateUser(['administer workflows']));
+  }
+
+  /**
+   * Test states operations uses a modal dialog.
+   */
+  public function testWorkflowStatesForm() {
+    $session = $this->assertSession();
+    $path = 'admin/config/workflow/workflows/manage/test';
+    $this->drupalGet($path);
+
+    // Assert that the state edit form is displayed in a modal.
+    $this->click('[data-drupal-selector="edit-states-draft-operations"] a:contains(Edit)');
+    $session->assertWaitOnAjaxRequest();
+    $modal = $session->waitForElementVisible('css', '#drupal-modal');
+    $this->assertTrue($modal->isVisible(), 'Modal window found.');
+    $this->assertElementVisibleAfterWait('css', '.ui-dialog-title:contains(Edit the Draft state for the Test workflow)');
+    // Assert that the form errors are displayed in the modal.
+    $input = $session->elementExists('css', '#drupal-modal .form-item-label input[name="label"]');
+    $input->setValue("");
+    $save_button = $session->waitForElementVisible('css', '.ui-dialog-buttonpane .button--primary');
+    $this->assertTrue($save_button->isVisible(), 'Save button found.');
+    $save_button->click();
+    $session->assertWaitOnAjaxRequest();
+    $session->pageTextContains('Label field is required.');
+    // Assert that the cancel button closes the modal.
+    $this->click('.ui-dialog-buttonpane button:contains(Cancel)');
+    $session->assertWaitOnAjaxRequest();
+    $this->assertFalse($modal->isVisible(), 'Modal window closed.');
+    // Assert that saving the state saves the state and that we are stil at the
+    // same path.
+    $this->click('[data-drupal-selector="edit-states-draft-operations"] a:contains(Edit)');
+    $session->assertWaitOnAjaxRequest();
+    $input = $session->elementExists('css', '#drupal-modal .form-item-label input[name="label"]');
+    $input->setValue('Drafty');
+    $save_button = $session->waitForElementVisible('css', '.ui-dialog-buttonpane .button--primary');
+    $this->assertTrue($save_button->isVisible(), 'Save button found.');
+    $save_button->click();
+    $this->assertElementVisibleAfterWait('css', 'body:contains(Saved Drafty state)');
+    $session->addressEquals($path);
+
+    // Assert that the state delete form is displayed in a modal.
+    $this->click('[data-drupal-selector="edit-states-draft"] .dropbutton-toggle button');
+    $delete_button = $session->waitForElementVisible('css', '[data-drupal-selector="edit-states-draft"] a:contains(Delete)');
+    $this->assertTrue($delete_button->isVisible(), 'Delete state button found.');
+    $delete_button->click();
+    $session->assertWaitOnAjaxRequest();
+    $modal = $session->waitForElementVisible('css', '#drupal-modal');
+    $this->assertTrue($modal->isVisible(), 'Modal window found.');
+    $delete_button = $session->waitForElementVisible('css', '.ui-dialog-buttonpane .button--primary');
+    $this->assertTrue($save_button->isVisible(), 'Delete button found.');
+    // Assert that the cancel button closes the modal.
+    $this->click('.ui-dialog-buttonpane button:contains(Cancel)');
+    $session->assertWaitOnAjaxRequest();
+    $this->assertFalse($modal->isVisible(), 'Modal window closed.');
+    // Assert that deleting the state deletes the state and that we are stil at
+    // the same path.
+    $this->drupalGet($path);
+    $this->click('[data-drupal-selector="edit-states-draft"] .dropbutton-toggle button');
+    $delete_button = $session->waitForElementVisible('css', '[data-drupal-selector="edit-states-draft"] a:contains(Delete)');
+    $this->assertTrue($delete_button->isVisible(), 'Delete state button found.');
+    $delete_button->click();
+    $session->assertWaitOnAjaxRequest();
+    $modal = $session->waitForElementVisible('css', '#drupal-modal');
+    $this->assertTrue($modal->isVisible(), 'Modal window found.');
+    $delete_button = $session->waitForElementVisible('css', '.ui-dialog-buttonpane .button--primary');
+    $this->assertTrue($save_button->isVisible(), 'Delete button found.');
+    $delete_button->click();
+    $this->assertElementVisibleAfterWait('css', 'body:contains(State Drafty deleted)');
+    $session->addressEquals($path);
+  }
+
+  /**
+   * Test transitions operations uses a modal dialog.
+   */
+  public function testWorkflowTransitionsForm() {
+    $session = $this->assertSession();
+    $path = 'admin/config/workflow/workflows/manage/test';
+    $this->drupalGet($path);
+
+    // Assert that the transition edit form is displayed in a modal.
+    $this->click('[data-drupal-selector="edit-transitions-publish-operations"] a:contains(Edit)');
+    $session->assertWaitOnAjaxRequest();
+    $modal = $session->waitForElementVisible('css', '#drupal-modal');
+    $this->assertTrue($modal->isVisible(), 'Modal window found.');
+    $this->assertElementVisibleAfterWait('css', '.ui-dialog-title:contains(Edit the Publish transition for Test workflow)');
+    // Assert that the form errors are displayed in the modal.
+    // Empty the Label text input which is required.
+    $label_input = $session->elementExists('css', '#drupal-modal .form-item-label input[name="label"]');
+    $label_input->setValue("");
+    // Check the 'Archived' state checkbox.
+    $archived_checkbox = $session->elementExists('css', '[data-drupal-selector="edit-from-archived"]');
+    $archived_checkbox->check();
+    // Try to save the form to trigger the errors.
+    $save_button = $session->waitForElementVisible('css', '.ui-dialog-buttonpane .button--primary');
+    $this->assertTrue($save_button->isVisible(), 'Save button found.');
+    $save_button->click();
+    $session->assertWaitOnAjaxRequest();
+    $session->pageTextContains('Label field is required.');
+    $session->pageTextContains('The transition from Archived to Published already exists.');
+    // Assert that the cancel button closes the modal with errors on the form.
+    $this->click('.ui-dialog-buttonpane button:contains(Cancel)');
+    $session->assertWaitOnAjaxRequest();
+    $this->assertFalse($modal->isVisible(), 'Modal window closed.');
+
+    // Assert that saving the transition saves the transition and that we are
+    // still at the same path.
+    $this->click('[data-drupal-selector="edit-transitions-publish-operations"] a:contains(Edit)');
+    $session->assertWaitOnAjaxRequest();
+    $modal = $session->waitForElementVisible('css', '#drupal-modal');
+    $this->assertTrue($modal->isVisible(), 'Modal window found.');
+    // Assert that the form errors are displayed in the modal.
+    // Empty the Label text input which is required.
+    $label_input = $session->elementExists('css', '#drupal-modal .form-item-label input[name="label"]');
+    $label_input->setValue('Publishedlicius');
+    $save_button = $session->waitForElementVisible('css', '.ui-dialog-buttonpane .button--primary');
+    $this->assertTrue($save_button->isVisible(), 'Save button found.');
+    $save_button->click();
+    $this->assertElementVisibleAfterWait('css', 'body:contains(Saved Publishedlicius transition)');
+    $session->addressEquals($path);
+
+    // Assert that the transition delete form is displayed in a modal.
+    $this->click('[data-drupal-selector="edit-transitions-create-new-draft"] .dropbutton-toggle button');
+    $delete_button = $session->waitForElementVisible('css', '[data-drupal-selector="edit-transitions-create-new-draft"] a:contains(Delete)');
+    $this->assertTrue($delete_button->isVisible(), 'Delete transition button found.');
+    $delete_button->click();
+    $session->assertWaitOnAjaxRequest();
+    $this->assertTrue($modal->isVisible(), 'Modal window found.');
+    $delete_button = $session->waitForElementVisible('css', '.ui-dialog-buttonpane .button--primary');
+    $this->assertTrue($save_button->isVisible(), 'Delete button found.');
+    // Assert that the cancel button closes the modal.
+    $this->click('.ui-dialog-buttonpane button:contains(Cancel)');
+    $session->assertWaitOnAjaxRequest();
+    $this->assertFalse($modal->isVisible(), 'Modal window closed.');
+
+    // Assert that deleting the transition deletes the transition and that we
+    // are still at the same path.
+    $this->drupalGet($path);
+    $this->click('[data-drupal-selector="edit-transitions-create-new-draft"] .dropbutton-toggle button');
+    $delete_button = $session->waitForElementVisible('css', '[data-drupal-selector="edit-transitions-create-new-draft"] a:contains(Delete)');
+    $this->assertTrue($delete_button->isVisible(), 'Delete transition button found.');
+    $delete_button->click();
+    $delete_button = $session->waitForElementVisible('css', '.ui-dialog-buttonpane .button--primary');
+    $this->assertTrue($save_button->isVisible(), 'Delete button found.');
+    $delete_button->click();
+    $this->assertElementVisibleAfterWait('css', 'body:contains(Create New Draft transition deleted)');
+    $session->addressEquals($path);
+  }
+
+  /**
+   * Asserts the specified selector is visible after a wait.
+   *
+   * @param string $selector
+   *   The selector engine name. See ElementInterface::findAll() for the
+   *   supported selectors.
+   * @param string|array $locator
+   *   The selector locator.
+   * @param int $timeout
+   *   (Optional) Timeout in milliseconds, defaults to 10000.
+   *
+   * @todo Remove and use method on JSWebAssert in
+   *   https://www.drupal.org/node/2898777.
+   */
+  protected function assertElementVisibleAfterWait($selector, $locator, $timeout = 10000) {
+    $this->assertNotEmpty($this->assertSession()->waitForElementVisible($selector, $locator, $timeout));
+  }
+
+}
diff --git a/core/modules/workflows/workflows.routing.yml b/core/modules/workflows/workflows.routing.yml
index 329ed10..7290c5f 100644
--- a/core/modules/workflows/workflows.routing.yml
+++ b/core/modules/workflows/workflows.routing.yml
@@ -2,7 +2,7 @@ entity.workflow.add_state_form:
   path: '/admin/config/workflow/workflows/manage/{workflow}/add_state'
   defaults:
     _entity_form: 'workflow.add-state'
-    _title: 'Add state'
+    _title_callback: '\Drupal\workflows\Form\WorkflowStateAddForm::getTitle'
   requirements:
     _entity_access: 'workflow.edit'
 
@@ -10,7 +10,7 @@ entity.workflow.edit_state_form:
   path: '/admin/config/workflow/workflows/manage/{workflow}/state/{workflow_state}'
   defaults:
     _entity_form: 'workflow.edit-state'
-    _title: 'Edit state'
+    _title_callback: '\Drupal\workflows\Form\WorkflowStateEditForm::getTitle'
   requirements:
     _entity_access: 'workflow.edit'
 
@@ -26,7 +26,7 @@ entity.workflow.add_transition_form:
   path: '/admin/config/workflow/workflows/manage/{workflow}/add_transition'
   defaults:
     _entity_form: 'workflow.add-transition'
-    _title: 'Add transition'
+    _title_callback: '\Drupal\workflows\Form\WorkflowTransitionAddForm::getTitle'
   requirements:
     _entity_access: 'workflow.edit'
 
@@ -34,7 +34,7 @@ entity.workflow.edit_transition_form:
   path: '/admin/config/workflow/workflows/manage/{workflow}/transition/{workflow_transition}'
   defaults:
     _entity_form: 'workflow.edit-transition'
-    _title: 'Edit transition'
+    _title_callback: '\Drupal\workflows\Form\WorkflowTransitionEditForm::getTitle'
   requirements:
     _entity_access: 'workflow.edit'
 
