diff --git a/core/core.services.yml b/core/core.services.yml
index 680c23b808..0ff3b35c9c 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -528,7 +528,7 @@ services:
     arguments: ['@app.root', '@config.factory', '@module_handler', '@state', '@info_parser']
   theme_installer:
     class: Drupal\Core\Extension\ThemeInstaller
-    arguments: ['@theme_handler', '@config.factory', '@config.installer', '@module_handler', '@config.manager', '@asset.css.collection_optimizer', '@router.builder', '@logger.channel.default', '@state']
+    arguments: ['@theme_handler', '@config.factory', '@config.installer', '@module_handler', '@config.manager', '@asset.css.collection_optimizer', '@router.builder', '@logger.channel.default', '@state', '@module_installer']
   # @deprecated in Drupal 8.0.x and will be removed before 9.0.0. Use the other
   #   entity* services instead.
   entity.manager:
diff --git a/core/lib/Drupal/Core/Extension/ThemeHandler.php b/core/lib/Drupal/Core/Extension/ThemeHandler.php
index d54ff1fa1a..b478c1a26a 100644
--- a/core/lib/Drupal/Core/Extension/ThemeHandler.php
+++ b/core/lib/Drupal/Core/Extension/ThemeHandler.php
@@ -278,6 +278,7 @@ public function rebuildThemeData() {
       'screenshot' => 'screenshot.png',
       'php' => DRUPAL_MINIMUM_PHP,
       'libraries' => [],
+      'dependencies' => []
     ];
 
     $sub_themes = [];
@@ -364,6 +365,17 @@ public function rebuildThemeData() {
       }
     }
 
+    foreach ($themes as $key => $theme) {
+      // buildModuleDependencies() adds a theme->requires array that contains
+      // both module and base theme dependencies, if they are specified. Ensure
+      // that every theme stores the list of module dependencies separately
+      // from the full requires list.
+      if (!isset($theme->requires)) {
+        $theme->requires = [];
+      }
+      $themes[$key]->module_dependencies = isset($theme->base_themes) ? array_diff_key($theme->requires, $theme->base_themes) : $theme->requires;
+    }
+
     return $themes;
   }
 
diff --git a/core/lib/Drupal/Core/Extension/ThemeInstaller.php b/core/lib/Drupal/Core/Extension/ThemeInstaller.php
index 43a5469e3f..4a963c2d00 100644
--- a/core/lib/Drupal/Core/Extension/ThemeInstaller.php
+++ b/core/lib/Drupal/Core/Extension/ThemeInstaller.php
@@ -10,6 +10,7 @@
 use Drupal\Core\Routing\RouteBuilderInterface;
 use Drupal\Core\State\StateInterface;
 use Psr\Log\LoggerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Manages theme installation/uninstallation.
