diff --git a/core/core.services.yml b/core/core.services.yml
index 183f7e2..1c31e28 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -560,8 +560,8 @@ services:
     tags:
       - { name: event_subscriber }
     arguments: ['@settings']
-  ajax_response_subscriber:
-    class: Drupal\Core\EventSubscriber\AjaxResponseSubscriber
+  ajax_subscriber:
+    class: Drupal\Core\EventSubscriber\AjaxSubscriber
     tags:
       - { name: event_subscriber }
   route_enhancer.param_conversion:
diff --git a/core/includes/common.inc b/core/includes/common.inc
index b9f2d8f..5f8f1b8 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -13,6 +13,7 @@
 use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
 use Drupal\Component\Utility\Bytes;
 use Drupal\Component\Utility\Crypt;
+use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\Number;
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Component\Utility\SortArray;
@@ -1364,25 +1365,12 @@ function drupal_clear_css_cache() {
  *
  * @return
  *   The cleaned identifier.
+ *
+ * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.0.
+ *   Use \Drupal\Component\Utility\Html::cleanCssIdentifier()
  */
 function drupal_clean_css_identifier($identifier, $filter = array(' ' => '-', '_' => '-', '__' => '__', '/' => '-', '[' => '-', ']' => '')) {
-  // By default, we filter using Drupal's coding standards.
-  $identifier = strtr($identifier, $filter);
-
-  // Valid characters in a CSS identifier are:
-  // - the hyphen (U+002D)
-  // - a-z (U+0030 - U+0039)
-  // - A-Z (U+0041 - U+005A)
-  // - the underscore (U+005F)
-  // - 0-9 (U+0061 - U+007A)
-  // - ISO 10646 characters U+00A1 and higher
-  // We strip out any character not in the above list.
-  $identifier = preg_replace('/[^\x{002D}\x{0030}-\x{0039}\x{0041}-\x{005A}\x{005F}\x{0061}-\x{007A}\x{00A1}-\x{FFFF}]/u', '', $identifier);
-
-  // Identifiers cannot start with a digit, two hyphens, or a hyphen followed by a digit.
-  $identifier = preg_replace(array('/^[0-9]/', '/^(-[0-9])|^(--)/'), array('_', '__') , $identifier);
-
-  return $identifier;
+  return Html::cleanCssIdentifier($identifier, $filter);
 }
 
 /**
@@ -1396,16 +1384,12 @@ function drupal_clean_css_identifier($identifier, $filter = array(' ' => '-', '_
  *
  * @return
  *   The cleaned class name.
+ *
+ * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.0.
+ *   Use \Drupal\Component\Utility\Html::getClass()
  */
 function drupal_html_class($class) {
-  // The output of this function will never change, so this uses a normal
-  // static instead of drupal_static().
-  static $classes = array();
-
-  if (!isset($classes[$class])) {
-    $classes[$class] = drupal_clean_css_identifier(drupal_strtolower($class));
-  }
-  return $classes[$class];
+  return Html::getClass($class);
 }
 
 /**
@@ -1434,66 +1418,12 @@ function drupal_html_class($class) {
  *
  * @return
  *   The cleaned ID.
+ *
+ * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.0.
+ *   Use \Drupal\Component\Utility\Html::getUniqueId()
  */
 function drupal_html_id($id) {
-  // If this is an Ajax request, then content returned by this page request will
-  // be merged with content already on the base page. The HTML IDs must be
-  // unique for the fully merged content. Therefore, initialize $seen_ids to
-  // take into account IDs that are already in use on the base page.
-  $seen_ids_init = &drupal_static(__FUNCTION__ . ':init');
-  if (!isset($seen_ids_init)) {
-    $ajax_html_ids = \Drupal::request()->request->get('ajax_html_ids');
-    // Ideally, Drupal would provide an API to persist state information about
-    // prior page requests in the database, and we'd be able to add this
-    // function's $seen_ids static variable to that state information in order
-    // to have it properly initialized for this page request. However, no such
-    // page state API exists, so instead, ajax.js adds all of the in-use HTML
-    // IDs to the POST data of Ajax submissions. Direct use of $_POST is
-    // normally not recommended as it could open up security risks, but because
-    // the raw POST data is cast to a number before being returned by this
-    // function, this usage is safe.
-    if (empty($ajax_html_ids)) {
-      $seen_ids_init = array();
-    }
-    else {
-      // This function ensures uniqueness by appending a counter to the base id
-      // requested by the calling function after the first occurrence of that
-      // requested id. $_POST['ajax_html_ids'] contains the ids as they were
-      // returned by this function, potentially with the appended counter, so
-      // we parse that to reconstruct the $seen_ids array.
-      $ajax_html_ids = explode(' ', $ajax_html_ids);
-      foreach ($ajax_html_ids as $seen_id) {
-        // We rely on '--' being used solely for separating a base id from the
-        // counter, which this function ensures when returning an id.
-        $parts = explode('--', $seen_id, 2);
-        if (!empty($parts[1]) && is_numeric($parts[1])) {
-          list($seen_id, $i) = $parts;
-        }
-        else {
-          $i = 1;
-        }
-        if (!isset($seen_ids_init[$seen_id]) || ($i > $seen_ids_init[$seen_id])) {
-          $seen_ids_init[$seen_id] = $i;
-        }
-      }
-    }
-  }
-  $seen_ids = &drupal_static(__FUNCTION__, $seen_ids_init);
-
-  $id = drupal_clean_id_identifier($id);
-  // Ensure IDs are unique by appending a counter after the first occurrence.
-  // The counter needs to be appended with a delimiter that does not exist in
-  // the base ID. Requiring a unique delimiter helps ensure that we really do
-  // return unique IDs and also helps us re-create the $seen_ids array during
-  // Ajax requests.
-  if (isset($seen_ids[$id])) {
-    $id = $id . '--' . ++$seen_ids[$id];
-  }
-  else {
-    $seen_ids[$id] = 1;
-  }
-
-  return $id;
+  return Html::getUniqueId($id);
 }
 
 /**
@@ -1509,21 +1439,12 @@ function drupal_html_id($id) {
  *   The cleaned ID.
  *
  * @see drupal_html_id()
+ *
+ * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.0.
+ *   Use \Drupal\Component\Utility\Html::getId()
  */
 function drupal_clean_id_identifier($id) {
-  $id = strtr(drupal_strtolower($id), array(' ' => '-', '_' => '-', '[' => '-', ']' => ''));
-
-  // As defined in http://www.w3.org/TR/html4/types.html#type-name, HTML IDs can
-  // only contain letters, digits ([0-9]), hyphens ("-"), underscores ("_"),
-  // colons (":"), and periods ("."). We strip out any character not in that
-  // list. Note that the CSS spec doesn't allow colons or periods in identifiers
-  // (http://www.w3.org/TR/CSS21/syndata.html#characters), so we strip those two
-  // characters as well.
-  $id = preg_replace('/[^A-Za-z0-9\-_]/', '', $id);
-
-  // Removing multiple consecutive hyphens.
-  $id = preg_replace('/\-+/', '-', $id);
-  return $id;
+  return Html::getId($id);
 }
 
 /**
diff --git a/core/lib/Drupal/Component/Utility/Html.php b/core/lib/Drupal/Component/Utility/Html.php
index 2b5455f..4dd9d27 100644
--- a/core/lib/Drupal/Component/Utility/Html.php
+++ b/core/lib/Drupal/Component/Utility/Html.php
@@ -15,6 +15,229 @@
 class Html {
 
   /**
+   * An array of previously cleaned HTML classes.
+   *
+   * @var array
+   */
+  protected static $classes = array();
+
+  /**
+   * An array of the initial IDs used in one request.
+   *
+   * @var array
+   */
+  protected static $seenIdsInit;
+
+  /**
+   * An array of IDs, including incremented versions when an ID is duplicated.
+   * @var array
+   */
+  protected static $seenIds;
+
+  /**
+   * Contains the current AJAX HTML IDs.
+   *
+   * @var string
+   */
+  protected static $ajaxHTMLIDs;
+
+  /**
+   * Prepares a string for use as a valid class name.
+   *
+   * Do not pass one string containing multiple classes as they will be
+   * incorrectly concatenated with dashes, i.e. "one two" will become "one-two".
+   *
+   * @param string $class
+   *   The class name to clean.
+   *
+   * @return string
+   *   The cleaned class name.
+   */
+  public static function getClass($class) {
+    if (!isset(static::$classes[$class])) {
+      static::$classes[$class] = static::cleanCssIdentifier(Unicode::strtolower($class));
+    }
+    return static::$classes[$class];
+  }
+
+  /**
+   * Prepares a string for use as a CSS identifier (element, class, or ID name).
+   *
+   * http://www.w3.org/TR/CSS21/syndata.html#characters shows the syntax for
+   * valid CSS identifiers (including element names, classes, and IDs in
+   * selectors.)
+   *
+   * @param string $identifier
+   *   The identifier to clean.
+   * @param array $filter
+   *   An array of string replacements to use on the identifier.
+   *
+   * @return string
+   *   The cleaned identifier.
+   */
+  public static function cleanCssIdentifier($identifier, array $filter = array(
+    ' ' => '-',
+    '_' => '-',
+    '__' => '__',
+    '/' => '-',
+    '[' => '-',
+    ']' => ''
+  )) {
+    $identifier = strtr($identifier, $filter);
+    // Valid characters in a CSS identifier are:
+    // - the hyphen (U+002D)
+    // - a-z (U+0030 - U+0039)
+    // - A-Z (U+0041 - U+005A)
+    // - the underscore (U+005F)
+    // - 0-9 (U+0061 - U+007A)
+    // - ISO 10646 characters U+00A1 and higher
+    // We strip out any character not in the above list.
+    $identifier = preg_replace('/[^\x{002D}\x{0030}-\x{0039}\x{0041}-\x{005A}\x{005F}\x{0061}-\x{007A}\x{00A1}-\x{FFFF}]/u', '', $identifier);
+    // Identifiers cannot start with a digit, two hyphens, or a hyphen followed by a digit.
+    $identifier = preg_replace(array(
+      '/^[0-9]/',
+      '/^(-[0-9])|^(--)/'
+    ), array('_', '__'), $identifier);
+    return $identifier;
+  }
+
+  /**
+   * Sets the AJAX HTML IDs.
+   *
+   * @param string $ajax_html_ids
+   *   The AJAX HTML IDs, probably coming from the current request.
+   */
+  public static function setAjaxHtmlIds($ajax_html_ids = '') {
+    static::$ajaxHTMLIDs = $ajax_html_ids;
+  }
+
+  /**
+   * Prepares a string for use as a valid HTML ID and guarantees uniqueness.
+   *
+   * This function ensures that each passed HTML ID value only exists once on
+   * the page. By tracking the already returned ids, this function enables
+   * forms, blocks, and other content to be output multiple times on the same
+   * page, without breaking (X)HTML validation.
+   *
+   * For already existing IDs, a counter is appended to the ID string.
+   * Therefore, JavaScript and CSS code should not rely on any value that was
+   * generated by this function and instead should rely on manually added CSS
+   * classes or similarly reliable constructs.
+   *
+   * Two consecutive hyphens separate the counter from the original ID. To
+   * manage uniqueness across multiple Ajax requests on the same page, Ajax
+   * requests POST an array of all IDs currently present on the page, which are
+   * used to prime this function's cache upon first invocation.
+   *
+   * To allow reverse-parsing of IDs submitted via Ajax, any multiple
+   * consecutive hyphens in the originally passed $id are replaced with a
+   * single hyphen.
+   *
+   * @param string $id
+   *   The ID to clean.
+   *
+   * @return string
+   *   The cleaned ID.
+   */
+  public static function getUniqueId($id) {
+    // If this is an Ajax request, then content returned by this page request
+    // will be merged with content already on the base page. The HTML IDs must
+    // be unique for the fully merged content. Therefore, initialize $seen_ids
+    // to take into account IDs that are already in use on the base page.
+    if (!isset(static::$seenIdsInit)) {
+      // Ideally, Drupal would provide an API to persist state information about
+      // prior page requests in the database, and we'd be able to add this
+      // function's $seen_ids static variable to that state information in order
+      // to have it properly initialized for this page request. However, no such
+      // page state API exists, so instead, ajax.js adds all of the in-use HTML
+      // IDs to the POST data of Ajax submissions. Direct use of $_POST is
+      // normally not recommended as it could open up security risks, but
+      // because the raw POST data is cast to a number before being returned by
+      // this function, this usage is safe.
+      if (empty(static::$ajaxHTMLIDs)) {
+        static::$seenIdsInit = array();
+      }
+      else {
+        // This function ensures uniqueness by appending a counter to the base
+        // id requested by the calling function after the first occurrence of
+        // that requested id. $_POST['ajax_html_ids'] contains the ids as they
+        // were returned by this function, potentially with the appended
+        // counter, so we parse that to reconstruct the $seen_ids array.
+        $ajax_html_ids = explode(' ', static::$ajaxHTMLIDs);
+        foreach ($ajax_html_ids as $seen_id) {
+          // We rely on '--' being used solely for separating a base id from the
+          // counter, which this function ensures when returning an id.
+          $parts = explode('--', $seen_id, 2);
+          if (!empty($parts[1]) && is_numeric($parts[1])) {
+            list($seen_id, $i) = $parts;
+          }
+          else {
+            $i = 1;
+          }
+          if (!isset(static::$seenIdsInit[$seen_id]) || ($i > static::$seenIdsInit[$seen_id])) {
+            static::$seenIdsInit[$seen_id] = $i;
+          }
+        }
+      }
+    }
+    if (!isset(static::$seenIds)) {
+      static::$seenIds = static::$seenIdsInit;
+    }
+
+    $id = static::getId($id);
+
+    // Ensure IDs are unique by appending a counter after the first occurrence.
+    // The counter needs to be appended with a delimiter that does not exist in
+    // the base ID. Requiring a unique delimiter helps ensure that we really do
+    // return unique IDs and also helps us re-create the $seen_ids array during
+    // Ajax requests.
+    if (isset(static::$seenIds[$id])) {
+      $id = $id . '--' . ++static::$seenIds[$id];
+    }
+    else {
+      static::$seenIds[$id] = 1;
+    }
+    return $id;
+  }
+
+  /**
+   * Prepares a string for use as a valid HTML ID.
+   *
+   * Only use this function when you want to intentionally skip the uniqueness
+   * guarantee of self::getUniqueId().
+   *
+   * @param string $id
+   *   The ID to clean.
+   *
+   * @return string
+   *   The cleaned ID.
+   *
+   * @see self::getUniqueId()
+   */
+  public static function getId($id) {
+    $id = strtr(Unicode::strtolower($id), array(' ' => '-', '_' => '-', '[' => '-', ']' => ''));
+
+    // As defined in http://www.w3.org/TR/html4/types.html#type-name, HTML IDs can
+    // only contain letters, digits ([0-9]), hyphens ("-"), underscores ("_"),
+    // colons (":"), and periods ("."). We strip out any character not in that
+    // list. Note that the CSS spec doesn't allow colons or periods in identifiers
+    // (http://www.w3.org/TR/CSS21/syndata.html#characters), so we strip those two
+    // characters as well.
+    $id = preg_replace('/[^A-Za-z0-9\-_]/', '', $id);
+
+    // Removing multiple consecutive hyphens.
+    $id = preg_replace('/\-+/', '-', $id);
+    return $id;
+  }
+
+  /**
+   * Resets the list of seen IDs.
+   */
+  public static function resetSeenIds() {
+    static::$seenIds = NULL;
+  }
+
+  /**
    * Normalizes an HTML snippet.
    *
    * This function is essentially \DOMDocument::normalizeDocument(), but
diff --git a/core/lib/Drupal/Core/EventSubscriber/AjaxResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/AjaxSubscriber.php
similarity index 56%
rename from core/lib/Drupal/Core/EventSubscriber/AjaxResponseSubscriber.php
rename to core/lib/Drupal/Core/EventSubscriber/AjaxSubscriber.php
index cf4dca1..cc260b6 100644
--- a/core/lib/Drupal/Core/EventSubscriber/AjaxResponseSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/AjaxSubscriber.php
@@ -2,18 +2,32 @@
 
 /**
  * @file
- * Contains \Drupal\Core\EventSubscriber\AjaxResponseSubscriber.
+ * Contains \Drupal\Core\EventSubscriber\AjaxSubscriber.
  */
 
 namespace Drupal\Core\EventSubscriber;
 
+use Drupal\Component\Utility\Html;
 use Drupal\Core\Ajax\AjaxResponse;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
-use Symfony\Component\HttpKernel\HttpKernelInterface;
+use Symfony\Component\HttpKernel\Event\GetResponseEvent;
 use Symfony\Component\HttpKernel\KernelEvents;
 
-class AjaxResponseSubscriber implements EventSubscriberInterface {
+/**
+ * Subscribes to set AJAX HTML IDs and prepare AJAX responses.
+ */
+class AjaxSubscriber implements EventSubscriberInterface {
+
+  /**
+   * Sets the AJAX HTML IDs from the current request.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
+   *   The response event, which contains the current request.
+   */
+  public function onRequest(GetResponseEvent $event) {
+    Html::setAjaxHtmlIds($event->getRequest()->request->get('ajax_html_ids', ''));
+  }
 
   /**
    * Renders the ajax commands right before preparing the result.
@@ -33,6 +47,7 @@ public function onResponse(FilterResponseEvent $event) {
    */
   public static function getSubscribedEvents() {
     $events[KernelEvents::RESPONSE][] = array('onResponse', -100);
+    $events[KernelEvents::REQUEST][] = array('onREquest', 50);
 
     return $events;
   }
diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index bf9aa88..e1bcbe3 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -8,6 +8,7 @@
 namespace Drupal\Core\Form;
 
 use Drupal\Component\Utility\Crypt;
+use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Component\Utility\String;
@@ -434,10 +435,10 @@ public function retrieveForm($form_id, FormStateInterface &$form_state) {
     // Assign a default CSS class name based on $form_id.
     // This happens here and not in self::prepareForm() in order to allow the
     // form constructor function to override or remove the default class.
-    $form['#attributes']['class'][] = $this->drupalHtmlClass($form_id);
+    $form['#attributes']['class'][] = Html::getClass($form_id);
     // Same for the base form ID, if any.
     if (isset($form_state['build_info']['base_form_id'])) {
-      $form['#attributes']['class'][] = $this->drupalHtmlClass($form_state['build_info']['base_form_id']);
+      $form['#attributes']['class'][] = Html::getClass($form_state['build_info']['base_form_id']);
     }
 
     // We need to pass $form_state by reference in order for forms to modify it,
@@ -509,7 +510,7 @@ public function processForm($form_id, &$form, FormStateInterface &$form_state) {
       // element IDs needlessly.
       if (!FormState::hasAnyErrors()) {
         // In case of errors, do not break HTML IDs of other forms.
-        $this->drupalStaticReset('drupal_html_id');
+        Html::resetSeenIds();
       }
 
       if (!$form_state['rebuild'] && !FormState::hasAnyErrors()) {
@@ -611,7 +612,7 @@ public function prepareForm($form_id, &$form, FormStateInterface &$form_state) {
       else {
         $form['#token'] = $form_id;
         $form['form_token'] = array(
-          '#id' => $this->drupalHtmlId('edit-' . $form_id . '-form-token'),
+          '#id' => Html::getUniqueId('edit-' . $form_id . '-form-token'),
           '#type' => 'token',
           '#default_value' => $this->csrfToken->get($form['#token']),
           // Form processing and validation requires this value, so ensure the
@@ -626,7 +627,7 @@ public function prepareForm($form_id, &$form, FormStateInterface &$form_state) {
       $form['form_id'] = array(
         '#type' => 'hidden',
         '#value' => $form_id,
-        '#id' => $this->drupalHtmlId("edit-$form_id"),
+        '#id' => Html::getUniqueId("edit-$form_id"),
         // Form processing and validation requires this value, so ensure the
         // submitted form value appears literally, regardless of custom #tree
         // and #parents being set elsewhere.
@@ -634,7 +635,7 @@ public function prepareForm($form_id, &$form, FormStateInterface &$form_state) {
       );
     }
     if (!isset($form['#id'])) {
-      $form['#id'] = $this->drupalHtmlId($form_id);
+      $form['#id'] = Html::getUniqueId($form_id);
     }
 
     $form += $this->getElementInfo('form');
@@ -752,7 +753,7 @@ public function doBuildForm($form_id, &$element, FormStateInterface &$form_state
     }
 
     if (!isset($element['#id'])) {
-      $element['#id'] = $this->drupalHtmlId('edit-' . implode('-', $element['#parents']));
+      $element['#id'] = Html::getUniqueId('edit-' . implode('-', $element['#parents']));
     }
 
     // Add the aria-describedby attribute to associate the form control with its
@@ -1135,31 +1136,6 @@ protected function getElementInfo($type) {
   }
 
   /**
-   * Wraps drupal_html_class().
-   *
-   * @return string
-   */
-  protected function drupalHtmlClass($class) {
-    return drupal_html_class($class);
-  }
-
-  /**
-   * Wraps drupal_html_id().
-   *
-   * @return string
-   */
-  protected function drupalHtmlId($id) {
-    return drupal_html_id($id);
-  }
-
-  /**
-   * Wraps drupal_static_reset().
-   */
-  protected function drupalStaticReset($name = NULL) {
-    drupal_static_reset($name);
-  }
-
-  /**
    * Gets the current active user.
    *
    * @return \Drupal\Core\Session\AccountInterface
diff --git a/core/modules/block/src/Tests/BlockViewBuilderTest.php b/core/modules/block/src/Tests/BlockViewBuilderTest.php
index be26a86..58becb0 100644
--- a/core/modules/block/src/Tests/BlockViewBuilderTest.php
+++ b/core/modules/block/src/Tests/BlockViewBuilderTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\block\Tests;
 
+use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Cache\UrlCacheContext;
 use Drupal\simpletest\DrupalUnitTestBase;
@@ -94,7 +95,7 @@ public function testBasicRendering() {
     $this->assertEqual(drupal_render($output), $expected_output);
 
     // Reset the HTML IDs so that the next render is not affected.
-    drupal_static_reset('drupal_html_id');
+    Html::resetSeenIds();
 
     // Test the rendering of a block with a given title.
     $entity = $this->controller->create(array(
diff --git a/core/modules/system/src/Tests/Common/HtmlIdentifierUnitTest.php b/core/modules/system/src/Tests/Common/HtmlIdentifierUnitTest.php
deleted file mode 100644
index 74775ea..0000000
--- a/core/modules/system/src/Tests/Common/HtmlIdentifierUnitTest.php
+++ /dev/null
@@ -1,79 +0,0 @@
-<?php
-
-/**
- * @file
- * Definition of Drupal\system\Tests\Common\HtmlIdentifierUnitTest.
- */
-
-namespace Drupal\system\Tests\Common;
-
-use Drupal\simpletest\KernelTestBase;
-
-/**
- * Tests the functions drupal_html_class(), drupal_html_id() and
- * drupal_clean_css_identifier() for expected behavior.
- *
- * @group Common
- */
-class HtmlIdentifierUnitTest extends KernelTestBase {
-  /**
-   * Tests that drupal_clean_css_identifier() cleans the identifier properly.
-   */
-  function testDrupalCleanCSSIdentifier() {
-    // Verify that no valid ASCII characters are stripped from the identifier.
-    $identifier = 'abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789';
-    $this->assertIdentical(drupal_clean_css_identifier($identifier, array()), $identifier, 'Verify valid ASCII characters pass through.');
-
-    // Verify that valid UTF-8 characters are not stripped from the identifier.
-    $identifier = '¡¢£¤¥';
-    $this->assertIdentical(drupal_clean_css_identifier($identifier, array()), $identifier, 'Verify valid UTF-8 characters pass through.');
-
-    // Verify that invalid characters (including non-breaking space) are stripped from the identifier.
-    $this->assertIdentical(drupal_clean_css_identifier('invalid !"#$%&\'()*+,./:;<=>?@[\\]^`{|}~ identifier', array()), 'invalididentifier', 'Strip invalid characters.');
-
-    // Verify that double underscores are not stripped from the identifier.
-    $identifier = 'css__identifier__with__double__underscores';
-    $this->assertIdentical(drupal_clean_css_identifier($identifier), $identifier, 'Verify double underscores pass through.');
-
-    // Verify that an identifier starting with a digit is replaced.
-    $this->assertIdentical(drupal_clean_css_identifier('1cssidentifier', array()), '_cssidentifier', 'Verify identifier starting with a digit is replaced.');
-
-    // Verify that an identifier starting with a hyphen followed by a digit is
-    // replaced.
-    $this->assertIdentical(drupal_clean_css_identifier('-1cssidentifier', array()), '__cssidentifier', 'Verify identifier starting with a hyphen followed by a digit is replaced.');
-
-    // Verify that an identifier starting with two hyphens is replaced.
-    $this->assertIdentical(drupal_clean_css_identifier('--cssidentifier', array()), '__cssidentifier', 'Verify identifier starting with two hyphens is replaced.');
-  }
-
-  /**
-   * Tests that drupal_html_class() cleans the class name properly.
-   */
-  function testDrupalHTMLClass() {
-    // Verify Drupal coding standards are enforced.
-    $this->assertIdentical(drupal_html_class('CLASS NAME_[Ü]'), 'class-name--ü', 'Enforce Drupal coding standards.');
-  }
-
-  /**
-   * Tests that drupal_html_id() cleans the ID properly.
-   */
-  function testDrupalHTMLId() {
-    // Verify that letters, digits, and hyphens are not stripped from the ID.
-    $id = 'abcdefghijklmnopqrstuvwxyz-0123456789';
-    $this->assertIdentical(drupal_html_id($id), $id, 'Verify valid characters pass through.');
-
-    // Verify that invalid characters are stripped from the ID.
-    $this->assertIdentical(drupal_html_id('invalid,./:@\\^`{Üidentifier'), 'invalididentifier', 'Strip invalid characters.');
-
-    // Verify Drupal coding standards are enforced.
-    $this->assertIdentical(drupal_html_id('ID NAME_[1]'), 'id-name-1', 'Enforce Drupal coding standards.');
-
-    // Reset the static cache so we can ensure the unique id count is at zero.
-    drupal_static_reset('drupal_html_id');
-
-    // Clean up IDs with invalid starting characters.
-    $this->assertIdentical(drupal_html_id('test-unique-id'), 'test-unique-id', 'Test the uniqueness of IDs #1.');
-    $this->assertIdentical(drupal_html_id('test-unique-id'), 'test-unique-id--2', 'Test the uniqueness of IDs #2.');
-    $this->assertIdentical(drupal_html_id('test-unique-id'), 'test-unique-id--3', 'Test the uniqueness of IDs #3.');
-  }
-}
diff --git a/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php b/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php
index 8275a48..15e6ee2 100644
--- a/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php
+++ b/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\views_ui\Form\Ajax;
 
+use Drupal\Component\Utility\Html;
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormState;
 use Drupal\Core\Form\FormStateInterface;
@@ -96,8 +97,7 @@ public function getForm(ViewStorageInterface $view, $display_id, $js) {
     // Reset the cache of IDs. Drupal rather aggressively prevents ID
     // duplication but this causes it to remember IDs that are no longer even
     // being used.
-    $seen_ids_init = &drupal_static('drupal_html_id:init');
-    $seen_ids_init = array();
+    Html::resetSeenIds();
 
     // check to see if this is the top form of the stack. If it is, pop
     // it off; if it isn't, the user clicked somewhere else and the stack is
diff --git a/core/modules/views_ui/src/Tests/RowUITest.php b/core/modules/views_ui/src/Tests/RowUITest.php
index 08326a5..67e415d 100644
--- a/core/modules/views_ui/src/Tests/RowUITest.php
+++ b/core/modules/views_ui/src/Tests/RowUITest.php
@@ -61,7 +61,7 @@ public function testRowUI() {
 
     // Change the row plugin to fields using ajax.
     $this->drupalPostAjaxForm($row_plugin_url, array('row[type]' => 'fields'), array('op' => 'Apply'), str_replace('/nojs/', '/ajax/', $row_plugin_url));
-    $this->drupalPostAjaxForm(NULL, array(), array('op' => 'Apply'));
+    $this->drupalPostAjaxForm(NULL, array(), array('op' => 'Apply'), str_replace('/nojs/', '/ajax/', $row_plugin_url));
     $this->assertResponse(200);
     $this->assertFieldByName('row[type]', 'fields', 'Make sure that the fields got saved as used row plugin.');
   }
diff --git a/core/modules/views_ui/src/ViewUI.php b/core/modules/views_ui/src/ViewUI.php
index 64f8cae..588f379 100644
--- a/core/modules/views_ui/src/ViewUI.php
+++ b/core/modules/views_ui/src/ViewUI.php
@@ -8,6 +8,7 @@
 namespace Drupal\views_ui;
 
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\Timer;
 use Drupal\Component\Utility\Xss;
 use Drupal\Core\Form\FormStateInterface;
@@ -433,8 +434,7 @@ public function addFormToStack($key, $display_id, $type, $id = NULL, $top = FALS
     // Reset the cache of IDs. Drupal rather aggressively prevents ID
     // duplication but this causes it to remember IDs that are no longer even
     // being used.
-    $seen_ids_init = &drupal_static('drupal_html_id:init');
-    $seen_ids_init = array();
+    Html::resetSeenIds();
 
     if (empty($this->stack)) {
       $this->stack = array();
diff --git a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
index 22e0d6f..a953c83 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
@@ -205,7 +205,7 @@ public function testGetFormWithString() {
 
     $form = $this->formBuilder->getForm($form_id);
     $this->assertFormElement($expected_form, $form, 'test');
-    $this->assertSame($form_id, $form['#id']);
+    $this->assertSame('test-form-id', $form['#id']);
   }
 
   /**
@@ -219,7 +219,7 @@ public function testGetFormWithObject() {
 
     $form = $this->formBuilder->getForm($form_arg);
     $this->assertFormElement($expected_form, $form, 'test');
-    $this->assertSame($form_id, $form['#id']);
+    $this->assertArrayHasKey('#id', $form);
   }
 
   /**
@@ -234,7 +234,7 @@ public function testGetFormWithClassString() {
 
     $form = $this->formBuilder->getForm($form_id);
     $this->assertFormElement($expected_form, $form, 'test');
-    $this->assertSame('test_form', $form['#id']);
+    $this->assertSame('test-form', $form['#id']);
   }
 
   /**
@@ -249,7 +249,7 @@ public function testBuildFormWithString() {
 
     $form = $this->formBuilder->getForm($form_id);
     $this->assertFormElement($expected_form, $form, 'test');
-    $this->assertSame($form_id, $form['#id']);
+    $this->assertArrayHasKey('#id', $form);
   }
 
   /**
@@ -264,7 +264,7 @@ public function testBuildFormWithClassString() {
 
     $form = $this->formBuilder->buildForm($form_id, $form_state);
     $this->assertFormElement($expected_form, $form, 'test');
-    $this->assertSame('test_form', $form['#id']);
+    $this->assertSame('test-form', $form['#id']);
   }
 
   /**
@@ -280,7 +280,7 @@ public function testBuildFormWithObject() {
     $form = $this->formBuilder->buildForm($form_arg, $form_state);
     $this->assertFormElement($expected_form, $form, 'test');
     $this->assertSame($form_id, $form_state['build_info']['form_id']);
-    $this->assertSame($form_id, $form['#id']);
+    $this->assertArrayHasKey('#id', $form);
   }
 
   /**
@@ -423,13 +423,13 @@ public function testUniqueHtmlId() {
       ->setMethods(array('drupalSetMessage'))
       ->getMock();
     $form = $this->simulateFormSubmission($form_id, $form_arg, $form_state);
-    $this->assertSame($form_id, $form['#id']);
+    $this->assertSame('test-form-id', $form['#id']);
 
     $form_state = $this->getMockBuilder('Drupal\Core\Form\FormState')
       ->setMethods(array('drupalSetMessage'))
       ->getMock();
     $form = $this->simulateFormSubmission($form_id, $form_arg, $form_state);
-    $this->assertSame("$form_id--2", $form['#id']);
+    $this->assertSame('test-form-id--2', $form['#id']);
   }
 
 }
diff --git a/core/tests/Drupal/Tests/Core/Form/FormTestBase.php b/core/tests/Drupal/Tests/Core/Form/FormTestBase.php
index 108f35a..5ef25fd 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormTestBase.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormTestBase.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Tests\Core\Form {
 
+use Drupal\Component\Utility\Html;
 use Drupal\Core\Form\FormBuilder;
 use Drupal\Core\Form\FormInterface;
 use Drupal\Core\Form\FormStateInterface;
@@ -193,7 +194,7 @@ protected function setUp() {
    * {@inheritdoc}
    */
   protected function tearDown() {
-    $this->formBuilder->drupalStaticReset();
+    Html::resetSeenIds();
   }
 
   /**
diff --git a/core/tests/Drupal/Tests/Core/Utility/HtmlTest.php b/core/tests/Drupal/Tests/Core/Utility/HtmlTest.php
new file mode 100644
index 0000000..cd78d20
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Utility/HtmlTest.php
@@ -0,0 +1,213 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Utility\HtmlTest.
+ */
+
+namespace Drupal\Tests\Core\Utility;
+
+use Drupal\Component\Utility\Html;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests \Drupal\Component\Utility\Html.
+ *
+ * @group Common
+ *
+ * @coversDefaultClass \Drupal\Component\Utility\Html
+ */
+class HtmlTest extends UnitTestCase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $property = new \ReflectionProperty('Drupal\Component\Utility\Html', 'seenIdsInit');
+    $property->setAccessible(TRUE);
+    $property->setValue(NULL);
+  }
+
+  /**
+   * Tests the Html::cleanCssIdentifier() method.
+   *
+   * @param string $expected
+   *   The expected result.
+   * @param string $source
+   *   The string being transformed to an ID.
+   * @param array|null $filter
+   *   (optional) An array of string replacements to use on the identifier. If
+   *   NULL, no filter will be passed and a default will be used.
+   *
+   * @dataProvider providerTestCleanCssIdentifier
+   *
+   * @covers ::cleanCssIdentifier
+   */
+  public function testCleanCssIdentifier($expected, $source, $filter = NULL) {
+    if ($filter !== NULL) {
+      $this->assertSame($expected, Html::cleanCssIdentifier($source, $filter));
+    }
+    else {
+      $this->assertSame($expected, Html::cleanCssIdentifier($source));
+    }
+  }
+
+  /**
+   * Provides test data for testCleanCssIdentifier().
+   *
+   * @return array
+   *   Test data.
+   */
+  public function providerTestCleanCssIdentifier() {
+    $id1 = 'abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789';
+    $id2 = '¡¢£¤¥';
+    $id3 = 'css__identifier__with__double__underscores';
+    return array(
+      // Verify that no valid ASCII characters are stripped from the identifier.
+      array($id1, $id1, array()),
+      // Verify that valid UTF-8 characters are not stripped from the identifier.
+      array($id2, $id2, array()),
+      // Verify that invalid characters (including non-breaking space) are stripped from the identifier.
+      array($id3, $id3),
+      // Verify that double underscores are not stripped from the identifier.
+      array('invalididentifier', 'invalid !"#$%&\'()*+,./:;<=>?@[\\]^`{|}~ identifier', array()),
+      // Verify that an identifier starting with a digit is replaced.
+      array('_cssidentifier', '1cssidentifier', array()),
+      // Verify that an identifier starting with a hyphen followed by a digit is
+      // replaced.
+      array('__cssidentifier', '-1cssidentifier', array()),
+      // Verify that an identifier starting with two hyphens is replaced.
+      array('__cssidentifier', '--cssidentifier', array())
+    );
+  }
+
+  /**
+   * Tests that Html::getClass() cleans the class name properly.
+   *
+   * @coversDefaultClass ::getClass
+   */
+  public function testHtmlClass() {
+    // Verify Drupal coding standards are enforced.
+    $this->assertSame(Html::getClass('CLASS NAME_[Ü]'), 'class-name--ü', 'Enforce Drupal coding standards.');
+  }
+
+  /**
+   * Tests the Html::getUniqueId() method.
+   *
+   * @param string $expected
+   *   The expected result.
+   * @param string $source
+   *   The string being transformed to an ID.
+   * @param bool $reset
+   *   (optional) If TRUE, reset the list of seen IDs. Defaults to FALSE.
+   *
+   * @dataProvider providerTestHtmlGetUniqueId
+   *
+   * @covers ::getUniqueId
+   */
+  public function testHtmlGetUniqueId($expected, $source, $reset = FALSE) {
+    if ($reset) {
+      Html::resetSeenIds();
+    }
+    $this->assertSame($expected, Html::getUniqueId($source));
+  }
+
+  /**
+   * Provides test data for testHtmlGetId().
+   *
+   * @return array
+   *   Test data.
+   */
+  public function providerTestHtmlGetUniqueId() {
+    $id = 'abcdefghijklmnopqrstuvwxyz-0123456789';
+    return array(
+      // Verify that letters, digits, and hyphens are not stripped from the ID.
+      array($id, $id),
+      // Verify that invalid characters are stripped from the ID.
+      array('invalididentifier', 'invalid,./:@\\^`{Üidentifier'),
+      // Verify Drupal coding standards are enforced.
+      array('id-name-1', 'ID NAME_[1]'),
+      // Verify that a repeated ID is made unique.
+      array('test-unique-id', 'test-unique-id', TRUE),
+      array('test-unique-id--2', 'test-unique-id'),
+      array('test-unique-id--3', 'test-unique-id'),
+    );
+  }
+
+  /**
+   * Tests the Html::getUniqueId() method.
+   *
+   * @param string $expected
+   *   The expected result.
+   * @param string $source
+   *   The string being transformed to an ID.
+   * @param bool $reset
+   *   (optional) If TRUE, reset the list of seen IDs. Defaults to FALSE.
+   *
+   * @dataProvider providerTestHtmlGetUniqueIdWithAjaxIds
+   *
+   * @covers ::getUniqueId
+   */
+  public function testHtmlGetUniqueIdWithAjaxIds($expected, $source, $reset = FALSE) {
+    if ($reset) {
+      Html::resetSeenIds();
+    }
+    Html::setAjaxHtmlIds('test-unique-id1 test-unique-id2--3');
+    $this->assertSame($expected, Html::getUniqueId($source));
+  }
+
+  /**
+   * Provides test data for testHtmlGetId().
+   *
+   * @return array
+   *   Test data.
+   */
+  public function providerTestHtmlGetUniqueIdWithAjaxIds() {
+    return array(
+      array('test-unique-id1--2', 'test-unique-id1', TRUE),
+      array('test-unique-id1--3', 'test-unique-id1'),
+      array('test-unique-id2--4', 'test-unique-id2', TRUE),
+      array('test-unique-id2--5', 'test-unique-id2'),
+    );
+  }
+
+  /**
+   * Tests the Html::getUniqueId() method.
+   *
+   * @param string $expected
+   *   The expected result.
+   * @param string $source
+   *   The string being transformed to an ID.
+   *
+   * @dataProvider providerTestHtmlGetId
+   *
+   * @covers ::getId
+   */
+  public function testHtmlGetId($expected, $source) {
+    $this->assertSame($expected, Html::getId($source));
+  }
+
+  /**
+   * Provides test data for testHtmlGetId().
+   *
+   * @return array
+   *   Test data.
+   */
+  public function providerTestHtmlGetId() {
+    $id = 'abcdefghijklmnopqrstuvwxyz-0123456789';
+    return array(
+      // Verify that letters, digits, and hyphens are not stripped from the ID.
+      array($id, $id),
+      // Verify that invalid characters are stripped from the ID.
+      array('invalididentifier', 'invalid,./:@\\^`{Üidentifier'),
+      // Verify Drupal coding standards are enforced.
+      array('id-name-1', 'ID NAME_[1]'),
+      // Verify that a repeated ID is made unique.
+      array('test-unique-id', 'test-unique-id'),
+      array('test-unique-id', 'test-unique-id'),
+    );
+  }
+
+}
