diff --git a/core/includes/menu.inc b/core/includes/menu.inc
index a6278ff..8d88679 100644
--- a/core/includes/menu.inc
+++ b/core/includes/menu.inc
@@ -7,7 +7,6 @@
 
 use Drupal\Component\Utility\String;
 use Drupal\Core\Render\Element;
-use Drupal\Core\Template\Attribute;
 
 /**
  * @defgroup menu Menu and routing system
@@ -219,60 +218,53 @@ function _menu_link_translate(&$item) {
 }
 
 /**
- * Implements template_preprocess_HOOK() for theme_menu_tree().
+ * Prepares variables for menu tree templates.
+ *
+ * Default template: menu-tree.html.twig.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - tree: A render array for a menu tree.
  */
 function template_preprocess_menu_tree(&$variables) {
   $variables['tree'] = $variables['tree']['#children'];
 }
 
 /**
- * Returns HTML for a wrapper for a menu sub-tree.
- *
- * @param $variables
- *   An associative array containing:
- *   - tree: An HTML string containing the tree's items.
+ * Prepares variables for menu link plus submenu templates.
  *
- * @see template_preprocess_menu_tree()
- * @ingroup themeable
- */
-function theme_menu_tree($variables) {
-  return '<ul class="menu">' . $variables['tree'] . '</ul>';
-}
-
-/**
- * Returns HTML for a menu link and submenu.
+ * Default template: menu-link.html.twig.
  *
- * @param $variables
+ * @param array $variables
  *   An associative array containing:
  *   - element: Structured array data for a menu link.
- *
- * @ingroup themeable
  */