@@ -61,6 +62,13 @@ class ThemeInstaller implements ThemeInstallerInterface {
    */
   protected $logger;
 
+  /**
+   * The module installer used to install modules depended on by themes.
+   *
+   * @var \Drupal\Core\Extension\ModuleInstallerInterface
+   */
+  protected $moduleInstaller;
+
   /**
    * Constructs a new ThemeInstaller.
    *
@@ -85,8 +93,10 @@ class ThemeInstaller implements ThemeInstallerInterface {
    *   A logger instance.
    * @param \Drupal\Core\State\StateInterface $state
    *   The state store.
+   * @param \Drupal\Core\Extension\ModuleInstallerInterface $module_installer
+   *   The module installer.
    */
-  public function __construct(ThemeHandlerInterface $theme_handler, ConfigFactoryInterface $config_factory, ConfigInstallerInterface $config_installer, ModuleHandlerInterface $module_handler, ConfigManagerInterface $config_manager, AssetCollectionOptimizerInterface $css_collection_optimizer, RouteBuilderInterface $route_builder, LoggerInterface $logger, StateInterface $state) {
+  public function __construct(ThemeHandlerInterface $theme_handler, ConfigFactoryInterface $config_factory, ConfigInstallerInterface $config_installer, ModuleHandlerInterface $module_handler, ConfigManagerInterface $config_manager, AssetCollectionOptimizerInterface $css_collection_optimizer, RouteBuilderInterface $route_builder, LoggerInterface $logger, StateInterface $state, ModuleInstallerInterface $module_installer = NULL) {
     $this->themeHandler = $theme_handler;
     $this->configFactory = $config_factory;
     $this->configInstaller = $config_installer;
@@ -96,15 +106,23 @@ public function __construct(ThemeHandlerInterface $theme_handler, ConfigFactoryI
     $this->routeBuilder = $route_builder;
     $this->logger = $logger;
     $this->state = $state;
+
+    // @todo https://www.drupal.org/node/2971389 Remove this in Drupal 9.0.0.
+    if ($module_installer === NULL) {
+      @trigger_error('Not supplying the $module_installer parameter is deprecated since version 8.6.0 and will be a required parameter in Drupal 9.0.0. Supply the $module_installer parameter. See https://www.drupal.org/node/2971389.', E_USER_DEPRECATED);
+      $module_installer = \Drupal::service('module_installer');
+    }
+
+    $this->moduleInstaller = $module_installer;
   }
 
   /**
    * {@inheritdoc}
    */
   public function install(array $theme_list, $install_dependencies = TRUE) {
-    $extension_config = $this->configFactory->getEditable('core.extension');
 
     $theme_data = $this->themeHandler->rebuildThemeData();
+    $installed_themes = $this->configFactory->get('core.extension')->get('theme') ?: [];
 
     if ($install_dependencies) {
       $theme_list = array_combine($theme_list, $theme_list);
@@ -115,16 +133,21 @@ public function install(array $theme_list, $install_dependencies = TRUE) {
       }
 
       // Only process themes that are not installed currently.
-      $installed_themes = $extension_config->get('theme') ?: [];
       if (!$theme_list = array_diff_key($theme_list, $installed_themes)) {
         // Nothing to do. All themes already installed.
         return TRUE;
       }
 
       foreach ($theme_list as $theme => $value) {
-        // Add dependencies to the list. The new themes will be processed as
-        // the parent foreach loop continues.
-        foreach (array_keys($theme_data[$theme]->requires) as $dependency) {
+        $module_dependencies = $theme_data[$theme]->module_dependencies;
+        $theme_dependencies = array_diff_key($theme_data[$theme]->requires, $module_dependencies);
+
+        // Install the module dependencies.
+        $this->moduleInstaller->install(array_keys($module_dependencies));
+
+        // Add dependencies to the list of themes to install. The new themes
+        // will be processed as the parent foreach loop continues.
+        foreach (array_keys($theme_dependencies) as $dependency) {
           if (!isset($theme_data[$dependency])) {
             // The dependency does not exist.
             return FALSE;
@@ -146,12 +169,13 @@ public function install(array $theme_list, $install_dependencies = TRUE) {
       arsort($theme_list);
       $theme_list = array_keys($theme_list);
     }
-    else {
-      $installed_themes = $extension_config->get('theme') ?: [];
-    }
 
     $themes_installed = [];
     foreach ($theme_list as $key) {
+      // Update all the dependency injected services.
+      $this->updateDependencies(\Drupal::getContainer());
+      $extension_config = $this->configFactory->getEditable('core.extension');
+
       // Only process themes that are not already installed.
       $installed = $extension_config->get("theme.$key") !== NULL;
       if ($installed) {
@@ -301,4 +325,31 @@ protected function systemListReset() {
     system_list_reset();
   }
 
+  /**
+   * Updates an object's external dependencies from the container.
+   *
+   * This method depends on \Drupal\Core\DependencyInjection\Container::get()
+   * adding the _serviceId property to all services.
+   *
+   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
+   *   The container.
+   *
+   * @see \Drupal\Core\DependencyInjection\Container
+   *
+   * @todo https://www.drupal.org/node/2380293 Remove this method and instead
+   *   add the trait that supplies this method from that issue.
+   */
+  protected function updateDependencies(ContainerInterface $container) {
+    $vars = get_object_vars($this);
+    foreach ($vars as $key => $value) {
+      if (is_object($value) && isset($value->_serviceId)) {
+        $this->$key = $container->get($value->_serviceId);
+      }
+      // Special case the container, which might not have a service ID.
+      elseif ($value instanceof ContainerInterface) {
+        $this->$key = $container;
+      }
+    }
+  }
+
 }
diff --git a/core/modules/system/src/Controller/SystemController.php b/core/modules/system/src/Controller/SystemController.php
index 42b50d0691..81e9d39606 100644
--- a/core/modules/system/src/Controller/SystemController.php
+++ b/core/modules/system/src/Controller/SystemController.php
@@ -10,6 +10,7 @@
 use Drupal\Core\Menu\MenuTreeParameters;
 use Drupal\Core\Theme\ThemeAccessCheck;
 use Drupal\Core\Url;
+use Drupal\system\Form\ModulesListForm;
 use Drupal\system\SystemManager;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -190,6 +191,7 @@ public function themesPage() {
     $theme_groups  = ['installed' => [], 'uninstalled' => []];
     $admin_theme = $config->get('admin');
     $admin_theme_options = [];
+    $modules = [];
 
     foreach ($themes as &$theme) {
       if (!empty($theme->info['hidden'])) {
@@ -232,9 +234,40 @@ public function themesPage() {
         $theme->incompatible_base = (isset($theme->info['base theme']) && !($theme->base_themes === array_filter($theme->base_themes)));
         // Confirm that the theme engine is available.
         $theme->incompatible_engine = isset($theme->info['engine']) && !isset($theme->owner);
+        // Confirm that module dependencies are available.
+        $theme->incompatible_module = FALSE;
       }
+
+      // Check module dependencies.
+      if ($theme->module_dependencies) {
+        if (empty($modules)) {
+          $modules = system_rebuild_module_data();
+        }
+        foreach ($theme->module_dependencies as $dependency => $version) {
+          if ($incompatible = ModulesListForm::checkDependencyMessage($modules, $dependency, $version)) {
+            $theme->module_dependencies[$dependency] = $incompatible;
+            $theme->incompatible_module = TRUE;
+            continue;
+          }
+          // Only display visible modules.
+          elseif (!empty($modules[$dependency]->hidden)) {
+            unset($theme->module_dependencies[$dependency]);
+            continue;
+          }
+          else {
+            $name = $modules[$dependency]->info['name'];
+            if ($modules[$dependency]->status) {
+              $theme->module_dependencies[$dependency] = $this->t('@module', ['@module' => $name]);
+            }
+            else {
+              $theme->module_dependencies[$dependency] = $this->t('@module (<span class="admin-disabled">disabled</span>)', ['@module' => $name]);
+            }
+          }
+        }
+      }
+
       $theme->operations = [];
-      if (!empty($theme->status) || !$theme->incompatible_core && !$theme->incompatible_php && !$theme->incompatible_base && !$theme->incompatible_engine) {
+      if (!empty($theme->status) || !$theme->incompatible_core && !$theme->incompatible_php && !$theme->incompatible_base && !$theme->incompatible_engine && !$theme->incompatible_module) {
         // Create the operations links.
         $query['theme'] = $theme->getName();
         if ($this->themeAccess->checkAccess($theme->getName())) {
diff --git a/core/modules/system/src/Controller/ThemeController.php b/core/modules/system/src/Controller/ThemeController.php
index 31be59ed1a..db66e71e16 100644
--- a/core/modules/system/src/Controller/ThemeController.php
+++ b/core/modules/system/src/Controller/ThemeController.php
@@ -6,6 +6,7 @@
 use Drupal\Core\Config\PreExistingConfigException;
 use Drupal\Core\Config\UnmetDependenciesException;
 use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\DrupalKernelInterface;
 use Drupal\Core\Extension\ThemeHandlerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
@@ -23,6 +24,13 @@ class ThemeController extends ControllerBase {
    */
   protected $themeHandler;
 
+  /**
+   * The Drupal kernel.
+   *
+   * @var \Drupal\Core\DrupalKernelInterface
+   */
+  protected $kernel;
+
   /**
    * Constructs a new ThemeController.
    *
@@ -30,10 +38,13 @@ class ThemeController extends ControllerBase {
    *   The theme handler.
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The config factory.
+   * @param \Drupal\Core\DrupalKernelInterface $kernel
+   *   The Drupal kernel.
    */
-  public function __construct(ThemeHandlerInterface $theme_handler, ConfigFactoryInterface $config_factory) {
+  public function __construct(ThemeHandlerInterface $theme_handler, ConfigFactoryInterface $config_factory, DrupalKernelInterface $kernel) {
     $this->themeHandler = $theme_handler;
     $this->configFactory = $config_factory;
+    $this->kernel = $kernel;
   }
 
   /**
@@ -42,7 +53,8 @@ public function __construct(ThemeHandlerInterface $theme_handler, ConfigFactoryI
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('theme_handler'),
-      $container->get('config.factory')
+      $container->get('config.factory'),
+      $container->get('kernel')
     );
   }
 
@@ -75,6 +87,7 @@ public function uninstall(Request $request) {
         }
         else {
           $this->themeHandler->uninstall([$theme]);
+          $this->updateDependencies(\Drupal::getContainer());
           $this->messenger()->addStatus($this->t('The %theme theme has been uninstalled.', ['%theme' => $themes[$theme]->info['name']]));
         }
       }
@@ -106,9 +119,28 @@ public function install(Request $request) {
 
     if (isset($theme)) {
       try {
+        // Get an up-to-date list of modules in the system.
+        $previously_installed_modules = $this->moduleHandler()->getModuleList();
         if ($this->themeHandler->install([$theme])) {
-          $themes = $this->themeHandler->listInfo();
-          $this->messenger()->addStatus($this->t('The %theme theme has been installed.', ['%theme' => $themes[$theme]->info['name']]));
+          $this->updateDependencies(\Drupal::getContainer());
+          $theme_data = $this->themeHandler->listInfo();
+          if ($theme_data[$theme]->module_dependencies) {
+            $module_data = system_rebuild_module_data();
+            $newly_installed_modules = array_diff_key($theme_data[$theme]->module_dependencies, $previously_installed_modules);
+            $newly_installed_modules_names = array_map(function ($module_name) use ($module_data) {
+              return $module_data[$module_name]->info['name'];
+            }, array_keys($newly_installed_modules));
+          }
+          if (!empty($newly_installed_modules)) {
+            $this->messenger()->addStatus($this->formatPlural(count($newly_installed_modules_names), 'The %theme theme and its module dependency, %name, have been installed.', 'The %theme theme and its @count module dependencies have been installed: %names.', [
+              '%theme' => $theme_data[$theme]->info['name'],
+              '%name' => $newly_installed_modules_names[0],
+              '%names' => implode(', ', $newly_installed_modules_names),
+            ]));
+          }
+          else {
+            $this->messenger()->addStatus($this->t('The %theme theme has been installed.', ['%theme' => $theme_data[$theme]->info['name']]));
+          }
         }
         else {
           $this->messenger()->addError($this->t('The %theme theme was not found.', ['%theme' => $theme]));
@@ -190,4 +222,31 @@ public function setDefaultTheme(Request $request) {
     throw new AccessDeniedHttpException();
   }
 
+  /**
+   * Updates an object's external dependencies from the container.
+   *
+   * This method depends on \Drupal\Core\DependencyInjection\Container::get()
+   * adding the _serviceId property to all services.
+   *
+   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
+   *   The container.
+   *
+   * @see \Drupal\Core\DependencyInjection\Container
+   *
+   * @todo https://www.drupal.org/node/2380293 Remove this method and instead
+   *   add the trait that supplies this method from that issue.
+   */
+  protected function updateDependencies(ContainerInterface $container) {
+    $vars = get_object_vars($this);
+    foreach ($vars as $key => $value) {
+      if (is_object($value) && isset($value->_serviceId)) {
+        $this->$key = $container->get($value->_serviceId);
+      }
+      // Special case the container, which might not have a service ID.
+      elseif ($value instanceof ContainerInterface) {
+        $this->$key = $container;
+      }
+    }
+  }
+
 }
diff --git a/core/modules/system/src/Form/ModulesListForm.php b/core/modules/system/src/Form/ModulesListForm.php
index 3c6f2a8991..525e5a5fd5 100644
--- a/core/modules/system/src/Form/ModulesListForm.php
+++ b/core/modules/system/src/Form/ModulesListForm.php
@@ -190,11 +190,47 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     return $form;
   }
 
+  /**
+   * Determines a message for missing/invalid theme/module dependencies.
+   *
+   * @param array $modules
+   *   The list of existing modules.
+   * @param string $dependency
+   *   The module dependency to check.
+   * @param array $version
+   *   Version requirement data from the module or theme declaring the
+   *   dependency we are checking.
+   *
+   * @return string|null
+   *   NULL if compatible, otherwise a string describing the incompatibility.
+   */
+  public static function checkDependencyMessage(array $modules, $dependency, $version) {
+    if (!isset($modules[$dependency])) {
+      return t('@module (<span class="admin-missing">missing</span>)', ['@module' => Unicode::ucfirst($dependency)]);
+    }
+    else {
+      $name = $modules[$dependency]->info['name'];
+      // Check if it is incompatible with the dependency's version.
+      if ($incompatible_version = drupal_check_incompatibility($version, str_replace(\Drupal::CORE_COMPATIBILITY . '-', '', $modules[$dependency]->info['version']))) {
+        return t('@module (<span class="admin-missing">incompatible with</span> version @version)', [
+          '@module' => $name . $incompatible_version,
+          '@version' => $modules[$dependency]->info['version'],
+        ]);
+      }
+      // Ensure that incompatible modules cannot be installed.
+      if ($modules[$dependency]->info['core'] != \Drupal::CORE_COMPATIBILITY) {
+        return t('@module (<span class="admin-missing">incompatible with</span> this version of Drupal core)', [
+          '@module' => $name,
+        ]);
+      }
+    }
+  }
+
   /**
    * Builds a table row for the system modules page.
    *
    * @param array $modules
-   *   The list existing modules.
+   *   The list of existing modules.
    * @param \Drupal\Core\Extension\Extension $module
    *   The module for which to build the form row.
    * @param $distribution
@@ -301,35 +337,20 @@ protected function buildRow(array $modules, Extension $module, $distribution) {
 
     // If this module requires other modules, add them to the array.
     foreach ($module->requires as $dependency => $version) {
-      if (!isset($modules[$dependency])) {
-        $row['#requires'][$dependency] = $this->t('@module (<span class="admin-missing">missing</span>)', ['@module' => Unicode::ucfirst($dependency)]);
-        $row['enable']['#disabled'] = TRUE;
-      }
-      // Only display visible modules.
-      elseif (empty($modules[$dependency]->hidden)) {
-        $name = $modules[$dependency]->info['name'];
-        // Disable the module's checkbox if it is incompatible with the
-        // dependency's version.
-        if ($incompatible_version = drupal_check_incompatibility($version, str_replace(\Drupal::CORE_COMPATIBILITY . '-', '', $modules[$dependency]->info['version']))) {
-          $row['#requires'][$dependency] = $this->t('@module (<span class="admin-missing">incompatible with</span> version @version)', [
-            '@module' => $name . $incompatible_version,
-            '@version' => $modules[$dependency]->info['version'],
-          ]);
-          $row['enable']['#disabled'] = TRUE;
-        }
-        // Disable the checkbox if the dependency is incompatible with this
-        // version of Drupal core.
-        elseif ($modules[$dependency]->info['core'] != \Drupal::CORE_COMPATIBILITY) {
-          $row['#requires'][$dependency] = $this->t('@module (<span class="admin-missing">incompatible with</span> this version of Drupal core)', [
-            '@module' => $name,
-          ]);
+      // Only display missing or visible modules.
+      if (empty($modules[$dependency]->hidden)) {
+        if ($incompatible = static::checkDependencyMessage($modules, $dependency, $version)) {
+          $row['#requires'][$dependency] = $incompatible;
           $row['enable']['#disabled'] = TRUE;
         }
-        elseif ($modules[$dependency]->status) {
-          $row['#requires'][$dependency] = $this->t('@module', ['@module' => $name]);
-        }
         else {
-          $row['#requires'][$dependency] = $this->t('@module (<span class="admin-disabled">disabled</span>)', ['@module' => $name]);
+          $name = $modules[$dependency]->info['name'];
+          if ($modules[$dependency]->status) {
+            $row['#requires'][$dependency] = $this->t('@module', ['@module' => $name]);
+          }
+          else {
+            $row['#requires'][$dependency] = $this->t('@module (<span class="admin-disabled">disabled</span>)', ['@module' => $name]);
+          }
         }
       }
     }
diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc
index 799869bbc3..13bbd07fa7 100644
--- a/core/modules/system/system.admin.inc
+++ b/core/modules/system/system.admin.inc
@@ -165,6 +165,8 @@ function template_preprocess_system_modules_details(&$variables) {
       ];
       $module['requires'] = $renderer->render($requires);
     }
+    // @TODO: Add theme dependencies, see
+    //   https://www.drupal.org/project/drupal/issues/2937952
     if (!empty($module['#required_by'])) {
       $required_by = [
         '#theme' => 'item_list',
@@ -290,6 +292,15 @@ function template_preprocess_system_themes_page(&$variables) {
       $current_theme['is_default'] = $theme->is_default;
       $current_theme['is_admin'] = $theme->is_admin;
 
+      $current_theme['module_dependencies'] = [];
+      if (!empty($theme->module_dependencies)) {
+        $current_theme['module_dependencies'] = [
+          '#theme' => 'item_list',
+          '#items' => $theme->module_dependencies,
+          '#context' => ['list_style' => 'comma-list'],
+        ];
+      }
+
       // Make sure to provide feedback on compatibility.
       $current_theme['incompatible'] = '';
       if (!empty($theme->incompatible_core)) {
@@ -310,6 +321,9 @@ function template_preprocess_system_themes_page(&$variables) {
       elseif (!empty($theme->incompatible_engine)) {
         $current_theme['incompatible'] = t('This theme requires the theme engine @theme_engine to operate correctly.', ['@theme_engine' => $theme->info['engine']]);
       }
+      elseif (!empty($theme->incompatible_module)) {
+        $current_theme['incompatible'] = t('This theme requires the listed modules to operate correctly.');
+      }
 
       // Build operation links.
       $current_theme['operations'] = [
diff --git a/core/modules/system/templates/system-themes-page.html.twig b/core/modules/system/templates/system-themes-page.html.twig
index 6e65d7641b..6916151f22 100644
--- a/core/modules/system/templates/system-themes-page.html.twig
+++ b/core/modules/system/templates/system-themes-page.html.twig
@@ -22,6 +22,7 @@
  *     - notes: Identifies what context this theme is being used in, e.g.,
  *       default theme, admin theme.
  *     - incompatible: Text describing any compatibility issues.
+ *     - module_dependencies: A list of modules that this theme requires.
  *     - operations: A list of operation links, e.g., Settings, Enable, Disable,
  *       etc. these links should only be displayed if the theme is compatible.
  *
@@ -62,6 +63,9 @@
               {%- endif -%}
             </h3>
             <div class="theme-info__description">{{ theme.description }}</div>
+            {% if theme.module_dependencies %}
+              <div class="theme-info__requires">Requires: {{ theme.module_dependencies }}</div>
+            {% endif %}
             {# Display operation links if the theme is compatible. #}
             {% if theme.incompatible %}
               <div class="incompatible">{{ theme.incompatible }}</div>
diff --git a/core/modules/system/tests/src/Functional/Theme/ThemeUiTest.php b/core/modules/system/tests/src/Functional/Theme/ThemeUiTest.php
new file mode 100644
index 0000000000..907d6dfa38
--- /dev/null
+++ b/core/modules/system/tests/src/Functional/Theme/ThemeUiTest.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Drupal\Tests\system\Functional\Theme;
+
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests the theme UI.
+ *
+ * @group system
+ */
+class ThemeUiTest extends BrowserTestBase {
+
+  /**
+   * A user with administrative permissions.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $adminUser;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['node', 'block', 'file'];
+
+  protected function setUp() {
+    parent::setUp();
+
+    $this->adminUser = $this->drupalCreateUser(['access administration pages', 'view the administration theme', 'administer themes', 'bypass node access', 'administer blocks']);
+    $this->drupalLogin($this->adminUser);
+  }
+
+  /**
+   * Tests installing a theme with existing module dependencies.
+   */
+  public function testInstallModuleWithNotInstalledDependencies() {
+    $this->drupalGet('admin/appearance');
+    $themeXpath = '//h3[contains(text(), "test theme depending on modules")]';
+    $elements = $this->xpath($themeXpath);
+    $this->assertCount(1, $elements);
+    $this->getSession()->getDriver()->click('//h3[contains(text(), "test theme depending on modules")]/../ul/li[1]/a');
+    $this->assertSession()->addressEquals('admin/appearance');
+    $this->assertSession()->pageTextContains('The test theme depending on modules theme and its 2 module dependencies have been installed: test module required by theme, test another module required by theme.');
+  }
+
+  /**
+   * Tests installing a theme with existing module dependencies.
+   */
+  public function testInstallModuleWithAlreadyInstalledDependencies() {
+    $this->drupalGet('admin/appearance');
+    $themeXpath = '//h3[contains(text(), "test theme depending on already installed module")]';
+    $elements = $this->xpath($themeXpath);
+    $this->assertCount(1, $elements);
+    $this->getSession()->getDriver()->click('//h3[contains(text(), "test theme depending on already installed module")]/../ul/li[1]/a');
+    $this->assertSession()->addressEquals('admin/appearance');
+    $this->assertSession()->pageTextContains('The test theme depending on already installed module theme has been installed.');
+  }
+
+  /**
+   * Tests installing a theme with missing module dependencies.
+   */
+  public function testInstallModuleWithMissingDependencies() {
+    $this->drupalGet('admin/appearance');
+    $themeXpath = '//h3[contains(text(), "test theme depending on nonexisting module")]';
+    $elements = $this->xpath($themeXpath);
+    $this->assertCount(1, $elements);
+    $parent = $elements[0]->find('xpath', '..');
+    $this->assertContains('missing', $parent->getText());
+    $this->assertContains('This theme requires the listed modules to operate correctly.', $parent->getText());
+  }
+
+}
diff --git a/core/modules/system/tests/themes/test_theme_depending_nonexisting_module/test_theme_depending_on_nonexisting_module.info.yml b/core/modules/system/tests/themes/test_theme_depending_nonexisting_module/test_theme_depending_on_nonexisting_module.info.yml
new file mode 100644
index 0000000000..a5977f57e0
--- /dev/null
+++ b/core/modules/system/tests/themes/test_theme_depending_nonexisting_module/test_theme_depending_on_nonexisting_module.info.yml
@@ -0,0 +1,5 @@
+name: test theme depending on nonexisting module
+type: theme
+core: 8.x
+dependencies:
+  - test_module_non_existing
diff --git a/core/modules/system/tests/themes/test_theme_depending_on_already_installed_module/test_theme_depending_on_already_installed_module.info.yml b/core/modules/system/tests/themes/test_theme_depending_on_already_installed_module/test_theme_depending_on_already_installed_module.info.yml
new file mode 100644
index 0000000000..1ce77eb096
--- /dev/null
+++ b/core/modules/system/tests/themes/test_theme_depending_on_already_installed_module/test_theme_depending_on_already_installed_module.info.yml
@@ -0,0 +1,5 @@
+name: test theme depending on already installed module
+type: theme
+core: 8.x
+dependencies:
+  - node
diff --git a/core/modules/system/tests/themes/test_theme_depending_on_modules/test_another_module_required_by_theme/test_another_module_required_by_theme.info.yml b/core/modules/system/tests/themes/test_theme_depending_on_modules/test_another_module_required_by_theme/test_another_module_required_by_theme.info.yml
new file mode 100644
index 0000000000..69dae0d799
--- /dev/null
+++ b/core/modules/system/tests/themes/test_theme_depending_on_modules/test_another_module_required_by_theme/test_another_module_required_by_theme.info.yml
@@ -0,0 +1,5 @@
+name: test another module required by theme
+type: module
+core: 8.x
+package: Testing
+version: VERSION
diff --git a/core/modules/system/tests/themes/test_theme_depending_on_modules/test_module_required_by_theme/src/Service.php b/core/modules/system/tests/themes/test_theme_depending_on_modules/test_module_required_by_theme/src/Service.php
new file mode 100644
index 0000000000..3e7d7eedfa
--- /dev/null
+++ b/core/modules/system/tests/themes/test_theme_depending_on_modules/test_module_required_by_theme/src/Service.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace Drupal\test_module_required_by_theme;
+
+class Service {
+
+}
diff --git a/core/modules/system/tests/themes/test_theme_depending_on_modules/test_module_required_by_theme/test_module_required_by_theme.info.yml b/core/modules/system/tests/themes/test_theme_depending_on_modules/test_module_required_by_theme/test_module_required_by_theme.info.yml
new file mode 100644
index 0000000000..9d402c7c1d
--- /dev/null
+++ b/core/modules/system/tests/themes/test_theme_depending_on_modules/test_module_required_by_theme/test_module_required_by_theme.info.yml
@@ -0,0 +1,5 @@
+name: test module required by theme
+type: module
+core: 8.x
+package: Testing
+version: VERSION
diff --git a/core/modules/system/tests/themes/test_theme_depending_on_modules/test_module_required_by_theme/test_module_required_by_theme.services.yml b/core/modules/system/tests/themes/test_theme_depending_on_modules/test_module_required_by_theme/test_module_required_by_theme.services.yml
new file mode 100644
index 0000000000..8be2053982
--- /dev/null
+++ b/core/modules/system/tests/themes/test_theme_depending_on_modules/test_module_required_by_theme/test_module_required_by_theme.services.yml
@@ -0,0 +1,3 @@
+services:
+  test_module_required_by_theme.service:
+    class: Drupal\test_module_required_by_theme\Service
diff --git a/core/modules/system/tests/themes/test_theme_depending_on_modules/test_theme_depending_on_modules.info.yml b/core/modules/system/tests/themes/test_theme_depending_on_modules/test_theme_depending_on_modules.info.yml
new file mode 100644
index 0000000000..5fdeccd657
--- /dev/null
+++ b/core/modules/system/tests/themes/test_theme_depending_on_modules/test_theme_depending_on_modules.info.yml
@@ -0,0 +1,6 @@
+name: test theme depending on modules
+type: theme
+core: 8.x
+dependencies:
+  - test_module_required_by_theme
+  - test_another_module_required_by_theme
diff --git a/core/tests/Drupal/KernelTests/Core/Asset/LibraryDiscoveryIntegrationTest.php b/core/tests/Drupal/KernelTests/Core/Asset/LibraryDiscoveryIntegrationTest.php
index a77adfcdc1..29792d1388 100644
--- a/core/tests/Drupal/KernelTests/Core/Asset/LibraryDiscoveryIntegrationTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Asset/LibraryDiscoveryIntegrationTest.php
@@ -13,6 +13,11 @@
  */
 class LibraryDiscoveryIntegrationTest extends KernelTestBase {
 
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['system'];
+
   /**
    * The library discovery service.
    *
diff --git a/core/tests/Drupal/KernelTests/Core/Render/ElementInfoIntegrationTest.php b/core/tests/Drupal/KernelTests/Core/Render/ElementInfoIntegrationTest.php
index cd33fcb6dc..512a3bd40a 100644
--- a/core/tests/Drupal/KernelTests/Core/Render/ElementInfoIntegrationTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Render/ElementInfoIntegrationTest.php
@@ -11,6 +11,11 @@
  */
 class ElementInfoIntegrationTest extends KernelTestBase {
 
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['system'];
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/tests/Drupal/KernelTests/Core/Theme/ThemeInstallerTest.php b/core/tests/Drupal/KernelTests/Core/Theme/ThemeInstallerTest.php
index 61f647528d..3bbeec7cc9 100644
--- a/core/tests/Drupal/KernelTests/Core/Theme/ThemeInstallerTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Theme/ThemeInstallerTest.php
@@ -5,6 +5,7 @@
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\Extension\ExtensionNameLengthException;
 use Drupal\KernelTests\KernelTestBase;
+use Drupal\test_theme_dependency\Service;
 
 /**
  * Tests installing and uninstalling of themes.
@@ -350,6 +351,22 @@ public function testThemeInfoAlter() {
     $this->assertFalse(isset($system_list[$name]->info['regions']['test_region']));
   }
 
+  /**
+   * Tests that it is possible to install a theme that depends on a module.
+   */
+  public function testThemeWithModuleDependency() {
+    $this->assertFalse($this->moduleHandler()->moduleExists('test_module_required_by_theme'));
+    $this->themeInstaller()->install(['test_theme_depending_on_modules']);
+
+    // Rebuild container to make sure we access non-cached configuration.
+    $this->container->get('kernel')->rebuildContainer();
+    $this->assertTrue($this->moduleHandler()->moduleExists('test_module_required_by_theme'));
+    $this->assertTrue($this->moduleHandler()->moduleExists('test_another_module_required_by_theme'));
+
+    $service = \Drupal::service('test_module_required_by_theme.service');
+    $this->assertInstanceOf(\Drupal\test_module_required_by_theme\Service::class, $service);
+  }
+
   /**
    * Returns the theme handler service.
    *
diff --git a/core/themes/stable/templates/admin/system-themes-page.html.twig b/core/themes/stable/templates/admin/system-themes-page.html.twig
index 5a23f1a14c..a191606190 100644
--- a/core/themes/stable/templates/admin/system-themes-page.html.twig
+++ b/core/themes/stable/templates/admin/system-themes-page.html.twig
@@ -22,6 +22,7 @@
  *     - notes: Identifies what context this theme is being used in, e.g.,
  *       default theme, admin theme.
  *     - incompatible: Text describing any compatibility issues.
+ *     - module_dependencies: A list of modules that this theme requires.
  *     - operations: A list of operation links, e.g., Settings, Enable, Disable,
  *       etc. these links should only be displayed if the theme is compatible.
  *
@@ -60,6 +61,9 @@
               {%- endif -%}
             </h3>
             <div class="theme-info__description">{{ theme.description }}</div>
+            {% if theme.module_dependencies %}
+              <div class="theme-info__requires">Requires: {{ theme.module_dependencies }}</div>
+            {% endif %}
             {# Display operation links if the theme is compatible. #}
             {% if theme.incompatible %}
               <div class="incompatible">{{ theme.incompatible }}</div>
