diff --git a/core/core.services.yml b/core/core.services.yml
index 582f0bd907..0694089def 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -553,7 +553,7 @@ services:
     arguments: ['@app.root', '@config.factory', '@extension.list.theme']
   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/ModuleHandler.php b/core/lib/Drupal/Core/Extension/ModuleHandler.php
index 3b60d528bf..3639d48dbc 100644
--- a/core/lib/Drupal/Core/Extension/ModuleHandler.php
+++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php
@@ -221,6 +221,17 @@ protected function add($type, $name, $path) {
    * {@inheritdoc}
    */
   public function buildModuleDependencies(array $modules) {
+    $themes = \Drupal::state()->get('theme.list', []);
+    $modules_required_by_themes = [];
+    foreach ($themes as $theme_name => $theme) {
+      if (!empty($theme->info['dependencies'])) {
+        foreach ($theme->info['dependencies'] as $dependency) {
+          if (isset($modules[$dependency]) && $modules[$dependency]->getType() === 'module') {
+            $modules_required_by_themes[$dependency][$theme_name] = $theme;
+          }
+        }
+      }
+    }
     foreach ($modules as $module) {
       $graph[$module->getName()]['edges'] = [];
       if (isset($module->info['dependencies']) && is_array($module->info['dependencies'])) {
@@ -236,6 +247,13 @@ public function buildModuleDependencies(array $modules) {
       $modules[$module_name]->required_by = isset($data['reverse_paths']) ? $data['reverse_paths'] : [];
       $modules[$module_name]->requires = isset($data['paths']) ? $data['paths'] : [];
       $modules[$module_name]->sort = $data['weight'];
+      // This prevents uninstalling a module required by a theme via drush,
+      // but the output of the command is not great at the moment.
+      if (isset($modules_required_by_themes[$module_name])) {
+        foreach ($modules_required_by_themes[$module_name] as $theme_name => $theme) {
+          $modules[$module_name]->required_by[$theme_name] = $theme;
+        }
+      }
     }
     return $modules;
   }
diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
index 62dbb6c9b0..3efcec0766 100644
--- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php
+++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
@@ -355,6 +355,7 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
     // Get all module data so we can find dependencies and sort.
     $module_data = \Drupal::service('extension.list.module')->getList();
     $module_list = $module_list ? array_combine($module_list, $module_list) : [];
+    $theme_list  = \Drupal::state()->get('theme.list', []);
     if (array_diff_key($module_list, $module_data)) {
       // One or more of the given modules doesn't exist.
       return FALSE;
@@ -372,7 +373,7 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
       // the foreach loop continues.
       foreach ($module_list as $module => $value) {
         foreach (array_keys($module_data[$module]->required_by) as $dependent) {
-          if (!isset($module_data[$dependent])) {
+          if (!isset($module_data[$dependent]) && !isset($theme_list[$dependent])) {
             // The dependent module does not exist.
             return FALSE;
           }
diff --git a/core/lib/Drupal/Core/Extension/ThemeExtensionList.php b/core/lib/Drupal/Core/Extension/ThemeExtensionList.php
index 5722d1c2fe..32264ac78d 100644
--- a/core/lib/Drupal/Core/Extension/ThemeExtensionList.php
+++ b/core/lib/Drupal/Core/Extension/ThemeExtensionList.php
@@ -51,6 +51,7 @@ class ThemeExtensionList extends ExtensionList {
     'libraries' => [],
     'libraries_extend' => [],
     'libraries_override' => [],
+    'dependencies' => [],
   ];
 
   /**
@@ -101,6 +102,12 @@ public function __construct($root, $type, CacheBackendInterface $cache, InfoPars
 
     $this->configFactory = $config_factory;
     $this->engineList = $engine_list;
+
+    // Store a list of themes in state so other classes in Drupal\Core\Extension
+    // can access this information without recursion problems.
+    //$state->set('theme.list', $this->doList());
+    $state->set('hi.buddy', 'hibuddy');
+    $stop = 'here';
   }
 
   /**
@@ -140,6 +147,51 @@ protected function doList() {
     // sub-themes.
     $this->fillInSubThemeData($themes, $sub_themes);
 
+    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;
+  }
+
+  /**
+   * Returns a list of themes without calling moduleHandler.
+   *
+   * @return array
+   *   A list of theme extensions.
+   */
+  public function getSimpleList() {
+    // Find themes.
+    $themes = parent::doList();
+    $sub_themes = [];
+    $engines = $this->engineList->getList();
+    // Always get the freshest list of themes (rather than the already cached
+    // list in $this->installedThemes) when building the theme listing because a
+    // theme could have just been installed or uninstalled.
+    $this->installedThemes = $this->configFactory->get('core.extension')->get('theme') ?: [];
+
+    // Read info files for each theme.
+    foreach ($themes as $name => $theme) {
+      // Defaults to 'twig' (see self::defaults above).
+      $engine = $theme->info['engine'];
+      if (isset($engines[$engine])) {
+        $theme->owner = $engines[$engine]->getExtensionPathname();
+        $theme->prefix = $engines[$engine]->getName();
+      }
+      // Add this theme as a sub-theme if it has a base theme.
+      if (!empty($theme->info['base theme'])) {
+        $sub_themes[] = $name;
+      }
+      // Add status.
+      $theme->status = (int) isset($this->installedThemes[$name]);
+    }
+    $this->fillInSubThemeData($themes, $sub_themes);
     return $themes;
   }
 
diff --git a/core/lib/Drupal/Core/Extension/ThemeHandler.php b/core/lib/Drupal/Core/Extension/ThemeHandler.php
index a1f43654ab..58fb033e53 100644
--- a/core/lib/Drupal/Core/Extension/ThemeHandler.php
+++ b/core/lib/Drupal/Core/Extension/ThemeHandler.php
@@ -161,7 +161,12 @@ public function reset() {
    * {@inheritdoc}
    */
   public function rebuildThemeData() {
-    return $this->themeList->reset()->getList();
+    $theme_list = $this->themeList->reset()->getList();
+
+    // Store a list of themes in state so other classes in Drupal\Core\Extension
+    // can access this information without recursion problems.
+    \Drupal::state()->set('theme.list', $theme_list);
+    return $theme_list;
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Extension/ThemeInstaller.php b/core/lib/Drupal/Core/Extension/ThemeInstaller.php
index e1149e7b66..c745c39ef4 100644
--- a/core/lib/Drupal/Core/Extension/ThemeInstaller.php
+++ b/core/lib/Drupal/Core/Extension/ThemeInstaller.php
@@ -11,6 +11,7 @@
 use Drupal\Core\Routing\RouteBuilderInterface;
 use Drupal\Core\State\StateInterface;
 use Psr\Log\LoggerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Manages theme installation/uninstallation.
@@ -62,6 +63,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.
    *
@@ -86,8 +94,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;
@@ -97,15 +107,22 @@ 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);
@@ -116,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;
@@ -147,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) {
@@ -279,4 +302,34 @@ protected function themeRegistryRebuild() {
     drupal_theme_rebuild();
   }
 
+  /**
+   * 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);
+        continue;
+      }
+
+      // Special case the container, which might not have a service ID.
+      if ($value instanceof ContainerInterface) {
+        $this->$key = $container;
+        continue;
+      }
+    }
+  }
+
 }
diff --git a/core/modules/media/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php b/core/modules/media/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php
index 40e169cc69..17611f7072 100644
--- a/core/modules/media/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php
+++ b/core/modules/media/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php
@@ -810,8 +810,8 @@ public function testTranslationAlt() {
     $assert_session->elementExists('css', '.cke_widget_drupalmedia[aria-label="Tatou poilu hurlant"]');
     $page->pressButton('Edit media');
     $this->waitForMetadataDialog();
-    // Assert that the placeholder is set to the value of the media field's
-    // alt text.
+    // Assert that the input is set to the value of the media field's alt text.
+    $assert_session->fieldValueEquals('attributes[alt]', 'default alt');
     $assert_session->elementAttributeContains('named', ['field', 'attributes[alt]'], 'placeholder', 'texte alternatif par défaut');
 
     // Fill in the alt field in the dialog.
diff --git a/core/modules/system/src/Controller/SystemController.php b/core/modules/system/src/Controller/SystemController.php
index f27cc01eb1..7f292c87b5 100644
--- a/core/modules/system/src/Controller/SystemController.php
+++ b/core/modules/system/src/Controller/SystemController.php
@@ -4,12 +4,14 @@
 
 use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Extension\ModuleExtensionList;
 use Drupal\Core\Extension\ThemeHandlerInterface;
 use Drupal\Core\Form\FormBuilderInterface;
 use Drupal\Core\Menu\MenuLinkTreeInterface;
 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;
 
@@ -53,6 +55,13 @@ class SystemController extends ControllerBase {
    */
   protected $menuLinkTree;
 
+  /**
+   * The module extension list.
+   *
+   * @var \Drupal\Core\Extension\ModuleExtensionList
+   */
+  protected $moduleExtensionList;
+
   /**
    * Constructs a new SystemController.
    *
@@ -66,13 +75,21 @@ class SystemController extends ControllerBase {
    *   The theme handler.
    * @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_link_tree
    *   The menu link tree service.
+   * @param \Drupal\Core\Extension\ModuleExtensionList $module_extension_list
+   *   The module extension list.
    */
-  public function __construct(SystemManager $systemManager, ThemeAccessCheck $theme_access, FormBuilderInterface $form_builder, ThemeHandlerInterface $theme_handler, MenuLinkTreeInterface $menu_link_tree) {
+  public function __construct(SystemManager $systemManager, ThemeAccessCheck $theme_access, FormBuilderInterface $form_builder, ThemeHandlerInterface $theme_handler, MenuLinkTreeInterface $menu_link_tree, ModuleExtensionList $module_extension_list = NULL) {
     $this->systemManager = $systemManager;
     $this->themeAccess = $theme_access;
     $this->formBuilder = $form_builder;
     $this->themeHandler = $theme_handler;
     $this->menuLinkTree = $menu_link_tree;
+    $this->moduleExtensionList = $module_extension_list;
+    if ($module_extension_list === NULL) {
+      @trigger_error('The extension.list.module service must be passed to \Drupal\system\Controller\SystemController::__construct. It was added in Drupal 8.9.0 and will be required before Drupal 9.0.0.', E_USER_DEPRECATED);
+      $module_extension_list = \Drupal::service('extension.list.module');
+    }
+    $this->moduleExtensionList = $module_extension_list;
   }
 
   /**
@@ -84,7 +101,8 @@ public static function create(ContainerInterface $container) {
       $container->get('access_check.theme'),
       $container->get('form_builder'),
       $container->get('theme_handler'),
-      $container->get('menu.link_tree')
+      $container->get('menu.link_tree'),
+      $container->get('extension.list.module')
     );
   }
 
@@ -190,6 +208,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'])) {
@@ -231,9 +250,39 @@ 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 = $this->moduleExtensionList->getList();
+        }
+        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.
+          if (!empty($modules[$dependency]->hidden)) {
+            unset($theme->module_dependencies[$dependency]);
+            continue;
+          }
+
+          $name = $modules[$dependency]->info['name'];
+          $theme->module_dependencies[$dependency] = $modules[$dependency]->status ? $this->t('@module', ['@module' => $name]) : $this->t('@module (<span class="admin-disabled">disabled</span>)', ['@module' => $name]);
+          if (!$modules[$dependency]->status) {
+            $theme->module_dependencies_disabled[$dependency] = $name;
+          }
+        }
+      }
+
       $theme->operations = [];
-      if (!empty($theme->status) || !$theme->info['core_incompatible'] && !$theme->incompatible_php && !$theme->incompatible_base && !$theme->incompatible_engine) {
+
+      if (!empty($theme->status) || !$theme->info['core_incompatible'] && !$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())) {
@@ -273,18 +322,39 @@ public function themesPage() {
           $admin_theme_options[$theme->getName()] = $theme->info['name'] . ($theme->is_experimental ? ' (' . t('Experimental') . ')' : '');
         }
         else {
-          $theme->operations[] = [
-            'title' => $this->t('Install'),
-            'url' => Url::fromRoute('system.theme_install'),
-            'query' => $query,
-            'attributes' => ['title' => $this->t('Install @theme theme', ['@theme' => $theme->info['name']])],
-          ];
-          $theme->operations[] = [
-            'title' => $this->t('Install and set as default'),
-            'url' => Url::fromRoute('system.theme_set_default'),
-            'query' => $query,
-            'attributes' => ['title' => $this->t('Install @theme as default theme', ['@theme' => $theme->info['name']])],
-          ];
+          if (!empty($theme->module_dependencies_disabled)) {
+            $query += [
+              'modules' => array_keys($theme->module_dependencies_disabled),
+            ];
+            $theme->operations[] = [
+              'title' => $this->t('Install'),
+              'url' => Url::fromRoute('system.theme_install_confirm'),
+              'query' => $query,
+              'attributes' => ['title' => $this->t('Install @theme theme', ['@theme' => $theme->info['name']])],
+            ];
+            $theme->operations[] = [
+              'title' => $this->t('Install and set as default'),
+              'url' => Url::fromRoute('system.theme_install_confirm'),
+              'query' => $query + ['set_default' => TRUE],
+              'attributes' => ['title' => $this->t('Install @theme as default theme', ['@theme' => $theme->info['name']])],
+            ];
+          }
+          else {
+            $theme->operations[] = [
+              'title' => $this->t('Install'),
+              'url' => Url::fromRoute('system.theme_install'),
+              'query' => $query,
+              'attributes' => ['title' => $this->t('Install @theme theme', ['@theme' => $theme->info['name']])],
+            ];
+            $theme->operations[] = [
+              'title' => $this->t('Install and set as default'),
+              'url' => Url::fromRoute('system.theme_set_default'),
+              'query' => $query,
+              'attributes' => ['title' => $this->t('Install @theme as default theme', ['@theme' => $theme->info['name']])],
+            ];
+          }
+
+
         }
       }
 
diff --git a/core/modules/system/src/Controller/ThemeController.php b/core/modules/system/src/Controller/ThemeController.php
index 0a8ad31cf8..392fe5098b 100644
--- a/core/modules/system/src/Controller/ThemeController.php
+++ b/core/modules/system/src/Controller/ThemeController.php
@@ -6,7 +6,9 @@
 use Drupal\Core\Config\PreExistingConfigException;
 use Drupal\Core\Config\UnmetDependenciesException;
 use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Extension\ModuleExtensionList;
 use Drupal\Core\Extension\ThemeExtensionList;
+use Drupal\Core\DrupalKernelInterface;
 use Drupal\Core\Extension\ThemeHandlerInterface;
 use Drupal\Core\Extension\ThemeInstallerInterface;
 use Drupal\system\Form\ThemeExperimentalConfirmForm;
@@ -40,6 +42,20 @@ class ThemeController extends ControllerBase {
    */
   protected $themeInstaller;
 
+  /**
+   * The Drupal kernel.
+   *
+   * @var \Drupal\Core\DrupalKernelInterface
+   */
+  protected $kernel;
+
+  /**
+   * The module extension list.
+   *
+   * @var \Drupal\Core\Extension\ModuleExtensionList
+   */
+  protected $moduleExtensionList;
+
   /**
    * Constructs a new ThemeController.
    *
@@ -51,12 +67,22 @@ class ThemeController extends ControllerBase {
    *   The config factory.
    * @param \Drupal\Core\Extension\ThemeInstallerInterface $theme_installer
    *   The theme installer.
+   * @param \Drupal\Core\DrupalKernelInterface $kernel
+   *   The Drupal kernel.
+   * @param \Drupal\Core\Extension\ModuleExtensionList $module_extension_list
+   *   The module extension list.
    */
-  public function __construct(ThemeHandlerInterface $theme_handler, ThemeExtensionList $theme_list, ConfigFactoryInterface $config_factory, ThemeInstallerInterface $theme_installer) {
+  public function __construct(ThemeHandlerInterface $theme_handler, ThemeExtensionList $theme_list, ConfigFactoryInterface $config_factory, ThemeInstallerInterface $theme_installer, DrupalKernelInterface $kernel, ModuleExtensionList $module_extension_list = NULL) {
     $this->themeHandler = $theme_handler;
     $this->themeList = $theme_list;
     $this->configFactory = $config_factory;
     $this->themeInstaller = $theme_installer;
+    $this->kernel = $kernel;
+    if ($module_extension_list === NULL) {
+      @trigger_error('The extension.list.module service must be passed to \Drupal\system\Controller\ThemeController::__construct. It was added in Drupal 8.9.0 and will be required before Drupal 9.0.0.', E_USER_DEPRECATED);
+      $module_extension_list = \Drupal::service('extension.list.module');
+    }
+    $this->moduleExtensionList = $module_extension_list;
   }
 
   /**
@@ -67,7 +93,9 @@ public static function create(ContainerInterface $container) {
       $container->get('theme_handler'),
       $container->get('extension.list.theme'),
       $container->get('config.factory'),
-      $container->get('theme_installer')
+      $container->get('theme_installer'),
+      $container->get('kernel'),
+      $container->get('extension.list.module')
     );
   }
 
@@ -100,6 +128,7 @@ public function uninstall(Request $request) {
         }
         else {
           $this->themeInstaller->uninstall([$theme]);
+          $this->updateDependencies(\Drupal::getContainer());
           $this->messenger()->addStatus($this->t('The %theme theme has been uninstalled.', ['%theme' => $themes[$theme]->info['name']]));
         }
       }
@@ -137,9 +166,28 @@ public function install(Request $request) {
       }
 
       try {
+        // Get an up-to-date list of modules in the system.
+        $previously_installed_modules = $this->moduleHandler()->getModuleList();
         if ($this->themeInstaller->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 = $this->moduleExtensionList->getList();
+            $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]));
@@ -249,4 +297,34 @@ 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);
+        continue;
+      }
+
+      // Special case the container, which might not have a service ID.
+      if ($value instanceof ContainerInterface) {
+        $this->$key = $container;
+        continue;
+      }
+    }
+  }
+
 }
diff --git a/core/modules/system/src/Form/ModulesListForm.php b/core/modules/system/src/Form/ModulesListForm.php
index f32dad0909..67feb0c878 100644
--- a/core/modules/system/src/Form/ModulesListForm.php
+++ b/core/modules/system/src/Form/ModulesListForm.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\system\Form;
 
+use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Config\PreExistingConfigException;
 use Drupal\Core\Config\UnmetDependenciesException;
 use Drupal\Core\Access\AccessManagerInterface;
@@ -73,6 +74,8 @@ class ModulesListForm extends FormBase {
    */
   protected $moduleExtensionList;
 
+  protected $modulesRequiredByThemes = [];
+
   /**
    * {@inheritdoc}
    */
@@ -132,7 +135,6 @@ public function buildForm(array $form, FormStateInterface $form_state) {
 
     // Include system.admin.inc so we can use the sort callbacks.
     $this->moduleHandler->loadInclude('system', 'inc', 'system.admin');
-
     $form['filters'] = [
       '#type' => 'container',
       '#attributes' => [
@@ -166,6 +168,19 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       $modules = [];
     }
 
+    // Get a list of themes and determine which depend on modules.
+    $theme_list = \Drupal::service('extension.list.theme');
+    $themes = $theme_list->getList();
+    foreach ($themes as $theme_name => $theme) {
+      if (!empty($theme->info['dependencies'])) {
+        foreach ($theme->info['dependencies'] as $dependency) {
+          if (isset($modules[$dependency])) {
+            $this->modulesRequiredByThemes[$dependency][] = $theme;
+          }
+        }
+      }
+    }
+
     // Iterate over each of the modules.
     $form['modules']['#tree'] = TRUE;
     foreach ($modules as $filename => $module) {
@@ -210,11 +225,53 @@ 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 $dependency_object
+   *   Dependency object used for comparing version requirement data.
+   *
+   * @return string|null
+   *   NULL if compatible, otherwise a string describing the incompatibility.
+   */
+  public static function checkDependencyMessage(array $modules, $dependency, $dependency_object) {
+    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.
+      $version = str_replace(\Drupal::CORE_COMPATIBILITY . '-', '', $modules[$dependency]->info['version']);
+      if ($dependency === 'common_test') {
+        $stop = 'here';
+
+      }
+      if (!$dependency_object->isCompatible($version)) {
+        $constraint_string = $dependency_object->getConstraintString();
+        return t('@module (<span class="admin-missing">incompatible with</span> version @version)', [
+          '@module' => "$name ($constraint_string)",
+          '@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
@@ -326,38 +383,19 @@ protected function buildRow(array $modules, Extension $module, $distribution) {
     // If this module requires other modules, add them to the array.
     /** @var \Drupal\Core\Extension\Dependency $dependency_object */
     foreach ($module->requires as $dependency => $dependency_object) {
-      if (!isset($modules[$dependency])) {
-        $row['#requires'][$dependency] = $this->t('@module (<span class="admin-missing">missing</span>)', ['@module' => $dependency]);
-        $row['enable']['#disabled'] = TRUE;
+      // Only display missing or visible modules.
+      if (!empty($modules[$dependency]->hidden)) {
+        continue;
       }
-      // 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 (!$dependency_object->isCompatible(str_replace(\Drupal::CORE_COMPATIBILITY . '-', '', $modules[$dependency]->info['version']))) {
-          $row['#requires'][$dependency] = $this->t('@module (@constraint) (<span class="admin-missing">incompatible with</span> version @version)', [
-            '@module' => $name,
-            '@constraint' => $dependency_object->getConstraintString(),
-            '@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_incompatible']) {
-          $row['#requires'][$dependency] = $this->t('@module (<span class="admin-missing">incompatible with</span> this version of Drupal core)', [
-            '@module' => $name,
-          ]);
-          $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]);
-        }
+
+      if ($incompatible = static::checkDependencyMessage($modules, $dependency, $dependency_object)) {
+        $row['#requires'][$dependency] = $incompatible;
+        $row['enable']['#disabled'] = TRUE;
+        continue;
       }
+
+      $name = $modules[$dependency]->info['name'];
+      $row['#requires'][$dependency] = $modules[$dependency]->status ? $this->t('@module', ['@module' => $name]) : $this->t('@module (<span class="admin-disabled">disabled</span>)', ['@module' => $name]);
     }
 
     // If this module is required by other modules, list those, and then make it
@@ -373,6 +411,19 @@ protected function buildRow(array $modules, Extension $module, $distribution) {
         }
       }
     }
+    if (isset($this->modulesRequiredByThemes[$module->getName()])) {
+      foreach ($this->modulesRequiredByThemes[$module->getName()] as $theme) {
+        $theme_name = $theme->info['name'];
+        if ($theme->status === 1) {
+          $row['#required_by'][$module->getName()] = $this->t('@theme_name', ['@theme_name' => $theme_name]);
+          $row['enable']['#disabled'] = TRUE;
+        }
+        else {
+          $row['#required_by'][$module->getName()] = $this->t('@theme_name (Theme) (<span class="admin-disabled">disabled</span>)', ['@theme_name' => $theme_name]);
+        }
+
+      }
+    }
 
     return $row;
   }
diff --git a/core/modules/system/src/Form/ModulesUninstallForm.php b/core/modules/system/src/Form/ModulesUninstallForm.php
index 9370050d15..eb199bc78e 100644
--- a/core/modules/system/src/Form/ModulesUninstallForm.php
+++ b/core/modules/system/src/Form/ModulesUninstallForm.php
@@ -131,6 +131,20 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     uasort($uninstallable, 'system_sort_modules_by_info_name');
     $validation_reasons = $this->moduleInstaller->validateUninstall(array_keys($uninstallable));
 
+    $theme_list = \Drupal::service('extension.list.theme');
+    $themes = $theme_list->getList();
+
+    $modules_required_by_themes = [];
+    foreach ($themes as $theme_name => $theme) {
+      if (!empty($theme->info['dependencies'])) {
+        foreach ($theme->info['dependencies'] as $dependency) {
+          if (isset($uninstallable[$dependency]) && $theme->status === 1) {
+            $modules_required_by_themes[$dependency][] = $theme;
+          }
+        }
+      }
+    }
+
     $form['uninstall'] = ['#tree' => TRUE];
     foreach ($uninstallable as $module_key => $module) {
       $name = $module->info['name'] ?: $module->getName();
@@ -159,6 +173,15 @@ public function buildForm(array $form, FormStateInterface $form_state) {
           $form['uninstall'][$module->getName()]['#disabled'] = TRUE;
         }
       }
+      // Modules required by an active theme should not be allowed to be
+      // uninstalled.
+      if (isset($modules_required_by_themes[$module->getName()])) {
+        foreach ($modules_required_by_themes[$module->getName()] as $theme) {
+          $theme_name = $theme->getName();
+          $form['modules'][$module->getName()]['#required_by'][] = $this->t('@theme_name (Theme)', ['@theme_name' => $theme_name]);
+          $form['uninstall'][$module->getName()]['#disabled'] = TRUE;
+        }
+      }
     }
 
     $form['#attached']['library'][] = 'system/drupal.system.modules';
diff --git a/core/modules/system/src/Form/ThemeInstallConfirmForm.php b/core/modules/system/src/Form/ThemeInstallConfirmForm.php
new file mode 100644
index 0000000000..617be5eaa1
--- /dev/null
+++ b/core/modules/system/src/Form/ThemeInstallConfirmForm.php
@@ -0,0 +1,120 @@
+<?php
+
+namespace Drupal\system\Form;
+
+use Drupal\Core\Extension\ThemeExtensionList;
+use Drupal\Core\Extension\ModuleExtensionList;
+use Drupal\Core\Form\ConfirmFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Builds a confirmation form for enabling themes with module dependencies.
+ *
+ * @internal
+ */
+class ThemeInstallConfirmForm extends ConfirmFormBase {
+
+  protected $modulesToBeInstalled;
+
+  protected $theme;
+
+  protected $themeName;
+
+  protected $makeDefault;
+
+  protected $themeExtensionList;
+
+  protected $moduleExtensionList;
+
+  /**
+   * Class constructor.
+   */
+  public function __construct(ModuleExtensionList $module_extension_list, ThemeExtensionList $theme_extension_list) {
+    $this->themeExtensionList = $theme_extension_list;
+    $this->moduleExtensionList = $module_extension_list;
+    $query = $this->getRequest()->query;
+    $this->modulesToBeInstalled = $query->get('modules');
+    $this->theme = $query->get('theme');
+    $this->setDefault = $query->get('set_default');
+    $themes = $this->themeExtensionList->getList();
+    $this->themeName = $themes[$this->theme]->info['name'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('extension.list.module'),
+      $container->get('extension.list.theme')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'system_modules_confirm_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $route = $this->setDefault ? 'system.theme_set_default' : 'system.theme_install';
+    $query = ['theme' => $this->theme];
+    $this->redirect($route, [], ['query' => $query]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    $modules = $this->moduleExtensionList->getList();
+    $modules_to_be_installed = $this->modulesToBeInstalled;
+    $theme_name = $this->themeName;
+    $module_names = array_map(function ($module_key) use ($modules) {
+      return $modules[$module_key]->info['name'];
+    }, $modules_to_be_installed);
+    $module_string = implode(", ", $module_names);
+    return $this->formatPlural(count($modules_to_be_installed),
+      'Enabling the %theme_name theme will also enable the module: %module_string.',
+      'Enabling the %theme_name theme will also enable these modules: %module_string.',
+      [
+        '%theme_name' => $theme_name,
+        '%module_string' => $module_string,
+      ]
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form = parent::buildForm($form, $form_state);
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    $theme_name = $this->themeName;
+    if ($this->setDefault) {
+      return $this->t('Enable @theme_name and make it the default theme', ['@theme_name' => $theme_name]);
+    }
+    else {
+      return $this->t('Enable @theme_name', ['@theme_name' => $theme_name]);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelUrl() {
+    return Url::fromRoute('system.themes_page');
+  }
+
+}
diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc
index 661677b51a..4598ea4160 100644
--- a/core/modules/system/system.admin.inc
+++ b/core/modules/system/system.admin.inc
@@ -166,6 +166,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',
@@ -291,6 +293,12 @@ 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'] = !empty($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->info['core_incompatible'])) {
@@ -311,6 +319,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/system.routing.yml b/core/modules/system/system.routing.yml
index 9591d84d69..121b108ac0 100644
--- a/core/modules/system/system.routing.yml
+++ b/core/modules/system/system.routing.yml
@@ -305,6 +305,13 @@ system.theme_install:
     _permission: 'administer themes'
     _csrf_token: 'TRUE'
 
+system.theme_install_confirm:
+  path: '/admin/appearance/install/confirm'
+  defaults:
+    _form: '\Drupal\system\Form\ThemeInstallConfirmForm'
+  requirements:
+    _permission: 'administer themes'
+
 system.status:
   path: '/admin/reports/status'
   defaults:
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/Module/InstallUninstallTest.php b/core/modules/system/tests/src/Functional/Module/InstallUninstallTest.php
index fd84c4db7e..7888671784 100644
--- a/core/modules/system/tests/src/Functional/Module/InstallUninstallTest.php
+++ b/core/modules/system/tests/src/Functional/Module/InstallUninstallTest.php
@@ -68,7 +68,7 @@ public function testInstallUninstall() {
     $this->assertText(t('hook_modules_installed fired for help'));
     $this->assertModuleSuccessfullyInstalled('help');
 
-    // Test help for the required modules.
+    // Test help forVers the required modules.
     foreach ($required_modules as $name => $module) {
       $this->assertHelp($name, $module->info['name']);
     }
diff --git a/core/modules/system/tests/src/Functional/Theme/ThemeModuleDependenciesTest.php b/core/modules/system/tests/src/Functional/Theme/ThemeModuleDependenciesTest.php
new file mode 100644
index 0000000000..bc685ddd5b
--- /dev/null
+++ b/core/modules/system/tests/src/Functional/Theme/ThemeModuleDependenciesTest.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Drupal\Tests\system\Functional\Theme;
+
+use Drupal\Tests\BrowserTestBase;
+
+class ThemeModuleDependenciesTest extends BrowserTestBase {
+
+  protected function setUp() {
+    parent::setUp();
+
+    $user = $this->drupalCreateUser(['access administration pages', 'administer modules', 'administer themes']);
+    $this->drupalLogin($user);
+  }
+
+  public function testModuleRequiredBy() {
+    $this->drupalGet('admin/modules');
+    $stop = 'here';
+    // #module-test-module-required-by-theme .admin-requirements
+    // #module-test-another-module-required-by-theme .admin-requirements
+    // test_theme_depending_on_modules (Theme)
+  }
+}
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..adfa28b221
--- /dev/null
+++ b/core/modules/system/tests/src/Functional/Theme/ThemeUiTest.php
@@ -0,0 +1,94 @@
+<?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',
+      'administer modules',
+    ]);
+    $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());
+  }
+
+  /**
+   * Tests the module install and uninstall pages with theme dependencies.
+   */
+  public function testModuleInstallUninstall() {
+    $assert_session = $this->assertSession();
+    $this->drupalGet('admin/modules');
+    $assert_session->elementTextContains('css', '[data-drupal-selector="edit-modules-node"] .requirements', 'test theme depending on already installed module (Theme) (disabled)');
+
+    $assert_session->elementTextContains('css', '[data-drupal-selector="edit-modules-test-another-module-required-by-theme"] .requirements', 'test theme depending on modules (Theme) (disabled)');
+    $assert_session->elementTextContains('css', '[data-drupal-selector="edit-modules-test-module-required-by-theme"] .requirements', 'test theme depending on modules (Theme) (disabled)');
+
+  }
+
+}
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..c94a3a2a00
--- /dev/null
+++ b/core/modules/system/tests/themes/test_theme_depending_nonexisting_module/test_theme_depending_on_nonexisting_module.info.yml
@@ -0,0 +1,6 @@
+name: test theme depending on nonexisting module
+type: theme
+core: 8.x
+base theme: stable
+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..8c03947a89
--- /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,6 @@
+name: test theme depending on already installed module
+type: theme
+core: 8.x
+base theme: stable
+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..f01ee85ee3
--- /dev/null
+++ b/core/modules/system/tests/themes/test_theme_depending_on_modules/test_theme_depending_on_modules.info.yml
@@ -0,0 +1,7 @@
+name: test theme depending on modules
+type: theme
+core: 8.x
+base theme: stable
+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..82755a9b84 100644
--- a/core/tests/Drupal/KernelTests/Core/Asset/LibraryDiscoveryIntegrationTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Asset/LibraryDiscoveryIntegrationTest.php
@@ -13,6 +13,12 @@
  */
 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 f5b58c6445..e79c843684 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 d625c2d9f6..b20d54d8f6 100644
--- a/core/tests/Drupal/KernelTests/Core/Theme/ThemeInstallerTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Theme/ThemeInstallerTest.php
@@ -6,6 +6,7 @@
 use Drupal\Core\Extension\ExtensionNameLengthException;
 use Drupal\Core\Extension\Exception\UnknownExtensionException;
 use Drupal\KernelTests\KernelTestBase;
+use Drupal\test_module_required_by_theme\Service as ModuleRequiredByThemeService;
 
 /**
  * Tests installing and uninstalling of themes.
@@ -345,6 +346,22 @@ public function testThemeInfoAlter() {
     $this->assertFalse(isset($theme_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(ModuleRequiredByThemeService::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>
