diff --git a/core/core.libraries.yml b/core/core.libraries.yml
index 773f2bd..6fff3c4 100644
--- a/core/core.libraries.yml
+++ b/core/core.libraries.yml
@@ -225,6 +225,15 @@ drupal.machine-name:
     - core/drupalSettings
     - core/drupal.form
 
+drupal.message:
+  version: VERSION
+  js:
+    misc/message.js: {}
+  dependencies:
+    - core/jquery
+    - core/drupal
+    - core/drupal.debounce
+
 drupal.progress:
   version: VERSION
   js:
diff --git a/core/lib/Drupal/Core/Render/Element/StatusMessages.php b/core/lib/Drupal/Core/Render/Element/StatusMessages.php
index 55990e6..26be82b 100644
--- a/core/lib/Drupal/Core/Render/Element/StatusMessages.php
+++ b/core/lib/Drupal/Core/Render/Element/StatusMessages.php
@@ -32,6 +32,9 @@ public function getInfo() {
       '#pre_render' => [
         get_class() . '::generatePlaceholder',
       ],
+      '#attached' => [
+        'library' => ['core/drupal.message'],
+      ],
     ];
   }
 
diff --git a/core/misc/announce.js b/core/misc/announce.js
index acf850a..0b2db67 100644
--- a/core/misc/announce.js
+++ b/core/misc/announce.js
@@ -34,7 +34,7 @@
    *   Attaches the behavior for drupalAnnouce.
    */
   Drupal.behaviors.drupalAnnounce = {
-    attach: function (context) {
+    attach: function () {
       // Create only one aria-live element.
       if (!liveElement) {
         liveElement = document.createElement('div');
@@ -50,39 +50,45 @@
   /**
    * Concatenates announcements to a single string; appends to the live region.
    */
-  function announce() {
+  function processAnnounce() {
     var text = [];
     var priority = 'polite';
     var announcement;
 
-    // Create an array of announcement strings to be joined and appended to the
-    // aria live region.
-    var il = announcements.length;
-    for (var i = 0; i < il; i++) {
-      announcement = announcements.pop();
-      text.unshift(announcement.text);
-      // If any of the announcements has a priority of assertive then the group
-      // of joined announcements will have this priority.
-      if (announcement.priority === 'assertive') {
-        priority = 'assertive';
+    if (announcements.length) {
+      // Create an array of announcement strings to be joined and appended to the
+      // aria live region.
+      var il = announcements.length;
+      for (var i = 0; i < il; i++) {
+        announcement = announcements.pop();
+        text.unshift(announcement.text);
+        // If any of the announcements has a priority of assertive then the group
+        // of joined announcements will have this priority.
+        if (announcement.priority === 'assertive') {
+          priority = 'assertive';
+        }
       }
-    }
 
-    if (text.length) {
-      // Clear the liveElement so that repeated strings will be read.
-      liveElement.innerHTML = '';
-      // Set the busy state to true until the node changes are complete.
-      liveElement.setAttribute('aria-busy', 'true');
-      // Set the priority to assertive, or default to polite.
-      liveElement.setAttribute('aria-live', priority);
-      // Print the text to the live region. Text should be run through
-      // Drupal.t() before being passed to Drupal.announce().
-      liveElement.innerHTML = text.join('\n');
-      // The live text area is updated. Allow the AT to announce the text.
-      liveElement.setAttribute('aria-busy', 'false');
+      if (text.length) {
+        // Clear the liveElement so that repeated strings will be read.
+        liveElement.innerHTML = '';
+        // Set the busy state to true until the node changes are complete.
+        liveElement.setAttribute('aria-busy', 'true');
+        // Set the priority to assertive, or default to polite.
+        liveElement.setAttribute('aria-live', priority);
+        // Print the text to the live region. Text should be run through
+        // Drupal.t() before being passed to Drupal.announce().
+        liveElement.innerHTML = text.join('\n');
+        // The live text area is updated. Allow the AT to announce the text.
+        liveElement.setAttribute('aria-busy', 'false');
+      }
     }
   }
 
+  // 200 ms is right at the cusp where humans notice a pause, so we will wait
+  // at most this much time before the set of queued announcements is read.
+  var debouncedProcessAnnounce = debounce(processAnnounce, 200);
+
   /**
    * Triggers audio UAs to read the supplied text.
    *
@@ -94,9 +100,9 @@
    * messages. These messages are then joined and append to the aria-live region
    * as one text node.
    *
-   * @param {string} text
+   * @param {String} text
    *   A string to be read by the UA.
-   * @param {string} [priority='polite']
+   * @param {String} priority
    *   A string to indicate the priority of the message. Can be either
    *   'polite' or 'assertive'.
    *
@@ -106,15 +112,14 @@
    * @see http://www.w3.org/WAI/PF/aria-practices/#liveprops
    */
   Drupal.announce = function (text, priority) {
-    // Save the text and priority into a closure variable. Multiple simultaneous
-    // announcements will be concatenated and read in sequence.
-    announcements.push({
-      text: text,
-      priority: priority
-    });
-    // Immediately invoke the function that debounce returns. 200 ms is right at
-    // the cusp where humans notice a pause, so we will wait
-    // at most this much time before the set of queued announcements is read.
-    return (debounce(announce, 200)());
+    if (typeof text === 'string') {
+      // Save the text and priority into a closure variable. Multiple simultaneous
+      // announcements will be concatenated and read in sequence.
+      announcements.push({
+        text: text,
+        priority: priority
+      });
+      debouncedProcessAnnounce(announcements);
+    }
   };
 }(Drupal, Drupal.debounce));
diff --git a/core/modules/system/templates/status-messages.html.twig b/core/modules/system/templates/status-messages.html.twig
index 73bd9b7..536a6de 100644
--- a/core/modules/system/templates/status-messages.html.twig
+++ b/core/modules/system/templates/status-messages.html.twig
@@ -23,25 +23,27 @@
  * @ingroup themeable
  */
 #}
-{% for type, messages in message_list %}
-  <div role="contentinfo" aria-label="{{ status_headings[type] }}"{{ attributes|without('role', 'aria-label') }}>
-    {% if type == 'error' %}
-      <div role="alert">
-    {% endif %}
-      {% if status_headings[type] %}
-        <h2 class="visually-hidden">{{ status_headings[type] }}</h2>
+<div data-drupal-messages>
+  {% for type, messages in message_list %}
+    <div role="contentinfo" aria-label="{{ status_headings[type] }}"{{ attributes|without('role', 'aria-label') }}>
+      {% if type == 'error' %}
+        <div role="alert">
       {% endif %}
-      {% if messages|length > 1 %}
-        <ul>
-          {% for message in messages %}
-            <li>{{ message }}</li>
-          {% endfor %}
-        </ul>
-      {% else %}
-        {{ messages|first }}
+        {% if status_headings[type] %}
+          <h2 class="visually-hidden">{{ status_headings[type] }}</h2>
+        {% endif %}
+        {% if messages|length > 1 %}
+          <ul>
+            {% for message in messages %}
+              <li>{{ message }}</li>
+            {% endfor %}
+          </ul>
+        {% else %}
+          {{ messages|first }}
+        {% endif %}
+      {% if type == 'error' %}
+        </div>
       {% endif %}
-    {% if type == 'error' %}
-      </div>
-    {% endif %}
-  </div>
-{% endfor %}
+    </div>
+  {% endfor %}
+</div>
\ No newline at end of file
diff --git a/core/themes/classy/templates/misc/status-messages.html.twig b/core/themes/classy/templates/misc/status-messages.html.twig
index 683a111..6042687 100644
--- a/core/themes/classy/templates/misc/status-messages.html.twig
+++ b/core/themes/classy/templates/misc/status-messages.html.twig
@@ -21,7 +21,7 @@
  *   - class: HTML classes.
  */
 #}
-{{ attach_library('classy/messages') }}
+<div data-drupal-messages>
 {% block messages %}
 {% for type, messages in message_list %}
   {%
@@ -54,3 +54,4 @@
   {{ attributes.removeClass(classes) }}
 {% endfor %}
 {% endblock messages %}
+</div>
