diff --git a/core/lib/Drupal/Core/Ajax/MessageCommand.php b/core/lib/Drupal/Core/Ajax/MessageCommand.php
new file mode 100644
index 0000000000..9c8252d08e
--- /dev/null
+++ b/core/lib/Drupal/Core/Ajax/MessageCommand.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Drupal\Core\Ajax;
+
+use Drupal\Core\Asset\AttachedAssets;
+
+/**
+ * AJAX command for a JavaScript Drupal.message() call.
+ *
+ * @ingroup ajax
+ */
+class MessageCommand implements CommandInterface, CommandWithAttachedAssetsInterface {
+
+  /**
+   * The message text.
+   *
+   * @var string
+   */
+  protected $message;
+
+  /**
+   * Whether to clear previous messages.
+   *
+   * @var boolean
+   */
+  protected $clearPrevious;
+
+  /**
+   * The query selector for the element the message will appear in.
+   *
+   * @var string
+   */
+  protected $messageWrapperQuerySelector;
+
+  /**
+   * The options passed to Drupal.message().add().
+   *
+   * @var array
+   */
+  protected $messageOptions;
+
+  /**
+   * Constructs a MessageCommand object.
+   *
+   * @param string $message
+   *   The text of the message.
+   * @param string|null $messageWrapperQuerySelector
+   *   The query selector of the element to display messages in when they
+   *   should be displayed somewhere other than the default
+   *   [data-drupal-messages].
+   * @param array $messageOptions
+   *   The options passed to Drupal.message().add().
+   * @param bool $clearPrevious
+   *   Whether to clear previous messages.
+   */
+  public function __construct($message, $messageWrapperQuerySelector = NULL, array $messageOptions = [], $clearPrevious = TRUE) {
+    $this->message = $message;
+    $this->messageWrapperQuerySelector = $messageWrapperQuerySelector;
+    $this->messageOptions = $messageOptions;
+    $this->clearPrevious = $clearPrevious;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render() {
+    return [
+      'command' => 'message',
+      'message' => $this->message,
+      'messageWrapperQuerySelector' => $this->messageWrapperQuerySelector,
+      'messageOptions' => $this->messageOptions,
+      'clearPrevious' => $this->clearPrevious,
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getAttachedAssets() {
+    $assets = new AttachedAssets();
+    $assets->setLibraries(['core/drupal.message']);
+    return $assets;
+  }
+
+}
diff --git a/core/misc/ajax.es6.js b/core/misc/ajax.es6.js
index 1b26397c5c..703f67610a 100644
--- a/core/misc/ajax.es6.js
+++ b/core/misc/ajax.es6.js
@@ -655,7 +655,9 @@
       // the complete response.
       this.ajaxing = false;
       window.alert(
-        `An error occurred while attempting to process ${this.options.url}: ${e.message}`,
+        `An error occurred while attempting to process ${this.options.url}: ${
+          e.message
+        }`,
       );
       // For consistency, return a rejected Deferred (i.e., jqXHR's superclass)
       // so that calling code can take appropriate action.
@@ -748,7 +750,9 @@
       // the complete response.
       ajax.ajaxing = false;
       window.alert(
-        `An error occurred while attempting to process ${ajax.options.url}: ${e.message}`,
+        `An error occurred while attempting to process ${ajax.options.url}: ${
+          e.message
+        }`,
       );
     }
   };
@@ -1562,5 +1566,29 @@
         } while (match);
       }
     },
+
+    /**
+     * Command to use Drupal.Message() via AJAX command.
+     *
+     * @param {Drupal.Ajax} [ajax]
+     *   {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
+     * @param {object} response
+     *   The response from the Ajax request.
+     * @param {string} response.message
+     *   The message text.
+     * @param {string} response.messageOptions
+     *   The options argument for Drupal.Message().add().
+     * @param {bool} response.clearPrevious
+     *   If TRUE, clear previous messages.
+     */
+    message(ajax, response) {
+      const messages = new Drupal.Message(
+        document.querySelector(response.messageWrapperQuerySelector),
+      );
+      if (response.clearPrevious) {
+        messages.clear();
+      }
+      messages.add(response.message, response.messageOptions);
+    },
   };
 })(jQuery, window, Drupal, drupalSettings);
