diff --git a/core/core.services.yml b/core/core.services.yml
index c9062d5397..048c298b90 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -547,6 +547,12 @@ services:
       - { name: module_install.uninstall_validator }
     arguments: ['@string_translation', '@extension.list.module']
     lazy: true
+  module_required_by_themes_uninstall_validator:
+    class: Drupal\Core\Extension\ModuleRequiredByThemesUninstallValidator
+    tags:
+      - { name: module_install.uninstall_validator }
+    arguments: ['@string_translation', '@extension.list.module', '@extension.list.theme', '@config.factory']
+    lazy: true
   theme_handler:
     class: Drupal\Core\Extension\ThemeHandler
     arguments: ['@app.root', '@config.factory', '@extension.list.theme']
diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
index 62dbb6c9b0..235ca5b269 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::service('extension.list.theme')->getList();
     if (array_diff_key($module_list, $module_data)) {
       // One or more of the given modules doesn't exist.
       return FALSE;
@@ -372,8 +373,8 @@ 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])) {
-            // The dependent module does not exist.
+          if (!isset($module_data[$dependent]) && !isset($theme_list[$dependent])) {
+            // The dependent module or theme does not exist.
             return FALSE;
           }
 
diff --git a/core/lib/Drupal/Core/Extension/ModuleRequiredByThemesUninstallValidator.php b/core/lib/Drupal/Core/Extension/ModuleRequiredByThemesUninstallValidator.php
new file mode 100644
index 0000000000..620a8684d1
--- /dev/null
+++ b/core/lib/Drupal/Core/Extension/ModuleRequiredByThemesUninstallValidator.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace Drupal\Core\Extension;
+
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+
+/**
+ * Ensures modules required by themes cannot be uninstalled.
+ */
+class ModuleRequiredByThemesUninstallValidator implements ModuleUninstallValidatorInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The module extension list.
+   *
+   * @var \Drupal\Core\Extension\ModuleExtensionList
+   */
+  protected $moduleExtensionList;
+
+  /**
+   * The theme extension list.
+   *
+   * @var \Drupal\Core\Extension\ThemeExtensionList
+   */
+  protected $themeExtensionList;
+
+  /**
+   * Constructs a new ModuleRequiredByThemesUninstallValidator.
+   *
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
+   *   The string translation service.
+   * @param \Drupal\Core\Extension\ModuleExtensionList $extension_list_module
+   *   The module extension list.
+   * @param \Drupal\Core\Extension\ThemeExtensionList $extension_list_theme
+   *   A extension discovery instance.
+   */
+  public function __construct(TranslationInterface $string_translation, ModuleExtensionList $extension_list_module, ThemeExtensionList $extension_list_theme) {
+    $this->stringTranslation = $string_translation;
+    $this->moduleExtensionList = $extension_list_module;
+    $this->themeExtensionList = $extension_list_theme;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate($module) {
+    $reasons = [];
+
+    $modules_themes_depend_on = $this->getModulesThemesDependOn();
+    if (isset($modules_themes_depend_on[$module])) {
+      $module_name = $this->getModuleName($module);
+      $theme_names = implode(", ", $modules_themes_depend_on[$module]);
+      $reasons[] = $this->formatPlural(count($modules_themes_depend_on[$module]),
+        'The @module_name module is required by the theme @theme_names',
+        'The @module_name module is required by the themes @theme_names',
+        ['@module_name' => $module_name, '@theme_names' => $theme_names]);
+    }
+
+    return $reasons;
+  }
+
+  /**
+   * Gets the name of a module.
+   *
+   * @param string $module
+   *   The module machine name.
+   *
+   * @return string
+   *   The module name.
+   */
+  protected function getModuleName($module) {
+    return $this->moduleExtensionList->get($module)->info['name'];
+  }
+
+  /**
+   * Populate $this->modulesThemesDependOn with modules themes depend on.
+   */
+  protected function getModulesThemesDependOn() {
+    $modules_themes_depend_on = [];
+    $installed_themes = $this->themeExtensionList->getAllInstalledInfo();
+    foreach ($this->themeExtensionList->getAllInstalledInfo() as $theme) {
+      foreach ($theme['dependencies'] as $dependency) {
+        if (!isset($installed_themes[$dependency])) {
+          $modules_themes_depend_on[$dependency][] = $theme['name'];
+        }
+      }
+    }
+
+    return $modules_themes_depend_on;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Extension/ThemeExtensionList.php b/core/lib/Drupal/Core/Extension/ThemeExtensionList.php
index 5722d1c2fe..ad04156115 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' => [],
   ];
 
   /**
@@ -140,6 +141,17 @@ protected function doList() {
     // sub-themes.
     $this->fillInSubThemeData($themes, $sub_themes);
 
+    foreach ($themes as $key => $theme) {
+      // After $theme is processed by buildModuleDependencies(), there is a
+      // `$theme->requires` array containing both module and base theme
+      // dependencies. The module dependencies are copied to their own
+      // property so they are available to operations that are specific to
+      // module dependencies.
+      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 e1149e7b66..daf2e65a36 100644
--- a/core/lib/Drupal/Core/Extension/ThemeInstaller.php
+++ b/core/lib/Drupal/Core/Extension/ThemeInstaller.php
@@ -103,9 +103,8 @@ public function __construct(ThemeHandlerInterface $theme_handler, ConfigFactoryI
    * {@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 +115,18 @@ 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);
+
+        // 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,11 +148,9 @@ 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 = [];
+    $extension_config = $this->configFactory->getEditable('core.extension');
     foreach ($theme_list as $key) {
       // Only process themes that are not already installed.
       $installed = $extension_config->get("theme.$key") !== NULL;
diff --git a/core/lib/Drupal/Core/ProxyClass/Extension/ModuleRequiredByThemesUninstallValidator.php b/core/lib/Drupal/Core/ProxyClass/Extension/ModuleRequiredByThemesUninstallValidator.php
new file mode 100644
index 0000000000..601f482e4d
--- /dev/null
+++ b/core/lib/Drupal/Core/ProxyClass/Extension/ModuleRequiredByThemesUninstallValidator.php
@@ -0,0 +1,88 @@
+<?php
+// @codingStandardsIgnoreFile
+
+/**
+ * This file was generated via php core/scripts/generate-proxy-class.php 'Drupal\Core\Extension\ModuleRequiredByThemesUninstallValidator' "core/lib/Drupal/Core".
+ */
+
+namespace Drupal\Core\ProxyClass\Extension {
+
+    /**
+     * Provides a proxy class for \Drupal\Core\Extension\ModuleRequiredByThemesUninstallValidator.
+     *
+     * @see \Drupal\Component\ProxyBuilder
+     */
+    class ModuleRequiredByThemesUninstallValidator implements \Drupal\Core\Extension\ModuleUninstallValidatorInterface
+    {
+
+        use \Drupal\Core\DependencyInjection\DependencySerializationTrait;
+
+        /**
+         * The id of the original proxied service.
+         *
+         * @var string
+         */
+        protected $drupalProxyOriginalServiceId;
+
+        /**
+         * The real proxied service, after it was lazy loaded.
+         *
+         * @var \Drupal\Core\Extension\ModuleRequiredByThemesUninstallValidator
+         */
+        protected $service;
+
+        /**
+         * The service container.
+         *
+         * @var \Symfony\Component\DependencyInjection\ContainerInterface
+         */
+        protected $container;
+
+        /**
+         * Constructs a ProxyClass Drupal proxy object.
+         *
+         * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
+         *   The container.
+         * @param string $drupal_proxy_original_service_id
+         *   The service ID of the original service.
+         */
+        public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $drupal_proxy_original_service_id)
+        {
+            $this->container = $container;
+            $this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id;
+        }
+
+        /**
+         * Lazy loads the real service from the container.
+         *
+         * @return object
+         *   Returns the constructed real service.
+         */
+        protected function lazyLoadItself()
+        {
+            if (!isset($this->service)) {
+                $this->service = $this->container->get($this->drupalProxyOriginalServiceId);
+            }
+
+            return $this->service;
+        }
+
+        /**
+         * {@inheritdoc}
+         */
+        public function validate($module)
+        {
+            return $this->lazyLoadItself()->validate($module);
+        }
+
+        /**
+         * {@inheritdoc}
+         */
+        public function setStringTranslation(\Drupal\Core\StringTranslation\TranslationInterface $translation)
+        {
+            return $this->lazyLoadItself()->setStringTranslation($translation);
+        }
+
+    }
+
+}
diff --git a/core/modules/path_alias/path_alias.info.yml b/core/modules/path_alias/path_alias.info.yml
index 76bdd35a48..2a3ded6abc 100644
--- a/core/modules/path_alias/path_alias.info.yml
+++ b/core/modules/path_alias/path_alias.info.yml
@@ -3,5 +3,6 @@ type: module
 description: 'Provides the API allowing to rename URLs.'
 package: Core
 version: VERSION
+core: 8.x
 required: true
 hidden: true
diff --git a/core/modules/system/src/Controller/SystemController.php b/core/modules/system/src/Controller/SystemController.php
index f27cc01eb1..5afa3684a4 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\ModuleDependencyMessageTrait;
 use Drupal\system\SystemManager;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -18,6 +20,8 @@
  */
 class SystemController extends ControllerBase {
 
+  use ModuleDependencyMessageTrait;
+
   /**
    * System Manager Service.
    *
@@ -53,6 +57,13 @@ class SystemController extends ControllerBase {
    */
   protected $menuLinkTree;
 
+  /**
+   * The module extension list.
+   *
+   * @var \Drupal\Core\Extension\ModuleExtensionList
+   */
+  protected $moduleExtensionList;
+
   /**
    * Constructs a new SystemController.
    *
@@ -66,13 +77,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 +103,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 +210,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 +252,47 @@ 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;
+        // Confirm that the user has permission to enable modules.
+        $theme->insufficient_module_permissions = FALSE;
       }
+
+      // Check module dependencies.
+      if ($theme->module_dependencies) {
+        if (empty($modules)) {
+          $modules = $this->moduleExtensionList->getList();
+        }
+        foreach ($theme->module_dependencies as $dependency => $dependency_object) {
+          if ($incompatible = $this->checkDependencyMessage($modules, $dependency, $dependency_object)) {
+            $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;
+          }
+
+          $module_name = $modules[$dependency]->info['name'];
+          $theme->module_dependencies[$dependency] = $modules[$dependency]->status ? $this->t('@module_name', ['@module_name' => $module_name]) : $this->t('@module_name (<span class="admin-disabled">disabled</span>)', ['@module_name' => $module_name]);
+          // Create an additional property that contains only disabled module
+          // dependencies. This will determine if it is possible to install
+          // the theme, or if dependencies must first be enabled at
+          // admin/modules.
+          if (!$modules[$dependency]->status) {
+            $theme->module_dependencies_disabled[$dependency] = $module_name;
+            if (!$this->currentUser()->hasPermission('administer modules')) {
+              $theme->insufficient_module_permissions = TRUE;
+            }
+          }
+        }
+      }
+
       $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 && empty($theme->module_dependencies_disabled)) {
         // Create the operations links.
         $query['theme'] = $theme->getName();
         if ($this->themeAccess->checkAccess($theme->getName())) {
diff --git a/core/modules/system/src/Form/ModulesListForm.php b/core/modules/system/src/Form/ModulesListForm.php
index f32dad0909..46732d4e64 100644
--- a/core/modules/system/src/Form/ModulesListForm.php
+++ b/core/modules/system/src/Form/ModulesListForm.php
@@ -17,6 +17,7 @@
 use Drupal\Core\Session\AccountInterface;
 use Drupal\user\PermissionHandlerInterface;
 use Drupal\Core\Url;
+use Drupal\system\ModuleDependencyMessageTrait;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -31,6 +32,8 @@
  */
 class ModulesListForm extends FormBase {
 
+  use ModuleDependencyMessageTrait;
+
   /**
    * The current user.
    *
@@ -214,7 +217,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    * 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 +329,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 = $this->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
diff --git a/core/modules/system/src/Form/ModulesUninstallForm.php b/core/modules/system/src/Form/ModulesUninstallForm.php
index 9370050d15..52fde1e5ce 100644
--- a/core/modules/system/src/Form/ModulesUninstallForm.php
+++ b/core/modules/system/src/Form/ModulesUninstallForm.php
@@ -5,6 +5,7 @@
 use Drupal\Core\Extension\ModuleExtensionList;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Extension\ModuleInstallerInterface;
+use Drupal\Core\Extension\ThemeExtensionList;
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface;
@@ -45,6 +46,13 @@ class ModulesUninstallForm extends FormBase {
    */
   protected $moduleExtensionList;
 
+  /**
+   * The theme extension list.
+   *
+   * @var \Drupal\Core\Extension\ThemeExtensionList
+   */
+  protected $themeExtensionList;
+
   /**
    * {@inheritdoc}
    */
@@ -53,7 +61,8 @@ public static function create(ContainerInterface $container) {
       $container->get('module_handler'),
       $container->get('module_installer'),
       $container->get('keyvalue.expirable')->get('modules_uninstall'),
-      $container->get('extension.list.module')
+      $container->get('extension.list.module'),
+      $container->get('extension.list.theme')
     );
   }
 
@@ -68,12 +77,19 @@ public static function create(ContainerInterface $container) {
    *   The key value expirable factory.
    * @param \Drupal\Core\Extension\ModuleExtensionList $extension_list_module
    *   The module extension list.
+   * @param \Drupal\Core\Extension\ThemeExtensionList $theme_extension_list
+   *   The theme extension list.
    */
-  public function __construct(ModuleHandlerInterface $module_handler, ModuleInstallerInterface $module_installer, KeyValueStoreExpirableInterface $key_value_expirable, ModuleExtensionList $extension_list_module) {
+  public function __construct(ModuleHandlerInterface $module_handler, ModuleInstallerInterface $module_installer, KeyValueStoreExpirableInterface $key_value_expirable, ModuleExtensionList $extension_list_module, ThemeExtensionList $theme_extension_list = NULL) {
     $this->moduleExtensionList = $extension_list_module;
     $this->moduleHandler = $module_handler;
     $this->moduleInstaller = $module_installer;
     $this->keyValueExpirable = $key_value_expirable;
+    if (is_null($theme_extension_list)) {
+      @trigger_error('The extension.list.theme service must be passed to ' . __NAMESPACE__ . '\ModulesUninstallForm::__construct(). It was added in Drupal 8.9.0 and will be required before Drupal 9.0.0.', E_USER_DEPRECATED);
+      $theme_extension_list = \Drupal::service('extension.list.theme');
+    }
+    $this->themeExtensionList = $theme_extension_list;
   }
 
   /**
@@ -131,6 +147,19 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     uasort($uninstallable, 'system_sort_modules_by_info_name');
     $validation_reasons = $this->moduleInstaller->validateUninstall(array_keys($uninstallable));
 
+    $themes = $this->themeExtensionList->getList();
+
+    $modules_required_by_enabled_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_enabled_themes[$dependency][] = $theme;
+          }
+        }
+      }
+    }
+
     $form['uninstall'] = ['#tree' => TRUE];
     foreach ($uninstallable as $module_key => $module) {
       $name = $module->info['name'] ?: $module->getName();
@@ -159,6 +188,14 @@ public function buildForm(array $form, FormStateInterface $form_state) {
           $form['uninstall'][$module->getName()]['#disabled'] = TRUE;
         }
       }
+      // Modules required by enabled themes should not be uninstallable.
+      if (isset($modules_required_by_enabled_themes[$module->getName()])) {
+        foreach ($modules_required_by_enabled_themes[$module->getName()] as $theme) {
+          $theme_name = $theme->getName();
+          $form['modules'][$module->getName()]['#required_by'][] = $theme_name . ' (' . (string) $this->t('Theme') . ')';
+          $form['uninstall'][$module->getName()]['#disabled'] = TRUE;
+        }
+      }
     }
 
     $form['#attached']['library'][] = 'system/drupal.system.modules';
diff --git a/core/modules/system/src/ModuleDependencyMessageTrait.php b/core/modules/system/src/ModuleDependencyMessageTrait.php
new file mode 100644
index 0000000000..2cb88d242b
--- /dev/null
+++ b/core/modules/system/src/ModuleDependencyMessageTrait.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace Drupal\system;
+
+use Drupal\Core\Extension\Dependency;
+
+/**
+ * Messages for missing or incompatible dependencies on modules.
+ *
+ * @internal
+ */
+trait ModuleDependencyMessageTrait {
+
+  /**
+   * Provides messages for missing or incompatible dependencies on modules.
+   *
+   * @param array $modules
+   *   The list of existing modules.
+   * @param string $dependency
+   *   The module dependency to check.
+   * @param \Drupal\Core\Extension\Dependency $dependency_object
+   *   Dependency object used for comparing version requirement data.
+   *
+   * @return string|null
+   *   NULL if compatible, otherwise a string describing the incompatibility.
+   */
+  public function checkDependencyMessage(array $modules, $dependency, Dependency $dependency_object) {
+    if (!isset($modules[$dependency])) {
+      return $this->t('@module_name (<span class="admin-missing">missing</span>)', ['@module_name' => $dependency]);
+    }
+    else {
+      $module_name = $modules[$dependency]->info['name'];
+
+      // Check if the module is incompatible with the dependency constraints.
+      $version = str_replace(\Drupal::CORE_COMPATIBILITY . '-', '', $modules[$dependency]->info['version']);
+      if (!$dependency_object->isCompatible($version)) {
+        $constraint_string = $dependency_object->getConstraintString();
+        return $this->t('@module_name (<span class="admin-missing">incompatible with</span> version @version)', [
+          '@module_name' => "$module_name ($constraint_string)",
+          '@version' => $modules[$dependency]->info['version'],
+        ]);
+      }
+      // Check if the module is compatible with the installed version of core.
+      if (!isset($modules[$dependency]->info['core']) || $modules[$dependency]->info['core'] != \Drupal::CORE_COMPATIBILITY) {
+        return $this->t('@module_name (<span class="admin-missing">incompatible with</span> this version of Drupal core)', [
+          '@module_name' => $module_name,
+        ]);
+      }
+    }
+  }
+
+}
diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc
index 661677b51a..ac08488d4f 100644
--- a/core/modules/system/system.admin.inc
+++ b/core/modules/system/system.admin.inc
@@ -9,6 +9,7 @@
 use Drupal\Core\Link;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Template\Attribute;
+use Drupal\Core\Url;
 
 /**
  * Prepares variables for administrative content block templates.
@@ -131,6 +132,19 @@ function template_preprocess_system_admin_index(&$variables) {
 function template_preprocess_system_modules_details(&$variables) {
   $form = $variables['form'];
 
+  // Identify modules that are depended on by themes.
+  $themes = \Drupal::service('extension.list.theme')->getList();
+  foreach ($themes as $theme) {
+    if (!empty($theme->info['dependencies'])) {
+      foreach ($theme->info['dependencies'] as $dependency) {
+        if (isset($form[$dependency])) {
+          // Add themes to the module's required by list.
+          $form[$dependency]['#required_by'][] = $theme->info['name'] . ' (' . t('Theme') . ')' . (!empty($theme->experimental) ? ' (' . t('Experimental') . ')' : '') . (!$theme->status ? ' (' . t('Disabled') . ')' : '');
+        }
+      }
+    }
+  }
+
   $variables['modules'] = [];
   // Iterate through all the modules, which are children of this element.
   foreach (Element::children($form) as $key) {
@@ -291,6 +305,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 +331,20 @@ 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.');
+      }
+      elseif (!empty($theme->module_dependencies_disabled)) {
+        if (!empty($theme->insufficient_module_permissions)) {
+          $current_theme['incompatible'] = t('This theme requires the listed modules to operate correctly. They must first be enabled by a user with permissions to do so.');
+        }
+        else {
+          $modules_url = (string) Url::fromRoute('system.modules_list')->toString();
+          $current_theme['incompatible'] = t('This theme requires the listed modules to operate correctly. They must first be enabled via the <a href=":modules_url">Extend page</a>.', [
+            ':modules_url' => $modules_url,
+          ]);
+        }
+      }
 
       // 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/Module/InstallUninstallTest.php b/core/modules/system/tests/src/Functional/Module/InstallUninstallTest.php
index 0be9688a6c..462bbdf435 100644
--- a/core/modules/system/tests/src/Functional/Module/InstallUninstallTest.php
+++ b/core/modules/system/tests/src/Functional/Module/InstallUninstallTest.php
@@ -133,6 +133,7 @@ public function testInstallUninstall() {
         '@name' => $module_names[0],
         '@names' => implode(', ', $module_names),
       ]);
+
       $this->assertText($expected_text, 'Modules status has been updated.');
 
       // Check that hook_modules_installed() was invoked with the expected list
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..999b9fa223
--- /dev/null
+++ b/core/modules/system/tests/src/Functional/Theme/ThemeUiTest.php
@@ -0,0 +1,183 @@
+<?php
+
+namespace Drupal\Tests\system\Functional\Theme;
+
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests the theme UI.
+ *
+ * @group Theme
+ */
+class ThemeUiTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['node', 'block', 'file'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->drupalLogin($this->adminUser = $this->drupalCreateUser([
+      'access administration pages',
+      'view the administration theme',
+      'administer themes',
+      'bypass node access',
+      'administer blocks',
+      'administer modules',
+    ]));
+  }
+
+  /**
+   * Tests permissions for enabling themes depending on disabled modules.
+   */
+  public function testModulePermissions() {
+    // Log in as a user without permission to enable modules.
+    $this->drupalLogin($this->drupalCreateUser([
+      'access administration pages',
+      'view the administration theme',
+      'administer themes',
+      'bypass node access',
+      'administer blocks',
+    ]));
+    $this->drupalGet('admin/appearance');
+
+    // The links to install a theme that would enable modules should be replaced
+    // by this message.
+    $this->assertSession()->pageTextContains('This theme requires the listed modules to operate correctly. They must first be enabled by a user with permissions to do so.');
+
+    // The confirmation page should not be reachable.
+    $this->drupalGet('admin/appearance/install/confirm?theme=test_theme_depending_on_modules&modules%5B0%5D=test_module_required_by_theme&modules%5B1%5D=test_another_module_required_by_theme');
+    $this->assertSession()->statusCodeEquals(404);
+
+    // The install page should not be reachable.
+    $this->drupalGet('admin/appearance/install?theme=test_theme_depending_on_modules');
+    $this->assertSession()->statusCodeEquals(404);
+
+    $this->drupalLogin($this->drupalCreateUser([
+      'access administration pages',
+      'view the administration theme',
+      'administer themes',
+      'bypass node access',
+      'administer blocks',
+      'administer modules',
+    ]));
+    $this->drupalGet('admin/appearance');
+    $this->assertSession()->pageTextNotContains('This theme requires the listed modules to operate correctly. They must first be enabled by a user with permissions to do so.');
+  }
+
+  /**
+   * Tests installing a theme with depending on modules that are not enabled.
+   */
+  public function testInstallModuleWithNotInstalledDependencies() {
+    $assert_session = $this->assertSession();
+    $page = $this->getSession()->getPage();
+    $this->drupalGet('admin/appearance');
+    $themeXpath = '//h3[contains(text(), "test theme depending on modules")]';
+    $elements = $this->xpath($themeXpath);
+    $this->assertCount(1, $elements);
+    $element = $elements[0];
+    $container = $element->getParent();
+    $requires = $container->find('css', '.theme-info__requires');
+    $requires_list_items = $requires->find('css', 'li');
+    $expected_requires_list_items = [
+      'test module required by theme (disabled)',
+      'test another module required by theme (disabled)',
+    ];
+
+    foreach ($requires_list_items as $key => $item) {
+      $this->assertSame($expected_requires_list_items[$key], $item->getText());
+    }
+
+    $incompatible = $container->find('css', '.incompatible');
+    $expected_incompatible_text = 'This theme requires the listed modules to operate correctly. They must first be enabled via the Extend page.';
+    $this->assertSame($expected_incompatible_text, $incompatible->getText());
+    $this->assertFalse($container->hasLink('Install test theme depending on modules theme'));
+    $this->drupalPostForm('admin/modules', [
+      'modules[test_another_module_required_by_theme][enable]' => 1,
+      'modules[test_module_required_by_theme][enable]' => 1,
+    ], 'Install');
+    $this->assertSession()->elementExists('css', '#edit-modules-test-another-module-required-by-theme-enable[checked]');
+    $this->assertSession()->elementExists('css', '#edit-modules-test-module-required-by-theme-enable[checked]');
+    $this->drupalGet('admin/appearance');
+
+    $page->clickLink('Install test theme depending on modules theme');
+
+    $assert_session->addressEquals('admin/appearance');
+    $assert_session->pageTextContains('The test theme depending on modules theme has been installed');
+  }
+
+  /**
+   * Tests installing a theme depending on an already enabled module.
+   */
+  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)');
+    $this->drupalPostForm('admin/modules', [
+      'modules[test_another_module_required_by_theme][enable]' => 1,
+      'modules[test_module_required_by_theme][enable]' => 1,
+    ], 'Install');
+    $assert_session->checkboxChecked('modules[test_another_module_required_by_theme][enable]');
+    $assert_session->checkboxChecked('modules[test_module_required_by_theme][enable]');
+
+    $this->drupalGet('admin/modules/uninstall');
+    $assert_session->elementExists('css', '[name="uninstall[test_module_required_by_theme]"]:not([disabled])');
+    $assert_session->elementExists('css', '[name="uninstall[test_another_module_required_by_theme]"]:not([disabled])');
+    $assert_session->elementTextNotContains('css', '[data-drupal-selector="edit-test-another-module-required-by-theme"]', 'Required by: test_theme_depending_on_modules (Theme)');
+    $assert_session->elementTextNotContains('css', '[data-drupal-selector="edit-test-module-required-by-theme"]', 'Required by: test_theme_depending_on_modules (Theme)');
+
+    $this->drupalGet('admin/appearance');
+    $this->getSession()->getPage()->clickLink('Install test theme depending on modules theme');
+    $assert_session->pageTextContains('The test theme depending on modules theme has been installed.');
+
+    $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)');
+    $assert_session->elementTextContains('css', '[data-drupal-selector="edit-modules-test-module-required-by-theme"] .requirements', 'test theme depending on modules (Theme)');
+
+    $this->drupalGet('admin/modules/uninstall');
+    $assert_session->elementExists('css', '[name="uninstall[test_module_required_by_theme]"][disabled]');
+    $assert_session->elementExists('css', '[name="uninstall[test_another_module_required_by_theme]"][disabled]');
+    $assert_session->elementTextContains('css', '[data-drupal-selector="edit-test-another-module-required-by-theme"] .item-list', 'Required by: test_theme_depending_on_modules (Theme)');
+    $assert_session->elementTextContains('css', '[data-drupal-selector="edit-test-module-required-by-theme"] .item-list', 'Required by: test_theme_depending_on_modules (Theme)');
+  }
+
+}
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..62ecbd2fd4
--- /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: stark
+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..7b4eaba9fc
--- /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: stark
+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..7edd71f3b8
--- /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: stark
+dependencies:
+  - test_module_required_by_theme
+  - test_another_module_required_by_theme
diff --git a/core/tests/Drupal/Tests/Core/Extension/ModuleRequiredByThemesUninstallValidatorTest.php b/core/tests/Drupal/Tests/Core/Extension/ModuleRequiredByThemesUninstallValidatorTest.php
new file mode 100644
index 0000000000..ed45ac4630
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Extension/ModuleRequiredByThemesUninstallValidatorTest.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace Drupal\Tests\Core\Extension;
+
+use Drupal\Tests\AssertHelperTrait;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Extension\ModuleRequiredByThemesUninstallValidator
+ * @group Extension
+ */
+class ModuleRequiredByThemesUninstallValidatorTest extends UnitTestCase {
+
+  use AssertHelperTrait;
+
+  /**
+   * The PHPUnit mock of ModuleRequiredByThemesUninstallValidator.
+   *
+   * @var \Drupal\Core\Extension\ModuleRequiredByThemesUninstallValidator|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $moduleRequiredByThemeUninstallValidator;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->moduleRequiredByThemeUninstallValidator = $this->getMockBuilder('Drupal\Core\Extension\ModuleRequiredByThemesUninstallValidator')
+      ->disableOriginalConstructor()
+      ->setMethods(['getModulesThemesDependOn', 'getModuleName'])
+      ->getMock();
+    $this->moduleRequiredByThemeUninstallValidator->setStringTranslation($this->getStringTranslationStub());
+  }
+
+  /**
+   * @covers ::validate
+   */
+  public function testValidateNoThemeDependency() {
+    $this->moduleRequiredByThemeUninstallValidator->expects($this->once())
+      ->method('getModulesThemesDependOn')
+      ->willReturn([]);
+
+    $module = $this->randomMachineName();
+    $expected = [];
+    $reasons = $this->moduleRequiredByThemeUninstallValidator->validate($module);
+    $this->assertSame($expected, $reasons);
+  }
+
+  /**
+   * @covers ::validate
+   */
+  public function testValidateOneThemeDependency() {
+    $module = 'single_module';
+    $module_name = 'Single Module';
+    $theme_name = 'One Theme';
+    $this->moduleRequiredByThemeUninstallValidator->expects($this->once())
+      ->method('getModulesThemesDependOn')
+      ->willReturn([
+        $module => [$theme_name],
+      ]);
+    $this->moduleRequiredByThemeUninstallValidator->expects($this->once())
+      ->method('getModuleName')
+      ->willReturn($module_name);
+
+    $expected = [
+      "The $module_name module is required by the theme $theme_name",
+    ];
+    $reasons = $this->moduleRequiredByThemeUninstallValidator->validate($module);
+    $this->assertSame($expected, $this->castSafeStrings($reasons));
+  }
+
+  /**
+   * @covers ::validate
+   */
+  public function testValidateTwoThemeDependencies() {
+    $module = 'popular_module';
+    $module_name = 'Popular Module';
+    $theme_name_1 = 'First Theme';
+    $theme_name_2 = 'Second Theme';
+    $this->moduleRequiredByThemeUninstallValidator->expects($this->once())
+      ->method('getModulesThemesDependOn')
+      ->willReturn([
+        $module => [$theme_name_1, $theme_name_2],
+      ]);
+    $this->moduleRequiredByThemeUninstallValidator->expects($this->once())
+      ->method('getModuleName')
+      ->willReturn($module_name);
+
+    $expected = [
+      "The $module_name module is required by the themes $theme_name_1, $theme_name_2",
+    ];
+    $reasons = $this->moduleRequiredByThemeUninstallValidator->validate($module);
+    $this->assertSame($expected, $this->castSafeStrings($reasons));
+  }
+
+}
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>