-function theme_menu_link(array $variables) {
+function template_preprocess_menu_link(&$variables) {
   $element = $variables['element'];
-  $sub_menu = '';
-
-  if ($element['#below']) {
-    $sub_menu = drupal_render($element['#below']);
-  }
   $element['#localized_options']['set_active_class'] = TRUE;
-  $output = l($element['#title'], $element['#href'], $element['#localized_options']);
-  return '<li' . new Attribute($element['#attributes']) . '>' . $output . $sub_menu . "</li>\n";
+  $variables['sub_menu'] = $element['#below'];
+  $variables['link'] = array(
+    '#type' => 'link',
+    '#title' => $element['#title'],
+    '#href' => $element['#href'],
+    '#options' => $element['#localized_options'],
+  );
+  $variables['attributes'] = $element['#attributes'];
 }
 
 /**
- * Returns HTML for a single local task link.
+ * Prepares variables for single local task link templates.
+ *
+ * Default template: menu-local-task.html.twig.
  *
- * @param $variables
+ * @param array $variables
  *   An associative array containing:
  *   - element: A render element containing:
  *     - #link: A menu link array with 'title', 'href', and 'localized_options'
  *       keys.
  *     - #active: A boolean indicating whether the local task is active.
- *
- * @ingroup themeable
  */
-function theme_menu_local_task($variables) {
+function template_preprocess_menu_local_task(&$variables) {
   $link = $variables['element']['#link'];
   $link += array(
     'localized_options' => array(),
@@ -282,6 +274,7 @@ function theme_menu_local_task($variables) {
   if (!empty($variables['element']['#active'])) {
     // Add text to indicate active tab for non-visual users.
     $active = '<span class="visually-hidden">' . t('(active tab)') . '</span>';
+    $variables['attributes']['class'] = array('active');
 
     // If the link does not contain HTML already, String::checkPlain() it now.
     // After we set 'html'=TRUE the link will not be sanitized by l().
@@ -293,29 +286,36 @@ function theme_menu_local_task($variables) {
   }
   $link['localized_options']['set_active_class'] = TRUE;
 
+  $variables['link'] = array(
+      '#type' => 'link',
+      '#title' => $link_text,
+      '#options' => $link['localized_options'],
+  );
+
   if (!empty($link['href'])) {
     // @todo - remove this once all pages are converted to routes.
-    $a_tag = l($link_text, $link['href'], $link['localized_options']);
+    $variables['link']['#href'] = $link['href'];
   }
   else {
-    $a_tag = \Drupal::l($link_text, $link['route_name'], $link['route_parameters'], $link['localized_options']);
+    $variables['link'] += array(
+      '#route_name' => $link['route_name'],
+      '#route_parameters' => $link['route_parameters'],
+    );
   }
-
-  return '<li' . (!empty($variables['element']['#active']) ? ' class="active"' : '') . '>' . $a_tag . '</li>';
 }
 
 /**
- * Returns HTML for a single local action link.
+ * Prepares variables for single local action link templates.
+ *
+ * Default template: menu-local-action.html.twig.
  *
- * @param $variables
+ * @param array $variables
  *   An associative array containing:
  *   - element: A render element containing:
  *     - #link: A menu link array with 'title', 'href', and 'localized_options'
  *       keys.
- *
- * @ingroup themeable
  */
-function theme_menu_local_action($variables) {
+function template_preprocess_menu_local_action(&$variables) {
   $link = $variables['element']['#link'];
   $link += array(
     'href' => '',
@@ -326,19 +326,24 @@ function theme_menu_local_action($variables) {
   $link['localized_options']['attributes']['class'][] = 'button-action';
   $link['localized_options']['set_active_class'] = TRUE;
 
-  $output = '<li>';
+  $variables['link'] = array(
+      '#type' => 'link',
+      '#title' => $link['title'],
+      '#options' => $link['localized_options'],
+  );
+
   // @todo Remove this check and the call to l() when all pages are converted to
   //   routes.
   // @todo Figure out how to support local actions without a href properly.
   if ($link['href'] === '' && !empty($link['route_name'])) {
-    $output .= Drupal::l($link['title'], $link['route_name'], $link['route_parameters'], $link['localized_options']);
+    $variables['link'] += array(
+      '#route_name' => $link['route_name'],
+      '#route_parameters' => $link['route_parameters'],
+    );
   }
   else {
-    $output .= l($link['title'], $link['href'], $link['localized_options']);
+    $variables['link']['#href'] = $link['href'];
   }
-  $output .= "</li>";
-
-  return $output;
 }
 
 /**
@@ -559,36 +564,6 @@ function menu_local_tabs() {
 }
 
 /**
- * Returns HTML for primary and secondary local tasks.
- *
- * @param $variables
- *   An associative array containing:
- *     - primary: (optional) An array of local tasks (tabs).
- *     - secondary: (optional) An array of local tasks (tabs).
- *
- * @ingroup themeable
- * @see menu_local_tasks()
- */
-function theme_menu_local_tasks(&$variables) {
-  $output = '';
-
-  if (!empty($variables['primary'])) {
-    $variables['primary']['#prefix'] = '<h2 class="visually-hidden">' . t('Primary tabs') . '</h2>';
-    $variables['primary']['#prefix'] .= '<ul class="tabs primary">';
-    $variables['primary']['#suffix'] = '</ul>';
-    $output .= drupal_render($variables['primary']);
-  }
-  if (!empty($variables['secondary'])) {
-    $variables['secondary']['#prefix'] = '<h2 class="visually-hidden">' . t('Secondary tabs') . '</h2>';
-    $variables['secondary']['#prefix'] .= '<ul class="tabs secondary">';
-    $variables['secondary']['#suffix'] = '</ul>';
-    $output .= drupal_render($variables['secondary']);
-  }
-
-  return $output;
-}
-
-/**
  * Sets (or gets) the active menu for the current page.
  *
  * The active menu for the page determines the active trail.
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 38569ad..67f7885 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -2603,18 +2603,23 @@ function drupal_common_theme() {
     // From menu.inc.
     'menu_link' => array(
       'render element' => 'element',
+      'template' => 'menu-link',
     ),
     'menu_tree' => array(
       'render element' => 'tree',
+      'template' => 'menu-tree',
     ),
     'menu_local_task' => array(
       'render element' => 'element',
+      'template' => 'menu-local-task',
     ),
     'menu_local_action' => array(
       'render element' => 'element',
+        'template' => 'menu-local-action',
     ),
     'menu_local_tasks' => array(
       'variables' => array('primary' => array(), 'secondary' => array()),
+      'template' => 'menu-local-tasks',
     ),
     // From form.inc.
     'input' => array(
diff --git a/core/modules/system/css/system.theme.css b/core/modules/system/css/system.theme.css
index 4fca3de..77e7895 100644
--- a/core/modules/system/css/system.theme.css
+++ b/core/modules/system/css/system.theme.css
@@ -451,7 +451,7 @@ ul.links a.active {
 }
 
 /**
- * Markup generated by theme_menu_local_tasks().
+ * Markup generated by menu-local-tasks.html.twig.
  */
 div.tabs {
   margin: 1em 0;
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index c96de79..9e097a0 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -719,6 +719,16 @@ function system_theme_suggestions_field(array $variables) {
 }
 
 /**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function system_theme_suggestions_menu_link(array $variables) {
+  $suggestions = array();
+  $element = $variables['element'];
+
+  $suggestions[] = 'menu_link__' . $element['#original_link']['menu_name'];
+}
+
+/**
  * Implements hook_stream_wrappers().
  */
 function system_stream_wrappers() {
diff --git a/core/modules/system/templates/menu-link.html.twig b/core/modules/system/templates/menu-link.html.twig
new file mode 100644
index 0000000..18c0bb9
--- /dev/null
+++ b/core/modules/system/templates/menu-link.html.twig
@@ -0,0 +1,21 @@
+{#
+/**
+ * @file
+ * Default theme implementation for a menu link and submenu.
+ *
+ * Available variables:
+ * - attributes: HTML attributes for the wrapper element.
+ * - link: An HTML link element.
+ * - sub_menu: Rendered list item children of the element.
+ *
+ * Note: This template renders the content for each individual menu item in
+ * menu-tree.html.twig.
+ *
+ * @see template_preprocess_menu_link()
+ *
+ * @ingroup themeable
+ */
+#}
+<li{{ attributes }}>
+  {{ link }}{{ sub_menu }}
+</li>
diff --git a/core/modules/system/templates/menu-local-action.html.twig b/core/modules/system/templates/menu-local-action.html.twig
new file mode 100644
index 0000000..eb02f42
--- /dev/null
+++ b/core/modules/system/templates/menu-local-action.html.twig
@@ -0,0 +1,14 @@
+{#
+/**
+ * @file
+ * Default theme implementation for a single local action link.
+ *
+ * Available variables:
+ * - link: A rendered link element.
+ *
+ * @see template_preprocess_menu_local_action()
+ *
+ * @ingroup themeable
+ */
+#}
+<li>{{ link }}</li>
diff --git a/core/modules/system/templates/menu-local-task.html.twig b/core/modules/system/templates/menu-local-task.html.twig
new file mode 100644
index 0000000..2d2d351
--- /dev/null
+++ b/core/modules/system/templates/menu-local-task.html.twig
@@ -0,0 +1,22 @@
+{#
+/**
+ * @file
+ * Default theme implementation for a local task link.
+ *
+ * Available variables:
+ * - attributes: HTML attributes for the wrapper element.
+ * - link: The rendered link (<a> tag).
+ *
+ * Note: This template renders the content for each task item in
+ * menu-local-tasks.html.twig.
+ *
+ * @see template_preprocess_menu_local_task()
+ *
+ * @ingroup themeable
+ */
+#}
+{% spaceless %}
+  <li{{ attributes }}>
+    {{ link }}
+  </li>
+{% endspaceless %}
diff --git a/core/modules/system/templates/menu-local-tasks.html.twig b/core/modules/system/templates/menu-local-tasks.html.twig
new file mode 100644
index 0000000..dce922b
--- /dev/null
+++ b/core/modules/system/templates/menu-local-tasks.html.twig
@@ -0,0 +1,25 @@
+{#
+/**
+ * @file
+ * Default theme implementation to display primary and secondary local tasks.
+ *
+ * Available variables:
+ * - primary: HTML list items representing primary tasks.
+ * - secondary: HTML list items representing primary tasks.
+ *
+ * Each item in these variables (primary and secondary) can be individually
+ * themed in menu-local-task.html.twig.
+ *
+ * @see template_preprocess_menu_local_tasks()
+ *
+ * @ingroup themeable
+ */
+#}
+{% if primary %}
+  <h2 class="visually-hidden">{{ 'Primary tabs'|t }}</h2>
+  <ul class="tabs primary">{{ primary }}</ul>
+{% endif %}
+{% if secondary %}
+  <h2 class="visually-hidden">{{ 'Secondary tabs'|t }}</h2>
+  <ul class="tabs secondary">{{ secondary }}</ul>
+{% endif %}
diff --git a/core/modules/system/templates/menu-tree.html.twig b/core/modules/system/templates/menu-tree.html.twig
new file mode 100644
index 0000000..f056794
--- /dev/null
+++ b/core/modules/system/templates/menu-tree.html.twig
@@ -0,0 +1,19 @@
+{#
+/**
+ * @file
+ * Default theme implementation for the wrapper of a menu tree.
+ *
+ * Available variables:
+ * - tree: An HTML string containing the tree's items.
+ *
+ * Note: Each item in the menu tree can be individually themed in
+ * menu-link.html.twig.
+ *
+ * @see template_preprocess_menu_tree()
+ *
+ * @ingroup themeable
+ */
+#}
+<ul class="menu">
+  {{ tree }}
+</ul>
diff --git a/core/themes/bartik/bartik.theme b/core/themes/bartik/bartik.theme
index db70a55..cf20c51 100644
--- a/core/themes/bartik/bartik.theme
+++ b/core/themes/bartik/bartik.theme
@@ -139,20 +139,6 @@ function bartik_preprocess_block(&$variables) {
 }
 
 /**
- * Implements THEME_menu_tree().
- */
-function bartik_menu_tree($variables) {
-  return '<ul class="menu clearfix">' . $variables['tree'] . '</ul>';
-}
-
-/**
- * Implements THEME_menu_tree__MENUNAME().
- */
-function bartik_menu_tree__shortcut_default($variables) {
-  return '<ul class="menu">' . $variables['tree'] . '</ul>';
-}
-
-/**
  * Implements hook_preprocess_HOOK() for field.html.twig.
  *
  * @see template_preprocess_field()
diff --git a/core/themes/bartik/templates/menu-tree.html.twig b/core/themes/bartik/templates/menu-tree.html.twig
new file mode 100644
index 0000000..0ca902b
--- /dev/null
+++ b/core/themes/bartik/templates/menu-tree.html.twig
@@ -0,0 +1,19 @@
+{#
+/**
+ * @file
+ * Bartik's theme implementation for the wrapper of a menu tree.
+ *
+ * Available variables:
+ * - tree: An HTML string containing the tree's items.
+ *
+ * Note: Each item in the menu tree can be individually themed in
+ * menu-link.html.twig.
+ *
+ * @see template_preprocess_menu_tree()
+ *
+ * @ingroup themeable
+ */
+#}
+<ul class="menu clearfix">
+  {{ tree }}
+</ul>
diff --git a/core/themes/seven/seven.theme b/core/themes/seven/seven.theme
index b04d2d5..4a7ef44 100644
--- a/core/themes/seven/seven.theme
+++ b/core/themes/seven/seven.theme
@@ -37,77 +37,34 @@ function seven_preprocess_page(&$variables) {
 }
 
 /**
- * Overrides theme_menu_local_tasks().
+ * Implements hook_pre_render_HOOK() for menu-local-tasks templates.
  *
- * Returns HTML for primary and secondary local tasks.
+ * Use preprocess hook to set #attached to child elemnts
+ * because they will be processed by Twig and drupal_render will
+ * be invoked.
  */
-function seven_menu_local_tasks(&$variables) {
-  $output = '';
-
+function seven_preprocess_menu_local_tasks(&$variables) {
   if (!empty($variables['primary'])) {
     $variables['primary']['#attached'] = array(
       'library' => array(
         'seven/drupal.nav-tabs',
       ),
     );
-    $variables['primary']['#prefix'] = '<h2 id="primary-tabs-title" class="visually-hidden">' . t('Primary tabs') . '</h2>';
-    $variables['primary']['#prefix'] .= '<nav role="navigation" class="is-horizontal is-collapsible" aria-labelledby="primary-tabs-title" data-drupal-nav-tabs>';
-    $variables['primary']['#prefix'] .= '<button class="reset-appearance tabs__tab tabs__trigger" aria-label="Primary tabs display toggle" data-drupal-nav-tabs-trigger>&bull;&bull;&bull;</button>';
-    $variables['primary']['#prefix'] .= '<ul class="tabs primary clearfix" data-drupal-nav-tabs-target>';
-    $variables['primary']['#suffix'] = '</ul>';
-    $variables['primary']['#suffix'] .= '</nav>';
-    $output .= drupal_render($variables['primary']);
   }
-  if (!empty($variables['secondary'])) {
+  elseif (!empty($variables['secondary'])) {
     $variables['secondary']['#attached'] = array(
       'library' => array(
         'seven/drupal.nav-tabs',
       ),
     );
-    $variables['secondary']['#prefix'] = '<h2 id="secondary-tabs-title" class="visually-hidden">' . t('Secondary tabs') . '</h2>';
-    $variables['secondary']['#prefix'] .= '<nav role="navigation" class="is-horizontal" aria-labelledby="secondary-tabs-title" data-drupal-nav-tabs>';
-    $variables['secondary']['#prefix'] .= '<ul class="tabs secondary clearfix">';
-    $variables['secondary']['#suffix'] = '</ul>';
-    $variables['secondary']['#suffix'] .= '</nav>';
-    $output .= drupal_render($variables['secondary']);
   }
-
-  return $output;
 }
 
 /**
- * Overrides theme_menu_local_task().
- *
- * Returns HTML for a local task.
+ * Implements hook_preprocess_HOOK() for menu-local-task templates.
  */
-function seven_menu_local_task($variables) {
-  $link = $variables['element']['#link'];
-  $link += array(
-    'localized_options' => array(),
-  );
-  $link_text = $link['title'];
-
-  if (!empty($variables['element']['#active'])) {
-    // Add text to indicate active tab for non-visual users.
-    $active = '<span class="visually-hidden">' . t('(active tab)') . '</span>';
-
-    // If the link does not contain HTML already, String::checkPlain() it now.
-    // After we set 'html'=TRUE the link will not be sanitized by l().
-    if (empty($link['localized_options']['html'])) {
-      $link['title'] = String::checkPlain($link['title']);
-    }
-    $link['localized_options']['html'] = TRUE;
-    $link_text = t('!local-task-title!active', array('!local-task-title' => $link['title'], '!active' => $active));
-  }
-  if (!empty($link['href'])) {
-    // @todo - remove this once all pages are converted to routes.
-    $a_tag = l($link_text, $link['href'], $link['localized_options']);
-  }
-  else {
-    $a_tag = \Drupal::l($link_text, $link['route_name'], $link['route_parameters'], $link['localized_options']);
-  }
-
-  return '<li' . (!empty($variables['element']['#active']) ? ' class="tabs__tab active"' : ' class="tabs__tab"') . '>' . $a_tag . '</li>';
+function seven_preprocess_menu_local_task(array &$variables) {
+  $variables['attributes']['class'][] = 'tabs__tab';
 }
 
 /**
@@ -212,22 +169,11 @@ function seven_tablesort_indicator($variables) {
 }
 
 /**
- * Overrides theme_menu_local_action().
+ * Implements hook_preprocess_HOOK() for menu-local-action templates.
  */
-function seven_menu_local_action($variables) {
-  $link = $variables['element']['#link'];
-  $link += array(
-    'href' => '',
-    'localized_options' => array(),
-    'route_parameters' => array(),
-  );
-  $link['localized_options']['attributes']['class'][] = 'button';
-  $link['localized_options']['attributes']['class'][] = 'button--primary';
-  $link['localized_options']['attributes']['class'][] = 'button--small';
-
-  // @todo Replace with a generalized solution for icons.
-  // See http://drupal.org/node/1849712
-  $link['localized_options']['attributes']['class'][] = 'button-action';
+function seven_preprocess_menu_local_action(array &$variables) {
+  $variables['link']['#options']['attributes']['class'][] = 'button--primary';
+  $variables['link']['#options']['attributes']['class'][] = 'button--small';
 
   // We require Modernizr's touch test for button styling.
   $libraries = array(
@@ -238,20 +184,6 @@ function seven_menu_local_action($variables) {
     ),
   );
   drupal_render($libraries);
-
-  $output = '<li>';
-  // @todo Remove this check and the call to l() when all pages are converted to
-  //   routes.
-  // @todo Figure out how to support local actions without a href properly.
-  if ($link['href'] === '' && !empty($link['route_name'])) {
-    $output .= Drupal::l($link['title'], $link['route_name'], $link['route_parameters'], $link['localized_options']);
-  }
-  else {
-    $output .= l($link['title'], $link['href'], $link['localized_options']);
-  }
-  $output .= "</li>";
-
-  return $output;
 }
 
 /**
diff --git a/core/themes/seven/templates/menu-local-tasks.html.twig b/core/themes/seven/templates/menu-local-tasks.html.twig
new file mode 100644
index 0000000..c71ae04
--- /dev/null
+++ b/core/themes/seven/templates/menu-local-tasks.html.twig
@@ -0,0 +1,30 @@
+{#
+/**
+ * @file
+ * Seven theme implementation to display primary and secondary local tasks.
+ *
+ * Available variables:
+ * - primary: HTML list items representing primary tasks.
+ * - secondary: HTML list items representing primary tasks.
+ *
+ * Each item in these variables (primary and secondary) can be individually
+ * themed in menu-local-task.html.twig.
+ *
+ * @see template_preprocess_menu_local_tasks()
+ *
+ * @ingroup themeable
+ */
+#}
+{% if primary %}
+  <h2 id="primary-tabs-title" class="visually-hidden">{{ 'Primary tabs'|t }}</h2>
+  <nav role="navigation" class="is-horizontal is-collapsible" aria-labelledby="primary-tabs-title" data-drupal-nav-tabs>
+    <button class="reset-appearance tabs__tab tabs__trigger" aria-label="{{ 'Primary tabs display toggle'|t }}" data-drupal-nav-tabs-trigger>&bull;&bull;&bull;</button>
+    <ul class="tabs primary clearfix" data-drupal-nav-tabs-target>{{ primary }}</ul>
+  </nav>
+{% endif %}
+{% if secondary %}
+  <h2 id="secondary-tabs-title" class="visually-hidden">{{ 'Secondary tabs'|t }}</h2>
+  <nav role="navigation" class="is-horizontal" aria-labelledby="secondary-tabs-title" data-drupal-nav-tabs>
+    <ul class="tabs secondary clearfix">{{ secondary }}</ul>
+  </nav>
+{% endif %}
