diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 7e0e5e2..7a81b09 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -862,17 +862,16 @@ function drupal_find_base_themes($themes, $key, $used_keys = array()) {
  * a noticeable performance penalty.
  *
  * @subsection sub_alternate_suggestions Suggesting Alternate Hooks
- * There are two special variables that these preprocess functions can set:
- * 'theme_hook_suggestion' and 'theme_hook_suggestions'. These will be merged
- * together to form a list of 'suggested' alternate theme hooks to use, in
- * reverse order of priority. theme_hook_suggestion will always be a higher
- * priority than items in theme_hook_suggestions. theme() will use the highest
- * priority implementation that exists. If none exists, theme() will use the
- * implementation for the theme hook it was called with. These suggestions are
- * similar to, and are used for similar reasons as, calling theme() with an
- * array as the $hook parameter (see below). The difference is whether the
- * suggestions are determined by the code that calls theme() or by a preprocess
- * function.
+ * Alternate hooks can be suggested by implementing the hook-specific
+ * hook_theme_suggestions_HOOK_alter() or the generic
+ * hook_theme_suggestions_alter(). These alter hooks are used to manipulate an
+ * array of suggested alternate theme hooks to use, in reverse order of
+ * priority. theme() will use the highest priority implementation that exists.
+ * If none exists, theme() will use the implementation for the theme hook it was
+ * called with. These suggestions are similar to and are used for similar
+ * reasons as calling theme() with an array as the $hook parameter (see below).
+ * The difference is whether the suggestions are determined by the code that
+ * calls theme() or by altering the suggestions via the suggestion alter hooks.
  *
  * @param $hook
  *   The name of the theme hook to call. If the name contains a
@@ -1002,11 +1001,39 @@ function theme($hook, $variables = array()) {
     'theme_hook_original' => $original_hook,
   );
 
-  // Invoke the variable preprocessors, if any. The preprocessors may specify
-  // alternate suggestions for which hook's template/function to use. If the
-  // hook is a suggestion of a base hook, invoke the variable preprocessors of
-  // the base hook, but retain the suggestion as a high priority suggestion to
-  // be used unless overridden by a variable preprocessor function.
+  // Set base hook for use with suggestions alter hooks. This way if e.g.
+  // '#theme' => 'node__article' is called, we run
+  // hook_theme_suggestions_node_alter() rather than
+  // hook_theme_suggestions_node__article_alter(), and also pass in the base
+  // hook as the last parameter to the suggestions alter hooks.
+  if (isset($info['base hook'])) {
+    $suggestions_base_hook = $info['base hook'];
+  }
+  else {
+    $suggestions_base_hook = $hook;
+  }
+
+  // Invoke theme hook suggestion alter hooks.
+  $suggestions = array();
+  $suggestion_hooks = array(
+    'theme_suggestions',
+    'theme_suggestions_' . $suggestions_base_hook,
+  );
+  Drupal::moduleHandler()->alter($suggestion_hooks, $suggestions, $variables, $suggestions_base_hook);
+
+  // Check if each suggestion exists in the theme registry, and if so,
+  // use it instead of the hook that theme() was called with. For example, a
+  // function may call theme('node', ...), but a module can add
+  // 'node__article' as a suggestion via hook_theme_suggestions_HOOK_alter(),
+  // enabling a theme to have an alternate template file for article nodes.
+  foreach (array_reverse($suggestions) as $suggestion) {
+    if (isset($hooks[$suggestion])) {
+      $info = $hooks[$suggestion];
+      break;
+    }
+  }
+
+  // Invoke the variable preprocessors, if any.
   if (isset($info['base hook'])) {
     $base_hook = $info['base hook'];
     $base_hook_info = $hooks[$base_hook];
@@ -1017,44 +1044,19 @@ function theme($hook, $variables = array()) {
         include_once DRUPAL_ROOT . '/' . $include_file;
       }
     }
+    // Previously we just replaced the entire $info array with $base_hook_info
+    // if any preprocess functions were set, but now we determine the
+    // suggestions first and then fire the appropriate preprocess functions.
     if (isset($base_hook_info['preprocess functions'])) {
-      $variables['theme_hook_suggestion'] = $hook;
-      $hook = $base_hook;
-      $info = $base_hook_info;
+      $info['preprocess functions'] = $base_hook_info['preprocess functions'];
     }
   }
   if (isset($info['preprocess functions'])) {
-    $variables['theme_hook_suggestions'] = array();
     foreach ($info['preprocess functions'] as $preprocessor_function) {
       if (function_exists($preprocessor_function)) {
         $preprocessor_function($variables, $hook, $info);
       }
     }
-    // If the preprocess functions specified hook suggestions, and the
-    // suggestion exists in the theme registry, use it instead of the hook that
-    // theme() was called with. This allows the preprocess step to route to a
-    // more specific theme hook. For example, a function may call
-    // theme('node', ...), but a preprocess function can add 'node__article' as
-    // a suggestion, enabling a theme to have an alternate template file for
-    // article nodes. Suggestions are checked in the following order:
-    // - The 'theme_hook_suggestion' variable is checked first. It overrides
-    //   all others.
-    // - The 'theme_hook_suggestions' variable is checked in FILO order, so the
-    //   last suggestion added to the array takes precedence over suggestions
-    //   added earlier.
-    $suggestions = array();
-    if (!empty($variables['theme_hook_suggestions'])) {
-      $suggestions = $variables['theme_hook_suggestions'];
-    }
-    if (!empty($variables['theme_hook_suggestion'])) {
-      $suggestions[] = $variables['theme_hook_suggestion'];
-    }
-    foreach (array_reverse($suggestions) as $suggestion) {
-      if (isset($hooks[$suggestion])) {
-        $info = $hooks[$suggestion];
-        break;
-      }
-    }
   }
 
   // Generate the output using either a function or a template.
