diff --git a/core/lib/Drupal/Core/Menu/MenuLinkTree.php b/core/lib/Drupal/Core/Menu/MenuLinkTree.php
index 090293c..6307c49 100644
--- a/core/lib/Drupal/Core/Menu/MenuLinkTree.php
+++ b/core/lib/Drupal/Core/Menu/MenuLinkTree.php
@@ -243,26 +243,28 @@ protected function buildItems(array $tree, CacheableMetadata &$tree_access_cache
       if ($data->access instanceof AccessResultInterface && !$data->access->isAllowed()) {
         continue;
       }
+      $element = [];
 
-      $class = ['menu-item'];
-      // Set a class for the <li>-tag. Only set 'expanded' class if the link
-      // also has visible children within the current tree.
+      // Set a variable for the <li> tag. Only set 'expanded' to true if the
+      // link also has visible children within the current tree.
+      $element['is_expanded'] = FALSE;
+      $element['is_collapsed'] = FALSE;
       if ($data->hasChildren && !empty($data->subtree)) {
-        $class[] = 'menu-item--expanded';
+        $element['is_expanded'] = TRUE;
       }
       elseif ($data->hasChildren) {
-        $class[] = 'menu-item--collapsed';
+        $element['is_collapsed'] = TRUE;
       }
-      // Set a class if the link is in the active trail.
+      // Set a helper variable to indicate whether the link is in the active
+      // trail.
+      $element['in_active_trail'] = FALSE;
       if ($data->inActiveTrail) {
-        $class[] = 'menu-item--active-trail';
+        $element['in_active_trail'] = TRUE;
       }
 
       // Note: links are rendered in the menu.html.twig template; and they
       // automatically bubble their associated cacheability metadata.
-      $element = array();
       $element['attributes'] = new Attribute();
-      $element['attributes']['class'] = $class;
       $element['title'] = $link->getTitle();
       $element['url'] = $link->getUrlObject();
       $element['url']->setOption('set_active_class', TRUE);
diff --git a/core/modules/book/src/BookManager.php b/core/modules/book/src/BookManager.php
index fcb46a4..e6e2588 100644
--- a/core/modules/book/src/BookManager.php
+++ b/core/modules/book/src/BookManager.php
@@ -539,30 +539,33 @@ protected function buildItems(array $tree) {
     $items = [];
 
     foreach ($tree as $data) {
-      $class = ['menu-item'];
+      $element = [];
+
       // Generally we only deal with visible links, but just in case.
       if (!$data['link']['access']) {
         continue;
       }
-      // Set a class for the <li>-tag. Since $data['below'] may contain local
-      // tasks, only set 'expanded' class if the link also has children within
+      // Set a class for the <li> tag. Since $data['below'] may contain local
+      // tasks, only set 'expanded' to true if the link also has children within
       // the current book.
+      $element['is_expanded'] = FALSE;
+      $element['is_collapsed'] = FALSE;
       if ($data['link']['has_children'] && $data['below']) {
-        $class[] = 'menu-item--expanded';
+        $element['is_expanded'] = TRUE;
       }
       elseif ($data['link']['has_children']) {
-        $class[] = 'menu-item--collapsed';
+        $element['is_collapsed'] = TRUE;
       }
 
-      // Set a class if the link is in the active trail.
+      // Set a helper variable to indicate whether the link is in the active
+      // trail.
+      $element['in_active_trail'] = FALSE;
       if ($data['link']['in_active_trail']) {
-        $class[] = 'menu-item--active-trail';
+        $element['in_active_trail'] = TRUE;
       }
 
       // Allow book-specific theme overrides.
-      $element = [];
       $element['attributes'] = new Attribute();
-      $element['attributes']['class'] = $class;
       $element['title'] = $data['link']['title'];
       $node = $this->entityManager->getStorage('node')->load($data['link']['nid']);
       $element['url'] = $node->urlInfo();
diff --git a/core/modules/book/templates/book-tree.html.twig b/core/modules/book/templates/book-tree.html.twig
index a4edb37..bf7424f 100644
--- a/core/modules/book/templates/book-tree.html.twig
+++ b/core/modules/book/templates/book-tree.html.twig
@@ -11,6 +11,11 @@
  *   - below: The book item child items.
  *   - title: The book link title.
  *   - url: The book link URL, instance of \Drupal\Core\Url.
+ *   - is_expanded: TRUE if the link has visible children within the current
+ *     book tree.
+ *   - is_collapsed: TRUE if the link has children within the current book tree
+ *     that are not currently visible.
+ *   - in_active_trail: TRUE if the link is in the active trail.
  *
  * @ingroup themeable
  */
@@ -31,14 +36,14 @@
     {% else %}
       <ul>
     {% endif %}
-      {% for item in items %}
-        <li{{ item.attributes }}>
-          {{ link(item.title, item.url) }}
-          {% if item.below %}
-            {{ book_tree.book_links(item.below, attributes, menu_level + 1) }}
-          {% endif %}
-        </li>
-      {% endfor %}
+    {% for item in items %}
+      <li{{ item.attributes }}>
+        {{ link(item.title, item.url) }}
+        {% if item.below %}
+          {{ book_tree.book_links(item.below, attributes, menu_level + 1) }}
+        {% endif %}
+      </li>
+    {% endfor %}
     </ul>
   {% endif %}
 {% endmacro %}
diff --git a/core/modules/system/templates/menu.html.twig b/core/modules/system/templates/menu.html.twig
index a9c7899..03704f2 100644
--- a/core/modules/system/templates/menu.html.twig
+++ b/core/modules/system/templates/menu.html.twig
@@ -11,6 +11,11 @@
  *   - title: The menu link title.
  *   - url: The menu link url, instance of \Drupal\Core\Url
  *   - localized_options: Menu link localized options.
+ *   - is_expanded: TRUE if the link has visible children within the current
+ *     menu tree.
+ *   - is_collapsed: TRUE if the link has children within the current menu tree
+ *     that are not currently visible.
+ *   - in_active_trail: TRUE if the link is in the active trail.
  *
  * @ingroup themeable
  */
@@ -31,14 +36,14 @@
     {% else %}
       <ul>
     {% endif %}
-      {% for item in items %}
-        <li{{ item.attributes }}>
-          {{ link(item.title, item.url) }}
-          {% if item.below %}
-            {{ menus.menu_links(item.below, attributes, menu_level + 1) }}
-          {% endif %}
-        </li>
-      {% endfor %}
+    {% for item in items %}
+      <li{{ item.attributes }}>
+        {{ link(item.title, item.url) }}
+        {% if item.below %}
+          {{ menus.menu_links(item.below, attributes, menu_level + 1) }}
+        {% endif %}
+      </li>
+    {% endfor %}
     </ul>
   {% endif %}
 {% endmacro %}
diff --git a/core/modules/system/tests/src/Unit/Menu/MenuLinkTreeTest.php b/core/modules/system/tests/src/Unit/Menu/MenuLinkTreeTest.php
index da545b2..9faa03d 100644
--- a/core/modules/system/tests/src/Unit/Menu/MenuLinkTreeTest.php
+++ b/core/modules/system/tests/src/Unit/Menu/MenuLinkTreeTest.php
@@ -140,14 +140,29 @@ public function providerTestBuildCacheability() {
       ]
     ];
 
-    $get_built_element = function(MenuLinkTreeElement $element, array $classes) {
-      return [
-        'attributes' => new Attribute(['class' => array_merge(['menu-item'], $classes)]),
+    $get_built_element = function(MenuLinkTreeElement $element) {
+      $return = [
+        'attributes' => new Attribute(),
         'title' => $element->link->getTitle(),
         'url' => new Url($element->link->getRouteName(), $element->link->getRouteParameters(), ['set_active_class' => TRUE]),
         'below' => [],
         'original_link' => $element->link,
+        'is_expanded' => FALSE,
+        'is_collapsed' => FALSE,
+        'in_active_trail' => FALSE,
       ];
+
+      if ($element->hasChildren && !empty($element->subtree)) {
+        $return['is_expanded'] = TRUE;
+      }
+      elseif ($element->hasChildren) {
+        $return['is_collapsed'] = TRUE;
+      }
+      if ($element->inActiveTrail) {
+        $return['in_active_trail'] = TRUE;
+      }
+
+      return $return;
     };
 
     // The three access scenarios described in this method's documentation.
@@ -195,7 +210,7 @@ public function providerTestBuildCacheability() {
         $tree[0]->access = $access;
         if ($access === NULL || $access->isAllowed()) {
           $expected_build = $base_expected_build;
-          $expected_build['#items']['test.example1'] = $get_built_element($tree[0], []);
+          $expected_build['#items']['test.example1'] = $get_built_element($tree[0]);
         }
         else {
           $expected_build = $base_expected_build_empty;
@@ -217,9 +232,9 @@ public function providerTestBuildCacheability() {
         $tree[0]->access = $access;
         $expected_build = $base_expected_build;
         if ($access === NULL || $access->isAllowed()) {
-          $expected_build['#items']['test.example1'] = $get_built_element($tree[0], []);
+          $expected_build['#items']['test.example1'] = $get_built_element($tree[0]);
         }
-        $expected_build['#items']['test.example2'] = $get_built_element($tree[1], []);
+        $expected_build['#items']['test.example2'] = $get_built_element($tree[1]);
         $expected_build['#cache']['contexts'] = array_merge($expected_build['#cache']['contexts'], $access_cache_contexts, $links[0]->getCacheContexts(), $links[1]->getCacheContexts());
         $data[] = [
           'description' => "Single-level tree; access=$i; link=$j.",
@@ -245,13 +260,13 @@ public function providerTestBuildCacheability() {
         ];
         $tree[0]->subtree[0]->subtree[0]->access = $access;
         $expected_build = $base_expected_build;
-        $expected_build['#items']['test.roota'] = $get_built_element($tree[0], ['menu-item--expanded']);
-        $expected_build['#items']['test.roota']['below']['test.parentc'] = $get_built_element($tree[0]->subtree[0], ['menu-item--expanded']);
+        $expected_build['#items']['test.roota'] = $get_built_element($tree[0]);
+        $expected_build['#items']['test.roota']['below']['test.parentc'] = $get_built_element($tree[0]->subtree[0]);
         if ($access === NULL || $access->isAllowed()) {
-          $expected_build['#items']['test.roota']['below']['test.parentc']['below']['test.example1'] = $get_built_element($tree[0]->subtree[0]->subtree[0], []);
+          $expected_build['#items']['test.roota']['below']['test.parentc']['below']['test.example1'] = $get_built_element($tree[0]->subtree[0]->subtree[0]);
         }
-        $expected_build['#items']['test.rootb'] = $get_built_element($tree[1], ['menu-item--expanded']);
-        $expected_build['#items']['test.rootb']['below']['test.example2'] = $get_built_element($tree[1]->subtree[0], []);
+        $expected_build['#items']['test.rootb'] = $get_built_element($tree[1]);
+        $expected_build['#items']['test.rootb']['below']['test.example2'] = $get_built_element($tree[1]->subtree[0]);
         $expected_build['#cache']['contexts'] = array_merge($expected_build['#cache']['contexts'], $access_cache_contexts, $links[0]->getCacheContexts(), $links[1]->getCacheContexts());
         $data[] = [
           'description' => "Multi-level tree; access=$i; link=$j.",
diff --git a/core/modules/toolbar/templates/menu--toolbar.html.twig b/core/modules/toolbar/templates/menu--toolbar.html.twig
index 3578b95..659e8f5 100644
--- a/core/modules/toolbar/templates/menu--toolbar.html.twig
+++ b/core/modules/toolbar/templates/menu--toolbar.html.twig
@@ -11,6 +11,11 @@
  *   - title: The menu link title.
  *   - url: The menu link url, instance of \Drupal\Core\Url
  *   - localized_options: Menu link localized options.
+ *   - is_expanded: TRUE if the link has visible children within the current
+ *     menu tree.
+ *   - is_collapsed: TRUE if the link has children within the current menu tree
+ *     that are not currently visible.
+ *   - in_active_trail: TRUE if the link is in the active trail.
  *
  * @ingroup themeable
  */
@@ -31,14 +36,22 @@
     {% else %}
       <ul class="toolbar-menu">
     {% endif %}
-      {% for item in items %}
-        <li{{ item.attributes }}>
-          {{ link(item.title, item.url) }}
-          {% if item.below %}
-            {{ menus.menu_links(item.below, attributes, menu_level + 1) }}
-          {% endif %}
-        </li>
-      {% endfor %}
+    {% for item in items %}
+      {%
+        set classes = [
+          'menu-item',
+          item.is_expanded ? 'menu-item--expanded',
+          item.is_collapsed ? 'menu-item--collapsed',
+          item.in_active_trail ? 'menu-item--active-trail',
+        ]
+      %}
+      <li{{ item.attributes.addClass(classes) }}>
+        {{ link(item.title, item.url) }}
+        {% if item.below %}
+          {{ menus.menu_links(item.below, attributes, menu_level + 1) }}
+        {% endif %}
+      </li>
+    {% endfor %}
     </ul>
   {% endif %}
 {% endmacro %}
diff --git a/core/themes/classy/templates/navigation/book-tree.html.twig b/core/themes/classy/templates/navigation/book-tree.html.twig
index 543696c..186a547 100644
--- a/core/themes/classy/templates/navigation/book-tree.html.twig
+++ b/core/themes/classy/templates/navigation/book-tree.html.twig
@@ -11,6 +11,11 @@
  *   - below: The book item child items.
  *   - title: The book link title.
  *   - url: The book link URL, instance of \Drupal\Core\Url.
+ *   - is_expanded: TRUE if the link has visible children within the current
+ *     book tree.
+ *   - is_collapsed: TRUE if the link has children within the current book tree
+ *     that are not currently visible.
+ *   - in_active_trail: TRUE if the link is in the active trail.
  */
 #}
 {% import _self as book_tree %}
@@ -29,14 +34,22 @@
     {% else %}
       <ul class="menu">
     {% endif %}
-      {% for item in items %}
-        <li{{ item.attributes }}>
-          {{ link(item.title, item.url) }}
-          {% if item.below %}
-            {{ book_tree.book_links(item.below, attributes, menu_level + 1) }}
-          {% endif %}
-        </li>
-      {% endfor %}
+    {% for item in items %}
+      {%
+        set classes = [
+          'menu-item',
+          item.is_expanded ? 'menu-item--expanded',
+          item.is_collapsed ? 'menu-item--collapsed',
+          item.in_active_trail ? 'menu-item--active-trail',
+        ]
+      %}
+      <li{{ item.attributes.addClass(classes) }}>
+        {{ link(item.title, item.url) }}
+        {% if item.below %}
+          {{ book_tree.book_links(item.below, attributes, menu_level + 1) }}
+        {% endif %}
+      </li>
+    {% endfor %}
     </ul>
   {% endif %}
 {% endmacro %}
diff --git a/core/themes/classy/templates/navigation/menu.html.twig b/core/themes/classy/templates/navigation/menu.html.twig
index 4e12ea1..67ada7d 100644
--- a/core/themes/classy/templates/navigation/menu.html.twig
+++ b/core/themes/classy/templates/navigation/menu.html.twig
@@ -11,6 +11,11 @@
  *   - title: The menu link title.
  *   - url: The menu link url, instance of \Drupal\Core\Url
  *   - localized_options: Menu link localized options.
+ *   - is_expanded: TRUE if the link has visible children within the current
+ *     tree.
+ *   - is_collapsed: TRUE if the link has children within the current tree that
+ *     are not currently visible.
+ *   - in_active_trail: TRUE if the link is in the active trail.
  */
 #}
 {% import _self as menus %}
@@ -29,14 +34,22 @@
     {% else %}
       <ul class="menu">
     {% endif %}
-      {% for item in items %}
-        <li{{ item.attributes }}>
-          {{ link(item.title, item.url) }}
-          {% if item.below %}
-            {{ menus.menu_links(item.below, attributes, menu_level + 1) }}
-          {% endif %}
-        </li>
-      {% endfor %}
+    {% for item in items %}
+      {%
+        set classes = [
+          'menu-item',
+          item.is_expanded ? 'menu-item--expanded',
+          item.is_collapsed ? 'menu-item--collapsed',
+          item.in_active_trail ? 'menu-item--active-trail',
+        ]
+      %}
+      <li{{ item.attributes.addClass(classes) }}>
+        {{ link(item.title, item.url) }}
+        {% if item.below %}
+          {{ menus.menu_links(item.below, attributes, menu_level + 1) }}
+        {% endif %}
+      </li>
+    {% endfor %}
     </ul>
   {% endif %}
 {% endmacro %}