diff --git a/core/misc/ajax.js b/core/misc/ajax.js
index 85cfa0739c..7df2501987 100644
--- a/core/misc/ajax.js
+++ b/core/misc/ajax.js
@@ -639,6 +639,13 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr
           document.styleSheets[0].addImport(match[1]);
         } while (match);
       }
+    },
+    message: function message(ajax, response) {
+      var messages = new Drupal.Message(document.querySelector(response.messageWrapperQuerySelector));
+      if (response.clearPrevious) {
+        messages.clear();
+      }
+      messages.add(response.message, response.messageOptions);
     }
   };
 })(jQuery, window, Drupal, drupalSettings);
\ No newline at end of file
diff --git a/core/misc/message.es6.js b/core/misc/message.es6.js
index 252d629310..cc6fb62806 100644
--- a/core/misc/message.es6.js
+++ b/core/misc/message.es6.js
@@ -162,6 +162,9 @@
      * @name Drupal.Message~messageDefinition.clear
      */
     clear() {
+      if (!this.messageWrapper) {
+        this.messageWrapper = Drupal.Message.defaultWrapper();
+      }
       Array.prototype.forEach.call(
         this.messageWrapper.querySelectorAll('[data-drupal-message-id]'),
         message => {
diff --git a/core/misc/message.js b/core/misc/message.js
index 38bc7b3a3a..9dc1fac77d 100644
--- a/core/misc/message.js
+++ b/core/misc/message.js
@@ -61,6 +61,9 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons
       value: function clear() {
         var _this = this;
 
+        if (!this.messageWrapper) {
+          this.messageWrapper = Drupal.Message.defaultWrapper();
+        }
         Array.prototype.forEach.call(this.messageWrapper.querySelectorAll('[data-drupal-message-id]'), function (message) {
           _this.messageWrapper.removeChild(message);
         });
diff --git a/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml b/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml
index 875b7caa96..a06e756572 100644
--- a/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml
+++ b/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml
@@ -77,3 +77,11 @@ ajax_test.render_error:
     _controller: '\Drupal\ajax_test\Controller\AjaxTestController::renderError'
   requirements:
     _access: 'TRUE'
+
+ajax_test.message_form:
+  path: '/ajax-test/message'
+  defaults:
+    _title: 'Ajax Message Form'
+    _form: '\Drupal\ajax_test\Form\AjaxTestMessageForm'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestMessageForm.php b/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestMessageForm.php
new file mode 100644
index 0000000000..f43691f58f
--- /dev/null
+++ b/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestMessageForm.php
@@ -0,0 +1,113 @@
+<?php
+
+namespace Drupal\ajax_test\Form;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\MessageCommand;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Form for testing AJAX MessageCommand.
+ *
+ * @internal
+ */
+class AjaxTestMessageForm extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'ajax_test_message_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['alternate-message-container'] = [
+      '#type' => 'container',
+      '#id' => 'alternate-message-container',
+    ];
+    $form['button_default'] = [
+      '#type' => 'submit',
+      '#name' => 'makedefaultmessage',
+      '#value' => 'Make Message In Default Location',
+      '#ajax' => [
+        'callback' => '::makeMessageDefault',
+      ],
+    ];
+    $form['button_alternate'] = [
+      '#type' => 'submit',
+      '#name' => 'makealternatemessage',
+      '#value' => 'Make Message In Alternate Location',
+      '#ajax' => [
+        'callback' => '::makeMessageAlternate',
+      ],
+    ];
+    $form['button_warning'] = [
+      '#type' => 'submit',
+      '#name' => 'makewarningmessage',
+      '#value' => 'Make Warning Message',
+      '#ajax' => [
+        'callback' => '::makeMessageWarning',
+      ],
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+
+  }
+
+  /**
+   * Callback for testing MessageCommand with default settings.
+   *
+   * @return \Drupal\Core\Ajax\AjaxResponse
+   *   The AJAX response.
+   */
+  public function makeMessageDefault() {
+    $response = new AjaxResponse();
+    $response->addCommand(new MessageCommand('I am a message in the default location'));
+
+    return $response;
+  }
+
+  /**
+   * Callback for testing MessageCommand using an alternate message location.
+   *
+   * @return \Drupal\Core\Ajax\AjaxResponse
+   *   The AJAX response.
+   */
+  public function makeMessageAlternate() {
+    $response = new AjaxResponse();
+    $response->addCommand(new MessageCommand('I am a message in an alternate location', '#alternate-message-container'));
+
+    return $response;
+  }
+
+  /**
+   * Callback for testing MessageCommand with warning status.
+   *
+   * @return \Drupal\Core\Ajax\AjaxResponse
+   *   The AJAX response.
+   */
+  public function makeMessageWarning() {
+    $response = new AjaxResponse();
+    $response->addCommand(new MessageCommand('I am a warning message', NULL, ['type' => 'warning']));
+
+    return $response;
+  }
+
+}
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MessageTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MessageTest.php
new file mode 100644
index 0000000000..3bbcc7a18f
--- /dev/null
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MessageTest.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Drupal\FunctionalJavascriptTests\Ajax;
+
+use Drupal\Core\Ajax\MessageCommand;
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+
+/**
+ * Tests adding messages via AJAX command.
+ *
+ * @group Ajax
+ */
+class MessageTest extends WebDriverTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['ajax_test'];
+
+  /**
+   * Test AJAX MessageCommand use in a form.
+   */
+  public function testMessage() {
+    $page = $this->getSession()->getPage();
+    $assert_session = $this->assertSession();
+
+    $this->drupalGet('ajax-test/message');
+    $page->pressButton('Make Message In Default Location');
+    $assert_session->waitForElementVisible('css', '[data-drupal-messages] .messages--status:contains("I am a message in the default location.")');
+    $assert_session->pageTextNotContains('I am a message in an alternate location.');
+
+    $this->drupalGet('ajax-test/message');
+    $page->pressButton('Make Message In Alternate Location');
+    $assert_session->waitForElementVisible('css', '#alternate-message-container .messages--status:contains("I am a message in an alternate location.")');
+    $assert_session->pageTextNotContains('I am a message in the default location.');
+
+    $page->pressButton('Make Warning Message');
+    $assert_session->waitForElementVisible('css', '[data-drupal-messages] .messages--warning:contains("I am a warning message")');
+    $assert_session->pageTextContains('I am a warning message in the default location.');
+
+    // Test that by default, previous messages in a location are removed.
+    $this->drupalGet('ajax-test/message');
+    for ($i = 0; $i < 6; $i++) {
+      $page->pressButton('Make Message In Default Location');
+      $assert_session->waitForElementVisible('css', '[data-drupal-messages] .messages--status:contains("I am a message in the default location.")');
+      $assert_session->elementsCount('css', '.messages__wrapper .messages', 1);
+      $page->pressButton('Make Warning Message');
+      $assert_session->waitForElementVisible('css', '[data-drupal-messages] .messages--warning:contains("I am a warning message in the default location.")');
+      $assert_session->elementsCount('css', '.messages__wrapper .messages', 1);
+    }
+
+    // Test that if MessageCommand::clearPrevious is FALSE, messages will not be cleared.
+    $this->drupalGet('ajax-test/message');
+    for ($i = 0; $i < 6; $i++) {
+      $page->pressButton('Make Message In Alternate Location');
+      $assert_session->assertWaitOnAjaxRequest();
+      $assert_session->elementsCount('css', '#alternate-message-container .messages', $i+1);
+    }
+
+  }
+
+}
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Core/JsMessageTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Core/JsMessageTest.php
index 9aeb4b6498..89c0b1ec02 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Core/JsMessageTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Core/JsMessageTest.php
@@ -6,7 +6,7 @@
 use Drupal\js_message_test\Controller\JSMessageTestController;
 
 /**
- * Tests core/drupal.messages library.
+ * Tests core/drupal.message library.
  *
  * @group Javascript
  */