@@ -1117,6 +1119,7 @@ function theme($hook, $variables = array()) {
     if (isset($info['path'])) {
       $template_file = $info['path'] . '/' . $template_file;
     }
+    $variables['theme_hook_suggestions'] = $suggestions;
     $output = $render_function($template_file, $variables);
   }
 
@@ -2596,11 +2599,6 @@ function template_preprocess_html(&$variables) {
     drupal_add_html_head($element, $name);
   }
 
-  // Populate the page template suggestions.
-  if ($suggestions = theme_get_suggestions(arg(), 'html')) {
-    $variables['theme_hook_suggestions'] = $suggestions;
-  }
-
   drupal_add_library('system', 'html5shiv', TRUE);
 
   // Render page_top and page_bottom into top level variables.
@@ -2703,11 +2701,6 @@ function template_preprocess_page(&$variables) {
     $variables['node'] = $node;
   }
 
-  // Populate the page template suggestions.
-  if ($suggestions = theme_get_suggestions(arg(), 'page')) {
-    $variables['theme_hook_suggestions'] = $suggestions;
-  }
-
   // Prepare render array for messages. drupal_get_messages() is called later,
   // when this variable is rendered in a theme function or template file.
   $variables['messages'] = array(
@@ -2728,9 +2721,10 @@ function template_preprocess_page(&$variables) {
 /**
  * Generate an array of suggestions from path arguments.
  *
- * This is typically called for adding to the 'theme_hook_suggestions' or
- * 'attributes' class key variables from within preprocess functions, when
- * wanting to base the additional suggestions on the path of the current page.
+ * This is typically called for adding to the suggestions in
+ * hook_theme_suggestions_HOOK_alter() or adding to 'attributes' class key
+ * variables from within preprocess functions, when wanting to base the
+ * additional suggestions or classes on the path of the current page.
  *
  * @param $args
  *   An array of path arguments, such as from function arg().
@@ -2744,9 +2738,8 @@ function template_preprocess_page(&$variables) {
  *
  * @return
  *   An array of suggestions, suitable for adding to
- *   $variables['theme_hook_suggestions'] within a preprocess function or to
- *   $variables['attributes']['class'] if the suggestions represent extra CSS
- *   classes.
+ *   hook_theme_suggestions_HOOK_alter() or to $variables['attributes']['class']
+ *   if the suggestions represent extra CSS classes.
  */
 function theme_get_suggestions($args, $base, $delimiter = '__') {
 
@@ -2908,12 +2901,6 @@ function template_preprocess_maintenance_page(&$variables) {
     $variables['attributes']['class'][] = 'sidebar-' . $variables['layout'];
   }
 
-  // Dead databases will show error messages so supplying this template will
-  // allow themers to override the page and the content completely.
-  if (isset($variables['db_is_active']) && !$variables['db_is_active']) {
-    $variables['theme_hook_suggestion'] = 'maintenance_page__offline';
-  }
-
   $variables['head'] = drupal_get_html_head();
 
   // While this code is used in the installer, the language module may not be
@@ -2966,7 +2953,6 @@ function template_preprocess_region(&$variables) {
 
   $variables['attributes']['class'][] = 'region';
   $variables['attributes']['class'][] = drupal_html_class('region-' . $variables['region']);
-  $variables['theme_hook_suggestions'][] = 'region__' . $variables['region'];
 }
 
 /**
diff --git a/core/modules/block/block.module b/core/modules/block/block.module
index 8eeb57f..c14fe4b 100644
--- a/core/modules/block/block.module
+++ b/core/modules/block/block.module
@@ -495,6 +495,35 @@ function block_rebuild() {
 }
 
 /**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function block_theme_suggestions_block_alter(array &$suggestions, array $variables) {
+  $suggestions[] = 'block__' . $variables['elements']['#configuration']['module'];
+  // Hyphens (-) and underscores (_) play a special role in theme suggestions.
+  // Theme suggestions should only contain underscores, because within
+  // drupal_find_theme_templates(), underscores are converted to hyphens to
+  // match template file names, and then converted back to underscores to match
+  // pre-processing and other function names. So if your theme suggestion
+  // contains a hyphen, it will end up as an underscore after this conversion,
+  // and your function names won't be recognized. So, we need to convert
+  // hyphens to underscores in block deltas for the theme suggestions.
+
+  // We can safely explode on : because we know the Block plugin type manager
+  // enforces that delimiter for all derivatives.
+  $parts = explode(':', $variables['elements']['#plugin_id']);
+  $suggestion = 'block';
+  while ($part = array_shift($parts)) {
+    $suggestions[] = $suggestion .= '__' . strtr($part, '-', '_');
+  }
+
+  if ($id = $variables['elements']['#block']->id()) {
+    $config_id = explode('.', $id);
+    $machine_name = array_pop($config_id);
+    $suggestions[] = 'block__' . $machine_name;
+  }
+}
+
+/**
  * Prepares variables for block templates.
  *
  * Default template: block.html.twig.
@@ -537,29 +566,11 @@ function template_preprocess_block(&$variables) {
   // Add default class for block content.
   $variables['content_attributes']['class'][] = 'content';
 
-  $variables['theme_hook_suggestions'][] = 'block__' . $variables['configuration']['module'];
-  // Hyphens (-) and underscores (_) play a special role in theme suggestions.
-  // Theme suggestions should only contain underscores, because within
-  // drupal_find_theme_templates(), underscores are converted to hyphens to
-  // match template file names, and then converted back to underscores to match
-  // pre-processing and other function names. So if your theme suggestion
-  // contains a hyphen, it will end up as an underscore after this conversion,
-  // and your function names won't be recognized. So, we need to convert
-  // hyphens to underscores in block deltas for the theme suggestions.
-
-  // We can safely explode on : because we know the Block plugin type manager
-  // enforces that delimiter for all derivatives.
-  $parts = explode(':', $variables['plugin_id']);
-  $suggestion = 'block';
-  while ($part = array_shift($parts)) {
-    $variables['theme_hook_suggestions'][] = $suggestion .= '__' . strtr($part, '-', '_');
-  }
   // Create a valid HTML ID and make sure it is unique.
   if ($id = $variables['elements']['#block']->id()) {
     $config_id = explode('.', $id);
     $machine_name = array_pop($config_id);
     $variables['attributes']['id'] = drupal_html_id('block-' . $machine_name);
-    $variables['theme_hook_suggestions'][] = 'block__' . $machine_name;
   }
 }
 
diff --git a/core/modules/block/lib/Drupal/block/Tests/BlockPreprocessUnitTest.php b/core/modules/block/lib/Drupal/block/Tests/BlockPreprocessUnitTest.php
new file mode 100644
index 0000000..1bfcc62
--- /dev/null
+++ b/core/modules/block/lib/Drupal/block/Tests/BlockPreprocessUnitTest.php
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\block\Tests\BlockPreprocessUnitTest.
+ */
+
+namespace Drupal\block\Tests;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Unit tests for template_preprocess_block().
+ */
+class BlockPreprocessUnitTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('block');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Block preprocess',
+      'description' => 'Test the template_preprocess_block() function.',
+      'group' => 'Block',
+    );
+  }
+
+  /**
+   * Tests block classes with template_preprocess_block().
+   */
+  function testBlockClasses() {
+    // Define a block with a derivative to be preprocessed, which includes both
+    // an underscore (not transformed) and a hyphen (transformed to underscore),
+    // and generates possibilities for each level of derivative.
+    // @todo Clarify this comment.
+    $block = entity_create('block', array(
+      'plugin' => 'system_menu_block:menu-admin',
+      'region' => 'footer',
+      'id' => \Drupal::config('system.theme')->get('default') . '.machinename',
+    ));
+
+    $variables = array();
+    $variables['elements']['#block'] = $block;
+    $variables['elements']['#configuration'] = $block->getPlugin()->getConfiguration();
+    $variables['elements']['#plugin_id'] = $block->get('plugin');
+    $variables['elements']['content'] = array();
+    $variables['content_attributes']['class'][] = 'test-class';
+    template_preprocess_block($variables);
+    $this->assertEqual($variables['content_attributes']['class'], array('test-class', 'content'), 'Default .content class added to block content_attributes');
+  }
+
+}
diff --git a/core/modules/block/lib/Drupal/block/Tests/BlockTemplateSuggestionsUnitTest.php b/core/modules/block/lib/Drupal/block/Tests/BlockTemplateSuggestionsUnitTest.php
index 3042b5b..edc62f6 100644
--- a/core/modules/block/lib/Drupal/block/Tests/BlockTemplateSuggestionsUnitTest.php
+++ b/core/modules/block/lib/Drupal/block/Tests/BlockTemplateSuggestionsUnitTest.php
@@ -10,7 +10,7 @@
 use Drupal\simpletest\WebTestBase;
 
 /**
- * Unit tests for template_preprocess_block().
+ * Unit tests for block_theme_suggestions_block_alter().
  */
 class BlockTemplateSuggestionsUnitTest extends WebTestBase {
 
@@ -24,13 +24,13 @@ class BlockTemplateSuggestionsUnitTest extends WebTestBase {
   public static function getInfo() {
     return array(
       'name' => 'Block template suggestions',
-      'description' => 'Test the template_preprocess_block() function.',
+      'description' => 'Test the block_theme_suggestions_block_alter() function.',
       'group' => 'Block',
     );
   }
 
   /**
-   * Test if template_preprocess_block() handles the suggestions right.
+   * Tests template suggestions from block_theme_suggestions_block_alter().
    */
   function testBlockThemeHookSuggestions() {
     // Define a block with a derivative to be preprocessed, which includes both
@@ -43,16 +43,14 @@ function testBlockThemeHookSuggestions() {
       'id' => config('system.theme')->get('default') . '.machinename',
     ));
 
+    $suggestions = array();
     $variables = array();
     $variables['elements']['#block'] = $block;
     $variables['elements']['#configuration'] = $block->getPlugin()->getConfiguration();
     $variables['elements']['#plugin_id'] = $block->get('plugin');
     $variables['elements']['content'] = array();
-    // Test adding a class to the block content.
-    $variables['content_attributes']['class'][] = 'test-class';
-    template_preprocess_block($variables);
-    $this->assertEqual($variables['theme_hook_suggestions'], array('block__system', 'block__system_menu_block', 'block__system_menu_block__menu_admin', 'block__machinename'));
-    $this->assertEqual($variables['content_attributes']['class'], array('test-class', 'content'), 'Default .content class added to block content_attributes');
+    block_theme_suggestions_block_alter($suggestions, $variables);
+    $this->assertEqual($suggestions, array('block__system', 'block__system_menu_block', 'block__system_menu_block__menu_admin', 'block__machinename'));
   }
 
 }
