diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 769e34c..665ca73 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -568,9 +568,8 @@ function template_preprocess_datetime_wrapper(&$variables) {
  *
  * @param array $variables
  *   An associative array containing:
- *   - links: An associative array of links to be themed. The key for each link
- *     is used as its CSS class. Each link should be itself an array, with the
- *     following elements:
+ *   - links: An array of links to be themed. Each link should be itself an
+ *     array, with the following elements:
  *     - title: The link text.
  *     - url: (optional) The url object to link to. If omitted, no a tag is
  *       printed out.
diff --git a/core/modules/system/src/Tests/Theme/FunctionsTest.php b/core/modules/system/src/Tests/Theme/FunctionsTest.php
index 5e31422..e64f10e 100644
--- a/core/modules/system/src/Tests/Theme/FunctionsTest.php
+++ b/core/modules/system/src/Tests/Theme/FunctionsTest.php
@@ -290,6 +290,127 @@ function testLinks() {
   }
 
   /**
+   * Tests links.html.twig.
+   */
+  function testIndexedKeyedLinks() {
+    // Turn off the query for the
+    // \Drupal\Core\Utility\LinkGeneratorInterface::generate() method to compare
+    // the active link correctly.
+    $original_query = \Drupal::request()->query->all();
+    \Drupal::request()->query->replace([]);
+    // Verify that empty variables produce no output.
+    $variables = [];
+    $expected = '';
+    $this->assertThemeOutput('links', $variables, $expected, 'Empty %callback generates no output.');
+
+    $variables = [];
+    $variables['heading'] = 'Some title';
+    $expected = '';
+    $this->assertThemeOutput('links', $variables, $expected, 'Empty %callback with heading generates no output.');
+
+    // Verify that a list of links is properly rendered.
+    $variables = [];
+    $variables['attributes'] = ['id' => 'somelinks'];
+    $variables['links'] = array(
+      array(
+        'title' => 'A <link>',
+        'url' => Url::fromUri('base:a/link'),
+      ),
+      array(
+        'title' => 'Plain "text"',
+      ),
+      array(
+        'title' => SafeMarkup::format('<span class="unescaped">@text</span>', array('@text' => 'potentially unsafe text that <should> be escaped')),
+      ),
+      array(
+        'title' => 'Front page',
+        'url' => Url::fromRoute('<front>'),
+      ),
+      array(
+        'title' => 'Test route',
+        'url' => Url::fromRoute('router_test.1'),
+      ),
+      array(
+        'title' => 'Query test route',
+        'url' => Url::fromRoute('router_test.1'),
+        'query' => array(
+          'key' => 'value',
+        )
+      ),
+    );
+
+    $expected_links = '';
+    $expected_links .= '<ul id="somelinks">';
+    $expected_links .= '<li><a href="' . Url::fromUri('base:a/link')->toString() . '">' . Html::escape('A <link>') . '</a></li>';
+    $expected_links .= '<li>' . Html::escape('Plain "text"') . '</li>';
+    $expected_links .= '<li><span class="unescaped">' . Html::escape('potentially unsafe text that <should> be escaped') . '</span></li>';
+    $expected_links .= '<li><a href="' . Url::fromRoute('<front>')->toString() . '">' . Html::escape('Front page') . '</a></li>';
+    $expected_links .= '<li><a href="' . \Drupal::urlGenerator()->generate('router_test.1') . '">' . Html::escape('Test route') . '</a></li>';
+    $query = ['key' => 'value'];
+    $expected_links .= '<li><a href="' . \Drupal::urlGenerator()->generate('router_test.1', $query) . '">' . Html::escape('Query test route') . '</a></li>';
+    $expected_links .= '</ul>';
+
+    // Verify that passing a string as heading works.
+    $variables['heading'] = 'Links heading';
+    $expected_heading = '<h2>Links heading</h2>';
+    $expected = $expected_heading . $expected_links;
+    $this->assertThemeOutput('links', $variables, $expected);
+
+    // Restore the original request's query.
+    \Drupal::request()->query->replace($original_query);
+
+    // Verify that passing an array as heading works (core support).
+    $variables['heading'] = [
+      'text' => 'Links heading',
+      'level' => 'h3',
+      'attributes' => ['class' => ['heading']],
+    ];
+    $expected_heading = '<h3 class="heading">Links heading</h3>';
+    $expected = $expected_heading . $expected_links;
+    $this->assertThemeOutput('links', $variables, $expected);
+
+    // Verify that passing attributes for the heading works.
+    $variables['heading'] = ['text' => 'Links heading', 'level' => 'h3', 'attributes' => ['id' => 'heading']];
+    $expected_heading = '<h3 id="heading">Links heading</h3>';
+    $expected = $expected_heading . $expected_links;
+    $this->assertThemeOutput('links', $variables, $expected);
+
+    // Verify that passing attributes for the links work.
+    $variables['links'][1]['attributes'] = [
+      'class' => ['a/class'],
+    ];
+    $expected_links = '';
+    $expected_links .= '<ul id="somelinks">';
+    $expected_links .= '<li><a href="' . Url::fromUri('base:a/link')->toString() . '">' . Html::escape('A <link>') . '</a></li>';
+    $expected_links .= '<li><span class="a/class">' . Html::escape('Plain "text"') . '</span></li>';
+    $expected_links .= '<li><span class="unescaped">' . Html::escape('potentially unsafe text that <should> be escaped') . '</span></li>';
+    $expected_links .= '<li><a href="' . Url::fromRoute('<front>')->toString() . '">' . Html::escape('Front page') . '</a></li>';
+    $expected_links .= '<li><a href="' . \Drupal::urlGenerator()->generate('router_test.1') . '">' . Html::escape('Test route') . '</a></li>';
+    $query = ['key' => 'value'];
+    $expected_links .= '<li><a href="' . \Drupal::urlGenerator()->generate('router_test.1', $query) . '">' . Html::escape('Query test route') . '</a></li>';
+    $expected_links .= '</ul>';
+    $expected = $expected_heading . $expected_links;
+    $this->assertThemeOutput('links', $variables, $expected);
+
+    // Verify the data- attributes for setting the "active" class on links.
+    \Drupal::currentUser()->setAccount(new UserSession(array('uid' => 1)));
+    $variables['set_active_class'] = TRUE;
+    $expected_links = '';
+    $expected_links .= '<ul id="somelinks">';
+    $expected_links .= '<li><a href="' . Url::fromUri('base:a/link')->toString() . '">' . Html::escape('A <link>') . '</a></li>';
+    $expected_links .= '<li><span class="a/class">' . Html::escape('Plain "text"') . '</span></li>';
+    $expected_links .= '<li><span class="unescaped">' . Html::escape('potentially unsafe text that <should> be escaped') . '</span></li>';
+    $expected_links .= '<li data-drupal-link-system-path="&lt;front&gt;"><a href="' . Url::fromRoute('<front>')->toString() . '" data-drupal-link-system-path="&lt;front&gt;">' . Html::escape('Front page') . '</a></li>';
+    $expected_links .= '<li data-drupal-link-system-path="router_test/test1"><a href="' . \Drupal::urlGenerator()->generate('router_test.1') . '" data-drupal-link-system-path="router_test/test1">' . Html::escape('Test route') . '</a></li>';
+    $query = ['key' => 'value'];
+    $encoded_query = Html::escape(Json::encode($query));
+    $expected_links .= '<li data-drupal-link-query="'.$encoded_query.'" data-drupal-link-system-path="router_test/test1"><a href="' . \Drupal::urlGenerator()->generate('router_test.1', $query) . '" data-drupal-link-query="'.$encoded_query.'" data-drupal-link-system-path="router_test/test1">' . Html::escape('Query test route') . '</a></li>';
+    $expected_links .= '</ul>';
+    $expected = $expected_heading . $expected_links;
+    $this->assertThemeOutput('links', $variables, $expected);
+  }
+
+  /**
    * Test the use of drupal_pre_render_links() on a nested array of links.
    */
   function testDrupalPreRenderLinks() {
diff --git a/core/modules/system/templates/links.html.twig b/core/modules/system/templates/links.html.twig
index 86713b5..77e7260 100644
--- a/core/modules/system/templates/links.html.twig
+++ b/core/modules/system/templates/links.html.twig
@@ -13,7 +13,6 @@
  *     to l() as its $options parameter.
  *   - attributes: (optional) HTML attributes for the anchor, or for the <span>
  *     tag if no 'href' is supplied.
- *   - link_key: The link CSS class.
  * - heading: (optional) A heading to precede the links.
  *   - text: The heading text.
  *   - level: The heading level (e.g. 'h2', 'h3').
@@ -43,8 +42,8 @@
     {%- endif -%}
   {%- endif -%}
   <ul{{ attributes }}>
-    {%- for key, item in links -%}
-      <li{{ item.attributes.addClass(key|clean_class) }}>
+    {%- for item in links -%}
+      <li{{ item.attributes }}>
         {%- if item.link -%}
           {{ item.link }}
         {%- elseif item.text_attributes -%}
diff --git a/core/modules/views/src/Tests/Plugin/RowRenderCacheTest.php b/core/modules/views/src/Tests/Plugin/RowRenderCacheTest.php
index c00d584..11d5799 100644
--- a/core/modules/views/src/Tests/Plugin/RowRenderCacheTest.php
+++ b/core/modules/views/src/Tests/Plugin/RowRenderCacheTest.php
@@ -156,11 +156,10 @@ protected function doTestRenderedOutput(AccountInterface $account, $check_cache
       $expected = $access ? "<a href=\"$node_url/delete?destination=/\" hreflang=\"en\">delete</a>" : "";
       $output = $view->style_plugin->getField($index, 'delete_node');
       $this->assertEqual($output, $expected);
-
-      $expected = $access ? "  <div class=\"dropbutton-wrapper\"><div class=\"dropbutton-widget\"><ul class=\"dropbutton\">" .
-          "<li class=\"edit\"><a href=\"$node_url/edit?destination=/\" hreflang=\"en\">Edit</a></li>" .
-          "<li class=\"delete\"><a href=\"$node_url/delete?destination=/\" hreflang=\"en\">Delete</a></li>" .
-          "</ul></div></div>" : "";
+      $expected = $access ? '  <div class="dropbutton-wrapper"><div class="dropbutton-widget"><ul class="dropbutton">' .
+        '<li><a href="' . $node_url . '/edit?destination=/" hreflang="en">Edit</a></li>' .
+        '<li><a href="' . $node_url . '/delete?destination=/" hreflang="en">Delete</a></li>' .
+        '</ul></div></div>' : '';
       $output = $view->style_plugin->getField($index, 'operations');
       $this->assertEqual($output, $expected);
 
diff --git a/core/themes/classy/templates/content/links--node.html.twig b/core/themes/classy/templates/content/links--node.html.twig
index f4c3d72..a7b91e7 100644
--- a/core/themes/classy/templates/content/links--node.html.twig
+++ b/core/themes/classy/templates/content/links--node.html.twig
@@ -13,7 +13,6 @@
  *     to l() as its $options parameter.
  *   - attributes: (optional) HTML attributes for the anchor, or for the <span>
  *     tag if no 'href' is supplied.
- *   - link_key: The link CSS class.
  * - heading: (optional) A heading to precede the links.
  *   - text: The heading text.
  *   - level: The heading level (e.g. 'h2', 'h3').
diff --git a/core/themes/classy/templates/navigation/links.html.twig b/core/themes/classy/templates/navigation/links.html.twig
index f82f41a..c1bff6b 100644
--- a/core/themes/classy/templates/navigation/links.html.twig
+++ b/core/themes/classy/templates/navigation/links.html.twig
@@ -13,7 +13,6 @@
  *     to l() as its $options parameter.
  *   - attributes: (optional) HTML attributes for the anchor, or for the <span>
  *     tag if no 'href' is supplied.
- *   - link_key: The link CSS class.
  * - heading: (optional) A heading to precede the links.
  *   - text: The heading text.
  *   - level: The heading level (e.g. 'h2', 'h3').
@@ -41,8 +40,8 @@
     {%- endif -%}
   {%- endif -%}
   <ul{{ attributes }}>
-    {%- for key, item in links -%}
-      <li{{ item.attributes.addClass(key|clean_class) }}>
+    {%- for item in links -%}
+      <li{{ item.attributes }}>
         {%- if item.link -%}
           {{ item.link }}
         {%- elseif item.text_attributes -%}
diff --git a/core/themes/stable/stable.theme b/core/themes/stable/stable.theme
new file mode 100644
index 0000000..663667a
--- /dev/null
+++ b/core/themes/stable/stable.theme
@@ -0,0 +1,20 @@
+<?php
+
+use Drupal\Component\Utility\Html;
+
+/**
+ * Implements template_preprocess_links().
+ */
+function stable_preprocess_links(&$variables) {
+  // @deprecated in Drupal 8.0.x and will be removed before 9.0.0. This feature
+  // of adding a class based on the associative key can cause CSS class
+  // name conflicts.
+  if (!empty($variables['links'])) {
+    foreach ($variables['links'] as $key => $value) {
+      if (!is_numeric($key)) {
+        $class = Html::getClass($key);
+        $variables['links'][$key]['attributes']->addClass($class);
+      }
+    }
+  }
+}
diff --git a/core/themes/stable/templates/navigation/links.html.twig b/core/themes/stable/templates/navigation/links.html.twig
index f82f41a..c1bff6b 100644
--- a/core/themes/stable/templates/navigation/links.html.twig
+++ b/core/themes/stable/templates/navigation/links.html.twig
@@ -13,7 +13,6 @@
  *     to l() as its $options parameter.
  *   - attributes: (optional) HTML attributes for the anchor, or for the <span>
  *     tag if no 'href' is supplied.
- *   - link_key: The link CSS class.
  * - heading: (optional) A heading to precede the links.
  *   - text: The heading text.
  *   - level: The heading level (e.g. 'h2', 'h3').
@@ -41,8 +40,8 @@
     {%- endif -%}
   {%- endif -%}
   <ul{{ attributes }}>
-    {%- for key, item in links -%}
-      <li{{ item.attributes.addClass(key|clean_class) }}>
+    {%- for item in links -%}
+      <li{{ item.attributes }}>
         {%- if item.link -%}
           {{ item.link }}
         {%- elseif item.text_attributes -%}
