diff --git a/flag.libraries.yml b/flag.libraries.yml
index e4322a6..29f8cfe 100644
--- a/flag.libraries.yml
+++ b/flag.libraries.yml
@@ -13,3 +13,15 @@ flag.link:
   css:
     theme:
       css/flag-link.css: {}
+
+flag.link.ajax:
+  version: VERSION
+  js:
+    js/flag.js: {}
+  css:
+    theme:
+      css/flag-link.css: {}
+  dependencies:
+    - core/jquery
+    - core/drupal
+    - core/drupalSettings
\ No newline at end of file
diff --git a/flag.module b/flag.module
index 5bce96b..83102e3 100644
--- a/flag.module
+++ b/flag.module
@@ -279,6 +279,16 @@ function flag_theme() {
         'flaggable' => NULL,
       ],
     ],
+    'flag.ajax' => [
+      'variables' => [
+        'attributes' => [],
+        'title' => NULL,
+        'action' => 'flag',
+        'flag' => NULL,
+        'flaggable' => NULL,
+        'entity_id' => NULL,
+      ],
+    ],
   ];
 }
 
diff --git a/js/flag.js b/js/flag.js
new file mode 100644
index 0000000..77ee7a7
--- /dev/null
+++ b/js/flag.js
@@ -0,0 +1,246 @@
+(function ($, Drupal, drupalSettings) {
+
+  'use strict';
+
+  /**
+   * Terminology:
+   *
+   *   "Link" means "Everything which is in flag.tpl.php" --and this may contain
+   *   much more than the <A> element. On the other hand, when we speak
+   *   specifically of the <A> element, we say "element" or "the <A> element".
+   */
+
+  /**
+   * The main behavior to perform AJAX toggling of links.
+   *
+   * @param context
+   *   The DOM context.
+   */
+  Drupal.flagLink = function (context) {
+
+    /**
+     * Helper function. Updates a link's HTML with a new one.
+     *
+     * @param element
+     *   The <A> element.
+     * @param newHtml
+     *   The replacement html.
+     * @return
+     *   The new link.
+     */
+    function updateLink(element, newHtml) {
+      var $newLink = $(newHtml);
+
+      // Initially hide the message so we can fade it in.
+      $('.flag-message', $newLink).css('display', 'none');
+
+      // Reattach the behavior to the new <A> element. This element
+      // is either whithin the wrapper or it is the outer element itself.
+      var $nucleus = $newLink.is('a') ? $newLink : $('a.flag', $newLink);
+      $nucleus.addClass('flag-processed').click(flagClick);
+
+      // Find the wrapper of the old link.
+      var $wrapper = $(element).parents('.flag-wrapper:first');
+      // Replace the old link with the new one.
+      $wrapper.after($newLink).remove();
+      Drupal.attachBehaviors($newLink.get(0));
+
+      $('.flag-message', $newLink).fadeIn();
+      setTimeout(function () {
+        $('.flag-message.flag-auto-remove', $newLink).fadeOut();
+      }, 3000);
+      return $newLink.get(0);
+    }
+
+    /**
+     * A click handler that is attached to all <A class="flag"> elements.
+     *
+     * @param event
+     *   The event.
+     */
+    function flagClick(event) {
+      // Prevent the default browser click handler
+      event.preventDefault();
+
+      // 'this' won't point to the element when it's inside the ajax closures,
+      // so we reference it using a variable.
+      var element = this;
+
+      // While waiting for a server response, the wrapper will have a
+      // 'flag-waiting' class. Themers are thus able to style the link
+      // differently, e.g., by displaying a throbber.
+      var $wrapper = $(element).parents('.flag-wrapper');
+      if ($wrapper.is('.flag-waiting')) {
+        // Guard against double-clicks.
+        return false;
+      }
+      $wrapper.addClass('flag-waiting');
+
+      // Hide any other active messages.
+      $('span.flag-message:visible').fadeOut();
+
+      // Send POST request
+      $.ajax({
+        type: 'POST',
+        url: element.href,
+        data: {js: true},
+        dataType: 'json',
+        success: function (data) {
+          data.link = $wrapper.get(0);
+          $.event.trigger('flagGlobalBeforeLinkUpdate', [data]);
+          if (!data.preventDefault) { // A handler may cancel updating the link.
+            data.link = updateLink(element, data.newLink);
+          }
+
+          // Find all the link wrappers on the page for this flag, but exclude
+          // the triggering element because Flag's own javascript updates it.
+          var $wrappers = $('.flag-wrapper.flag-' + data.flagName.flagNameToCSS() + '-' + data.contentId).not(data.link);
+          var $newLink = $(data.newLink);
+
+          // Hide message, because we want the message to be shown on the triggering element alone.
+          $('.flag-message', $newLink).hide();
+
+          // Finally, update the page.
+          $wrappers = $newLink.replaceAll($wrappers);
+          Drupal.attachBehaviors($wrappers.parent());
+
+          $.event.trigger('flagGlobalAfterLinkUpdate', [data]);
+        },
+        error: function (xmlhttp) {
+          alert('An HTTP error ' + xmlhttp.status + ' occurred.\n' + element.href);
+          $wrapper.removeClass('flag-waiting');
+        }
+      });
+    }
+
+    $('a.flag-link-toggle:not(.flag-processed)', context).addClass('flag-processed').click(flagClick);
+  };
+
+  /**
+   * Prevent anonymous flagging unless the user has JavaScript enabled.
+   */
+  Drupal.flagAnonymousLinks = function (context) {
+    $('a.flag:not(.flag-anonymous-processed)', context).each(function () {
+      this.href += (this.href.match(/\?/) ? '&' : '?') + 'has_js=1';
+      $(this).addClass('flag-anonymous-processed');
+    });
+  };
+
+  String.prototype.flagNameToCSS = function () {
+    return this.replace(/_/g, '-');
+  };
+
+  /**
+   * A behavior specifically for anonymous users. Update links to the proper state.
+   */
+  Drupal.flagAnonymousLinkTemplates = function (context) {
+    // Swap in current links. Cookies are set by PHP's setcookie() upon flagging.
+
+    var templates = drupalSettings.flag.templates;
+
+    // Build a list of user-flags.
+    var userFlags = Drupal.flagCookie('flags');
+    if (userFlags) {
+      userFlags = userFlags.split('+');
+      for (var n in userFlags) {
+        var flagInfo = userFlags[n].match(/(\w+)_(\d+)/);
+        var flagName = flagInfo[1];
+        var contentId = flagInfo[2];
+        // User flags always default to off and the JavaScript toggles them on.
+        if (templates[flagName + '_' + contentId]) {
+          $('.flag-' + flagName.flagNameToCSS() + '-' + contentId, context).after(templates[flagName + '_' + contentId]).remove();
+        }
+      }
+    }
+
+    // Build a list of global flags.
+    var globalFlags = document.cookie.match(/flag_global_(\w+)_(\d+)=([01])/g);
+    if (globalFlags) {
+      for (var n in globalFlags) {
+        var flagInfo = globalFlags[n].match(/flag_global_(\w+)_(\d+)=([01])/);
+        var flagName = flagInfo[1];
+        var contentId = flagInfo[2];
+        var flagState = (flagInfo[3] == '1') ? 'flag' : 'unflag';
+        // Global flags are tricky, they may or may not be flagged in the page
+        // cache. The template always contains the opposite of the current state.
+        // So when checking global flag cookies, we need to make sure that we
+        // don't swap out the link when it's already in the correct state.
+        if (templates[flagName + '_' + contentId]) {
+          $('.flag-' + flagName.flagNameToCSS() + '-' + contentId, context).each(function () {
+            if ($(this).find('.' + flagState + '-action').size()) {
+              $(this).after(templates[flagName + '_' + contentId]).remove();
+            }
+          });
+        }
+      }
+    }
+  };
+
+  /**
+   * Utility function used to set Flag cookies.
+   *
+   * Note this is a direct copy of the jQuery cookie library.
+   * Written by Klaus Hartl.
+   */
+  Drupal.flagCookie = function (name, value, options) {
+    if (typeof value != 'undefined') { // name and value given, set cookie
+      options = options || {};
+      if (value === null) {
+        value = '';
+        options = $.extend({}, options); // clone object since it's unexpected behavior if the expired property were changed
+        options.expires = -1;
+      }
+      var expires = '';
+      if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) {
+        var date;
+        if (typeof options.expires == 'number') {
+          date = new Date();
+          date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000));
+        } else {
+          date = options.expires;
+        }
+        expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE
+      }
+      // NOTE Needed to parenthesize options.path and options.domain
+      // in the following expressions, otherwise they evaluate to undefined
+      // in the packed version for some reason...
+      var path = options.path ? '; path=' + (options.path) : '';
+      var domain = options.domain ? '; domain=' + (options.domain) : '';
+      var secure = options.secure ? '; secure' : '';
+      document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join('');
+    } else { // only name given, get cookie
+      var cookieValue = null;
+      if (document.cookie && document.cookie != '') {
+        var cookies = document.cookie.split(';');
+        for (var i = 0; i < cookies.length; i++) {
+          var cookie = jQuery.trim(cookies[i]);
+          // Does this cookie string begin with the name we want?
+          if (cookie.substring(0, name.length + 1) == (name + '=')) {
+            cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+            break;
+          }
+        }
+      }
+      return cookieValue;
+    }
+  };
+
+  Drupal.behaviors.flagLink = {};
+  Drupal.behaviors.flagLink.attach = function (context) {
+    // For anonymous users with the page cache enabled, swap out links with their
+    // current state for the user.
+    if (drupalSettings.flag && drupalSettings.flag.templates) {
+      Drupal.flagAnonymousLinkTemplates(context);
+    }
+
+    // For all anonymous users, require JavaScript for flagging to prevent spiders
+    // from flagging things inadvertently.
+    if (drupalSettings.flag && drupalSettings.flag.anonymous) {
+      Drupal.flagAnonymousLinks(context);
+    }
+
+    // On load, bind the click behavior for all links on the page.
+    Drupal.flagLink(context);
+  };
+
+})(jQuery, Drupal, drupalSettings);
diff --git a/src/Plugin/ActionLink/AJAXactionLink.php b/src/Plugin/ActionLink/AJAXactionLink.php
index f58f7ec..338de0f 100644
--- a/src/Plugin/ActionLink/AJAXactionLink.php
+++ b/src/Plugin/ActionLink/AJAXactionLink.php
@@ -25,8 +25,10 @@ class AJAXactionLink extends Reload {
    */
   public function buildLink($action, FlagInterface $flag, EntityInterface $entity) {
     $render = parent::buildLink($action, $flag, $entity);
-    $render['#attached']['library'][] = 'core/drupal.ajax';
-    $render['#attributes']['class'][] = 'use-ajax';
+    // Redirect to theme with additional message flashing functionality.
+    $render['#theme'] = 'flag.ajax';
+    $render['#entity_id'] = $entity->id();
+
     return $render;
   }
 
diff --git a/templates/flag.ajax.html.twig b/templates/flag.ajax.html.twig
new file mode 100644
index 0000000..4b7ac26
--- /dev/null
+++ b/templates/flag.ajax.html.twig
@@ -0,0 +1,57 @@
+{#
+/**
+ * @file
+ * Default theme implementation for ajax flag links.
+ *
+ * Available variables:
+ * - attributes: HTML attributes for the link element.
+ * - title: The flag link title.
+ * - action: 'flag' or 'unflag'
+ * - flag: The flag object.
+ * - flaggable: The flaggable entity.
+ * - entity_id: The entities id.
+ */
+#}
+{% spaceless %}
+  {# Attach the flag CSS library.#}
+  {{ attach_library('core/drupal.ajax') }}
+  {{ attach_library('flag/flag.link.ajax') }}
+
+  {# Depending on the flag action, set the appropriate action class/message. #}
+  {% if action == 'unflag' %}
+    {% set action_class = 'action-unflag' %}
+    {% set message_text = flag.getUnflagMessage() %}
+  {% else %}
+    {% set action_class = 'action-flag' %}
+    {% set message_status = 'flag-' ~ '' ~ '-message' %}
+    {% set message_text = flag.getFlagMessage() %}
+  {% endif %}
+
+  {# Set the remaining Flag CSS classes. #}
+  {% set classes = [
+    'use-ajax',
+    'flag',
+    'flag-' ~ flag.id,
+    'flag-' ~ flag.id ~ '-' ~ flaggable.id,
+    action_class
+    ]
+  %}
+
+  {% set attributes = attributes.addClass(classes) %}
+
+  {# Convert name to one that is safe to use in a css attribute #}
+  {% set flag_name_css = flag.label|replace({'_': '-'}) %}
+
+  {% set flag_wrapper_classes = [
+    'flag-wrapper',
+    'flag-' ~ flag_name_css,
+    'flag-' ~ flag_name_css ~ '-' ~ entity_id,
+    ]
+  %}
+
+  <span class="{{ flag_wrapper_classes|join(' ')}}}">
+    <a{{ attributes }}>{{ title }}</a>
+    <span></span>
+    <span class="flag-message">{{ message_text }}</span>
+  </span>
+{% endspaceless %}
\ No newline at end of file