diff --git a/core/modules/field/field.module b/core/modules/field/field.module
index 3203f2e..2cb3ffa 100644
--- a/core/modules/field/field.module
+++ b/core/modules/field/field.module
@@ -918,6 +918,18 @@ function field_page_build(&$page) {
 }
 
 /**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function field_theme_suggestions_field_alter(array &$suggestions, array $variables) {
+  $element = $variables['element'];
+
+  $suggestions[] = 'field__' . $element['#field_type'];
+  $suggestions[] = 'field__' . $element['#field_name'];
+  $suggestions[] = 'field__' . $element['#bundle'];
+  $suggestions[] = 'field__' . $element['#field_name'] . '__' . $element['#bundle'];
+}
+
+/**
  * Prepares variables for field templates.
  *
  * Default template: field.html.twig.
@@ -971,14 +983,6 @@ function template_preprocess_field(&$variables, $hook) {
     $variables['attributes']['class'][] = 'clearfix';
   }
 
-  // Add specific suggestions that can override the default implementation.
-  $variables['theme_hook_suggestions'] = array(
-    'field__' . $element['#field_type'],
-    'field__' . $element['#field_name'],
-    'field__' . $element['#bundle'],
-    'field__' . $element['#field_name'] . '__' . $element['#bundle'],
-  );
-
   static $default_attributes;
   if (!isset($default_attributes)) {
     $default_attributes = new Attribute;
diff --git a/core/modules/forum/forum.module b/core/modules/forum/forum.module
index 4621ddd..4bc8890 100644
--- a/core/modules/forum/forum.module
+++ b/core/modules/forum/forum.module
@@ -955,6 +955,28 @@ function forum_preprocess_block(&$variables) {
 }
 
 /**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function forum_theme_suggestions_forums_alter(array &$suggestions, array $variables) {
+  // Provide separate template suggestions based on what's being output. Topic
+  // ID is also accounted for. Check both variables to be safe then the inverse.
+  // Forums with topic IDs take precedence.
+  if ($variables['forums'] && !$variables['topics']) {
+    $suggestions[] = 'forums__containers';
+    $suggestions[] = 'forums__' . $variables['tid'];
+    $suggestions[] = 'forums__containers__' . $variables['tid'];
+  }
+  elseif (!$variables['forums'] && $variables['topics']) {
+    $suggestions[] = 'forums__topics';
+    $suggestions[] = 'forums__' . $variables['tid'];
+    $suggestions[] = 'forums__topics__' . $variables['tid'];
+  }
+  else {
+    $suggestions[] = 'forums__' . $variables['tid'];
+  }
+}
+
+/**
  * Prepares variables for forums templates.
  *
  * Default template: forums.html.twig.
@@ -1000,23 +1022,6 @@ function template_preprocess_forums(&$variables) {
     else {
       $variables['topics'] = array();
     }
-
-    // Provide separate template suggestions based on what's being output. Topic id is also accounted for.
-    // Check both variables to be safe then the inverse. Forums with topic ID's take precedence.
-    if ($variables['forums'] && !$variables['topics']) {
-      $variables['theme_hook_suggestions'][] = 'forums__containers';
-      $variables['theme_hook_suggestions'][] = 'forums__' . $variables['tid'];
-      $variables['theme_hook_suggestions'][] = 'forums__containers__' . $variables['tid'];
-    }
-    elseif (!$variables['forums'] && $variables['topics']) {
-      $variables['theme_hook_suggestions'][] = 'forums__topics';
-      $variables['theme_hook_suggestions'][] = 'forums__' . $variables['tid'];
-      $variables['theme_hook_suggestions'][] = 'forums__topics__' . $variables['tid'];
-    }
-    else {
-      $variables['theme_hook_suggestions'][] = 'forums__' . $variables['tid'];
-    }
-
   }
   else {
     $variables['forums'] = array();
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index 26f11a2..599781a 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -667,6 +667,16 @@ function node_preprocess_block(&$variables) {
 }
 
 /**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function node_theme_suggestions_node_alter(array &$suggestions, array $variables) {
+  $node = $variables['elements']['#node'];
+
+  $suggestions[] = 'node__' . $node->bundle();
+  $suggestions[] = 'node__' . $node->id();
+}
+
+/**
  * Prepares variables for node templates.
  *
  * Default template: node.html.twig.
@@ -755,11 +765,6 @@ function template_preprocess_node(&$variables) {
   if (isset($variables['preview'])) {
     $variables['attributes']['class'][] = 'preview';
   }
-
-  // Clean up name so there are no underscores.
-  $variables['theme_hook_suggestions'][] = 'node__' . $node->bundle();
-  $variables['theme_hook_suggestions'][] = 'node__' . $node->id();
-
   $variables['content_attributes']['class'][] = 'content';
 }
 
diff --git a/core/modules/search/search.pages.inc b/core/modules/search/search.pages.inc
index a39a815..0ca1d8f 100644
--- a/core/modules/search/search.pages.inc
+++ b/core/modules/search/search.pages.inc
@@ -75,6 +75,13 @@ function search_view($module = NULL, $keys = '') {
 }
 
 /**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function search_theme_suggestions_search_results_alter(array &$suggestions, array $variables) {
+  $suggestions[] = 'search_results__' . $variables['module'];
+}
+
+/**
  * Prepares variables for search results templates.
  *
  * Default template: search-results.html.twig.
@@ -101,7 +108,13 @@ function template_preprocess_search_results(&$variables) {
   // @todo Revisit where this help text is added, see also
   //   http://drupal.org/node/1918856.
   $variables['help'] = search_help('search#noresults', drupal_help_arg());
-  $variables['theme_hook_suggestions'][] = 'search_results__' . $variables['module'];
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function search_theme_suggestions_search_result_alter(array &$suggestions, array $variables) {
+  $suggestions[] = 'search_result__' . $variables['module'];
 }
 
 /**
@@ -150,7 +163,6 @@ function template_preprocess_search_result(&$variables) {
   // Provide separated and grouped meta information..
   $variables['info_split'] = $info;
   $variables['info'] = implode(' - ', $info);
-  $variables['theme_hook_suggestions'][] = 'search_result__' . $variables['module'];
 }
 
 /**
diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeSuggestionsAlterTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeSuggestionsAlterTest.php
new file mode 100644
index 0000000..7faa77c
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeSuggestionsAlterTest.php
@@ -0,0 +1,97 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Theme\ThemeSuggestionsAlterTest.
+ */
+
+namespace Drupal\system\Tests\Theme;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests theme suggestion alter hooks.
+ */
+class ThemeSuggestionsAlterTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('theme_test');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Theme suggestions alter',
+      'description' => 'Test theme suggestion alter hooks.',
+      'group' => 'Theme',
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+    theme_enable(array('test_theme'));
+  }
+
+  /**
+   * Tests that theme suggestion alter hooks work for templates.
+   */
+  function testTemplateSuggestionsAlter() {
+    $this->drupalGet('theme-test/suggestion-alter');
+    $this->assertText('Original template.');
+
+    // Enable test_theme and test that themes can alter template suggestions.
+    config('system.theme')
+      ->set('default', 'test_theme')
+      ->save();
+    $this->drupalGet('theme-test/suggestion-alter');
+    $this->assertText('Template overridden based on new theme suggestion provided by the test_theme theme.');
+
+    // Enable the theme_suggestions_test module to test modules implementing
+    // suggestions alter hooks.
+    module_enable(array('theme_suggestions_test'));
+    $this->drupalGet('theme-test/suggestion-alter');
+    $this->assertText('Template overridden based on new theme suggestion provided by a module.');
+  }
+
+  /**
+   * Tests that theme suggestion alter hooks work for specific theme calls.
+   */
+  function testSpecificSuggestionsAlter() {
+    config('system.theme')
+      ->set('default', 'test_theme')
+      ->save();
+
+    // Test a specific theme call similar to '#theme' => 'node__article'.
+    $this->drupalGet('theme-test/specific-suggestion-alter');
+    $this->assertText('Template for testing specific theme calls.');
+
+    // Ensure that the base hook is used to determine the suggestion alter hook.
+    module_enable(array('theme_suggestions_test'));
+    $this->drupalGet('theme-test/specific-suggestion-alter');
+    $this->assertText('Template overridden based on suggestion alter hook determined by the base hook.');
+  }
+
+  /**
+   * Tests that theme suggestion alter hooks work for theme functions.
+   */
+  function testThemeFunctionSuggestionsAlter() {
+    $this->drupalGet('theme-test/function-suggestion-alter');
+    $this->assertText('Original theme function.');
+
+    // Enable test_theme and test that themes can alter theme suggestions.
+    config('system.theme')
+      ->set('default', 'test_theme')
+      ->save();
+    $this->drupalGet('theme-test/function-suggestion-alter');
+    $this->assertText('Theme function overridden based on new theme suggestion provided by the test_theme theme.');
+
+    // Enable the theme_suggestions_test module to test modules implementing
+    // suggestions alter hooks.
+    module_enable(array('theme_suggestions_test'));
+    $this->drupalGet('theme-test/function-suggestion-alter');
+    $this->assertText('Theme function overridden based on new theme suggestion provided by a module.');
+  }
+
+}
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 1935f5c..41c5dc2 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -968,6 +968,48 @@ function system_menu() {
 }
 
 /**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function system_theme_suggestions_html_alter(array &$suggestions, array $variables) {
+  $suggestions = array_merge($suggestions, theme_get_suggestions(arg(), 'html'));
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function system_theme_suggestions_page_alter(array &$suggestions, array $variables) {
+  $suggestions = array_merge($suggestions, theme_get_suggestions(arg(), 'page'));
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function system_theme_suggestions_maintenance_page_alter(array &$suggestions, array $variables) {
+  // Dead databases will show error messages so supplying this template will
+  // allow themers to override the page and the content completely.
+  $offline = defined('MAINTENANCE_MODE');
+  try {
+    drupal_is_front_page();
+  }
+  catch (Exception $e) {
+    // The database is not yet available.
+    $offline = TRUE;
+  }
+  if ($offline) {
+    $suggestions[] = 'maintenance_page__offline';
+  }
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function system_theme_suggestions_region_alter(array &$suggestions, array $variables) {
+  if (!empty($variables['elements']['#region'])) {
+    $suggestions[] = 'region__' . $variables['elements']['#region'];
+  }
+}
+
+/**
  * Theme callback for the default batch page.
  */
 function _system_batch_theme() {
diff --git a/core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.info.yml b/core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.info.yml
new file mode 100644
index 0000000..95f429f
--- /dev/null
+++ b/core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.info.yml
@@ -0,0 +1,7 @@
+name: 'Theme suggestions test'
+type: module
+description: 'Support module for testing theme suggestions.'
+package: Testing
+version: VERSION
+core: 8.x
+hidden: true
diff --git a/core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.module b/core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.module
new file mode 100644
index 0000000..58f9cdb
--- /dev/null
+++ b/core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.module
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * @file
+ * Support module for testing theme suggestions.
+ */
+
+/**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function theme_suggestions_test_theme_suggestions_theme_test_suggestions_alter(array &$suggestions, array $variables) {
+  $suggestions[] = 'theme_test_suggestions__' . 'module_override';
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function theme_suggestions_test_theme_suggestions_theme_test_function_suggestions_alter(array &$suggestions, array $variables) {
+  $suggestions[] = 'theme_test_function_suggestions__' . 'module_override';
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function theme_suggestions_test_theme_suggestions_theme_test_specific_suggestions_alter(array &$suggestions, array $variables, $hook) {
+  $suggestions[] = $hook . '__variant__foo';
+}
diff --git a/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/ThemeTestController.php b/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/ThemeTestController.php
index c790a84..add8edd 100644
--- a/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/ThemeTestController.php
+++ b/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/ThemeTestController.php
@@ -31,4 +31,25 @@ function functionTemplateOverridden() {
     );
   }
 
+  /**
+   * Menu callback for testing suggestion alter hooks with template files.
+   */
+  function suggestionAlter() {
+    return array('#theme' => 'theme_test_suggestions');
+  }
+
+  /**
+   * Menu callback for testing suggestion alter hooks with specific suggestions.
+   */
+  function specificSuggestionAlter() {
+    return array('#theme' => 'theme_test_specific_suggestions__variant');
+  }
+
+  /**
+   * Menu callback for testing suggestion alter hooks with theme functions.
+   */
+  function functionSuggestionAlter() {
+    return array('#theme' => 'theme_test_function_suggestions');
+  }
+
 }
diff --git a/core/modules/system/tests/modules/theme_test/templates/theme-test-suggestions--specific.html.twig b/core/modules/system/tests/modules/theme_test/templates/theme-test-suggestions--specific.html.twig
new file mode 100644
index 0000000..6e112fd
--- /dev/null
+++ b/core/modules/system/tests/modules/theme_test/templates/theme-test-suggestions--specific.html.twig
@@ -0,0 +1,2 @@
+{# Output for Theme API test #}
+Template for testing specific theme calls.
diff --git a/core/modules/system/tests/modules/theme_test/templates/theme-test-suggestions.html.twig b/core/modules/system/tests/modules/theme_test/templates/theme-test-suggestions.html.twig
new file mode 100644
index 0000000..dfc848c
--- /dev/null
+++ b/core/modules/system/tests/modules/theme_test/templates/theme-test-suggestions.html.twig
@@ -0,0 +1,2 @@
+{# Output for Theme API test #}
+Original template.
diff --git a/core/modules/system/tests/modules/theme_test/theme_test.module b/core/modules/system/tests/modules/theme_test/theme_test.module
index 51cb6d3..19f1ff5 100644
--- a/core/modules/system/tests/modules/theme_test/theme_test.module
+++ b/core/modules/system/tests/modules/theme_test/theme_test.module
@@ -14,6 +14,17 @@ function theme_test_theme($existing, $type, $theme, $path) {
   $items['theme_test_template_test_2'] = array(
     'template' => 'theme_test.template_test',
   );
+  $items['theme_test_specific_suggestions'] = array(
+    'template' => 'theme-test-specific-suggestions',
+    'variables' => array(),
+  );
+  $items['theme_test_suggestions'] = array(
+    'template' => 'theme-test-suggestions',
+    'variables' => array(),
+  );
+  $items['theme_test_function_suggestions'] = array(
+    'variables' => array(),
+  );
   $items['theme_test_foo'] = array(
     'variables' => array('foo' => NULL),
   );
@@ -52,6 +63,18 @@ function theme_test_menu() {
     'theme callback' => '_theme_custom_theme',
     'type' => MENU_CALLBACK,
   );
+  $items['theme-test/suggestion-alter'] = array(
+    'title' => 'Test suggestion alter hook for template files',
+    'route_name' => 'suggestion_alter',
+  );
+  $items['theme-test/specific-suggestion-alter'] = array(
+    'title' => 'Test suggestion alter hook for specific theme suggestions',
+    'route_name' => 'specific_suggestion_alter',
+  );
+  $items['theme-test/function-suggestion-alter'] = array(
+    'title' => 'Test suggestion alter hook for theme functions',
+    'route_name' => 'function_suggestion_alter',
+  );
   $items['theme-test/alter'] = array(
     'title' => 'Suggestion',
     'page callback' => '_theme_test_alter',
@@ -197,3 +220,10 @@ function template_preprocess_theme_test_render_element(&$variables) {
 function theme_theme_test_render_element_children($variables) {
   return drupal_render($variables['element']);
 }
+
+/**
+ * Returns HTML for a theme function suggestion test.
+ */
+function theme_theme_test_function_suggestions($variables) {
+  return 'Original theme function.';
+}
diff --git a/core/modules/system/tests/modules/theme_test/theme_test.routing.yml b/core/modules/system/tests/modules/theme_test/theme_test.routing.yml
index 866171e..1f3d8af 100644
--- a/core/modules/system/tests/modules/theme_test/theme_test.routing.yml
+++ b/core/modules/system/tests/modules/theme_test/theme_test.routing.yml
@@ -4,3 +4,21 @@ function_template_override:
     _content: '\Drupal\theme_test\ThemeTestController::functionTemplateOverridden'
   requirements:
     _permission: 'access content'
+suggestion_alter:
+  pattern: '/theme-test/suggestion-alter'
+  defaults:
+    _content: '\Drupal\theme_test\ThemeTestController::suggestionAlter'
+  requirements:
+    _permission: 'access content'
+specific_suggestion_alter:
+  pattern: '/theme-test/specific-suggestion-alter'
+  defaults:
+    _content: '\Drupal\theme_test\ThemeTestController::specificSuggestionAlter'
+  requirements:
+    _permission: 'access content'
+function_suggestion_alter:
+  pattern: '/theme-test/function-suggestion-alter'
+  defaults:
+    _content: '\Drupal\theme_test\ThemeTestController::functionSuggestionAlter'
+  requirements:
+    _permission: 'access content'
diff --git a/core/modules/system/tests/themes/test_theme/templates/theme-test-specific-suggestions--variant--foo.html.twig b/core/modules/system/tests/themes/test_theme/templates/theme-test-specific-suggestions--variant--foo.html.twig
new file mode 100644
index 0000000..f0de75d
--- /dev/null
+++ b/core/modules/system/tests/themes/test_theme/templates/theme-test-specific-suggestions--variant--foo.html.twig
@@ -0,0 +1,2 @@
+{# Output for Theme API test #}
+Template overridden based on suggestion alter hook determined by the base hook.
diff --git a/core/modules/system/tests/themes/test_theme/templates/theme-test-specific-suggestions--variant.html.twig b/core/modules/system/tests/themes/test_theme/templates/theme-test-specific-suggestions--variant.html.twig
new file mode 100644
index 0000000..6e112fd
--- /dev/null
+++ b/core/modules/system/tests/themes/test_theme/templates/theme-test-specific-suggestions--variant.html.twig
@@ -0,0 +1,2 @@
+{# Output for Theme API test #}
+Template for testing specific theme calls.
diff --git a/core/modules/system/tests/themes/test_theme/templates/theme-test-suggestions--module-override.html.twig b/core/modules/system/tests/themes/test_theme/templates/theme-test-suggestions--module-override.html.twig
new file mode 100644
index 0000000..26ce57b
--- /dev/null
+++ b/core/modules/system/tests/themes/test_theme/templates/theme-test-suggestions--module-override.html.twig
@@ -0,0 +1,2 @@
+{# Output for Theme API test #}
+Template overridden based on new theme suggestion provided by a module.
diff --git a/core/modules/system/tests/themes/test_theme/templates/theme-test-suggestions--theme-override.html.twig b/core/modules/system/tests/themes/test_theme/templates/theme-test-suggestions--theme-override.html.twig
new file mode 100644
index 0000000..dee829f
--- /dev/null
+++ b/core/modules/system/tests/themes/test_theme/templates/theme-test-suggestions--theme-override.html.twig
@@ -0,0 +1,2 @@
+{# Output for Theme API test #}
+Template overridden based on new theme suggestion provided by the test_theme theme.
diff --git a/core/modules/system/tests/themes/test_theme/test_theme.theme b/core/modules/system/tests/themes/test_theme/test_theme.theme
index 62b5abf..9b10b2b 100644
--- a/core/modules/system/tests/themes/test_theme/test_theme.theme
+++ b/core/modules/system/tests/themes/test_theme/test_theme.theme
@@ -29,3 +29,43 @@ function test_theme_theme_test__suggestion($variables) {
 function test_theme_theme_test_alter_alter(&$data) {
   $data = 'test_theme_theme_test_alter_alter was invoked';
 }
+
+/**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function test_theme_theme_suggestions_theme_test_suggestions_alter(array &$suggestions, array $variables) {
+  // Theme alter hooks run after module alter hooks, so add this theme
+  // suggestion to the beginning of the array so that the suggestion added by
+  // the theme_suggestions_test module can be picked up when that module is
+  // enabled.
+  array_unshift($suggestions, 'theme_test_suggestions__' . 'theme_override');
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function test_theme_theme_suggestions_theme_test_function_suggestions_alter(array &$suggestions, array $variables) {
+  // Theme alter hooks run after module alter hooks, so add this theme
+  // suggestion to the beginning of the array so that the suggestion added by
+  // the theme_suggestions_test module can be picked up when that module is
+  // enabled.
+  array_unshift($suggestions, 'theme_test_function_suggestions__' . 'theme_override');
+}
+
+/**
+ * Returns HTML for a theme function suggestion test.
+ *
+ * Implements the theme_test_function_suggestions__theme_override suggestion.
+ */
+function test_theme_theme_test_function_suggestions__theme_override($variables) {
+  return 'Theme function overridden based on new theme suggestion provided by the test_theme theme.';
+}
+
+/**
+ * Returns HTML for a theme function suggestion test.
+ *
+ * Implements the theme_test_function_suggestions__module_override suggestion.
+ */
+function test_theme_theme_test_function_suggestions__module_override($variables) {
+  return 'Theme function overridden based on new theme suggestion provided by a module.';
+}
diff --git a/core/modules/system/theme.api.php b/core/modules/system/theme.api.php
index ebda671..4ee2afb 100644
--- a/core/modules/system/theme.api.php
+++ b/core/modules/system/theme.api.php
@@ -158,6 +158,44 @@ function hook_preprocess_HOOK(&$variables) {
 }
 
 /**
+ * Alters theme suggestions.
+ *
+ * Provide alternative theme function or template suggestions for theme hooks.
+ *
+ * @param array $suggestions
+ *   An array of theme suggestions.
+ * @param array $variables
+ *   An array of variables passed to the theme hook. Note that this hook is
+ *   invoked before any preprocessing.
+ * @param string $hook
+ *   The theme hook being called.
+ *
+ * @see hook_theme_suggestions_HOOK_alter()
+ */
+function hook_theme_suggestions_alter(array &$suggestions, array $variables, $hook) {
+  if ($hook == 'node') {
+    $suggestions[] = 'node__' . $variables['elements']['#langcode'];
+  }
+}
+
+/**
+ * Alters theme suggestions for a specific theme hook.
+ *
+ * @param array $suggestions
+ *   An array of theme suggestions.
+ * @param array $variables
+ *   An array of variables passed to the theme hook. Note that this hook is
+ *   invoked before any preprocessing.
+ *
+ * @see hook_theme_suggestions_alter()
+ */
+function hook_theme_suggestions_HOOK_alter(array &$suggestions, array $variables) {
+  if (empty($variables['header'])) {
+    $suggestions[] = 'hookname__' . 'no_header';
+  }
+}
+
+/**
  * Respond to themes being enabled.
  *
  * @param array $theme_list
diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module
index 2167270..b2c8bea 100644
--- a/core/modules/taxonomy/taxonomy.module
+++ b/core/modules/taxonomy/taxonomy.module
@@ -435,6 +435,16 @@ function taxonomy_term_view_multiple(array $terms, $view_mode = 'full', $langcod
 }
 
 /**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function taxonomy_theme_suggestions_taxonomy_term_alter(array &$suggestions, array $variables) {
+  $term = $variables['elements']['#term'];
+
+  $suggestions[] = 'taxonomy_term__' . $term->bundle();
+  $suggestions[] = 'taxonomy_term__' . $term->id();
+}
+
+/**
  * Prepares variables for taxonomy term templates.
  *
  * Default template: taxonomy-term.html.twig.
@@ -475,9 +485,6 @@ function template_preprocess_taxonomy_term(&$variables) {
   $variables['attributes']['class'][] = 'taxonomy-term';
   $vocabulary_name_css = str_replace('_', '-', $term->bundle());
   $variables['attributes']['class'][] = 'vocabulary-' . $vocabulary_name_css;
-
-  $variables['theme_hook_suggestions'][] = 'taxonomy_term__' . $term->bundle();
-  $variables['theme_hook_suggestions'][] = 'taxonomy_term__' . $term->id();
 }
 
 /**
diff --git a/core/modules/views/views.module b/core/modules/views/views.module
index 4b90f77..e568b91 100644
--- a/core/modules/views/views.module
+++ b/core/modules/views/views.module
@@ -242,15 +242,13 @@ function views_preprocess_node(&$variables) {
   // \Drupal\views\Plugin\views\row\EntityRow::preRender().
   if (!empty($variables['node']->view) && $variables['node']->view->storage->id()) {
     $variables['view'] = $variables['node']->view;
-    $variables['theme_hook_suggestions'][] = 'node__view__' . $variables['node']->view->storage->id();
-    if (!empty($variables['node']->view->current_display)) {
-      $variables['theme_hook_suggestions'][] = 'node__view__' . $variables['node']->view->storage->id() . '__' . $variables['node']->view->current_display;
-
-      // If a node is being rendered in a view, and the view does not have a path,
-      // prevent drupal from accidentally setting the $page variable:
-      if ($variables['page'] && $variables['view_mode'] == 'full' && !$variables['view']->display_handler->hasPath()) {
-        $variables['page'] = FALSE;
-      }
+    // If a node is being rendered in a view, and the view does not have a path,
+    // prevent drupal from accidentally setting the $page variable:
+    if (!empty($variables['view']->current_display)
+        && $variables['page']
+        && $variables['view_mode'] == 'full'
+        && !$variables['view']->display_handler->hasPath()) {
+      $variables['page'] = FALSE;
     }
   }
 
@@ -261,6 +259,19 @@ function views_preprocess_node(&$variables) {
 }
 
 /**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function views_theme_suggestions_node_alter(array &$suggestions, array $variables) {
+  $node = $variables['elements']['#node'];
+  if (!empty($node->view) && $node->view->storage->id()) {
+    $suggestions[] = 'node__view__' . $node->view->storage->id();
+    if (!empty($node->view->current_display)) {
+      $suggestions[] = 'node__view__' . $node->view->storage->id() . '__' . $node->view->current_display;
+    }
+  }
+}
+
+/**
  * A theme preprocess function to automatically allow view-based node
  * templates if called from a view.
  */
@@ -269,9 +280,18 @@ function views_preprocess_comment(&$variables) {
   // \Drupal\views\Plugin\views\row\EntityRow::preRender().
   if (!empty($variables['comment']->view) && $variables['comment']->view->storage->id()) {
     $variables['view'] = &$variables['comment']->view;
-    $variables['theme_hook_suggestions'][] = 'comment__view__' . $variables['comment']->view->storage->id();
-    if (!empty($variables['node']->view->current_display)) {
-      $variables['theme_hook_suggestions'][] = 'comment__view__' . $variables['comment']->view->storage->id() . '__' . $variables['comment']->view->current_display;
+  }
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function views_theme_suggestions_comment_alter(array &$suggestions, array $variables) {
+  $comment = $variables['elements']['#comment'];
+  if (!empty($comment->view) && $comment->view->storage->id()) {
+    $suggestions[] = 'comment__view__' . $comment->view->storage->id();
+    if (!empty($comment->view->current_display)) {
+      $suggestions[] = 'comment__view__' . $comment->view->storage->id() . '__' . $comment->view->current_display;
     }
   }
 }
diff --git a/core/modules/views_ui/views_ui.theme.inc b/core/modules/views_ui/views_ui.theme.inc
index acd4573..d4f5dcb 100644
--- a/core/modules/views_ui/views_ui.theme.inc
+++ b/core/modules/views_ui/views_ui.theme.inc
@@ -473,5 +473,11 @@ function template_preprocess_views_ui_view_preview_section(&$variables) {
     );
     $variables['links'] = $build;
   }
-  $variables['theme_hook_suggestions'][] = 'views_ui_view_preview_section__' . $variables['section'];
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK_alter().
+ */
+function views_ui_theme_suggestions_views_ui_view_preview_section_alter(array &$suggestions, array $variables) {
+  $suggestions[] = 'views_ui_view_preview_section__' . $variables['section'];
 }
