diff --git a/core/core.services.yml b/core/core.services.yml
index 93e57d0fb4..20f3ac145e 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 70157580d4..089c271daa 100644
--- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php
+++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
@@ -369,12 +369,14 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
     }
 
     if ($uninstall_dependents) {
+      $theme_list = \Drupal::service('extension.list.theme')->getList();
+
       // Add dependent modules to the list. The new modules will be processed as
       // 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..0598b031e1
--- /dev/null
+++ b/core/lib/Drupal/Core/Extension/ModuleRequiredByThemesUninstallValidator.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace Drupal\Core\Extension;
+
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+
+/**
+ * Ensures modules cannot be uninstalled if enabled themes depend on them.
+ */
+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
+   *   The theme extension list.
+   */
+  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 = [];
+
+    $themes_depending_on_module = $this->getThemesDependingOnModule($module);
+    if (!empty($themes_depending_on_module)) {
+      $module_name = $this->moduleExtensionList->get($module)->info['name'];
+      $theme_names = implode(", ", $themes_depending_on_module);
+      $reasons[] = $this->formatPlural(count($themes_depending_on_module),
+        'Required by the theme: @theme_names',
+        'Required by the themes: @theme_names',
+        ['@module_name' => $module_name, '@theme_names' => $theme_names]);
+    }
+
+    return $reasons;
+  }
+
+  /**
+   * Returns themes that depend on a module.
+   *
+   * @param string $module
+   *   The module machine name.
+   *
+   * @return string[]
+   *   An array of the names of themes that depend on $module.
+   */
+  protected function getThemesDependingOnModule($module) {
+    $installed_themes = $this->themeExtensionList->getAllInstalledInfo();
+    $themes_depending_on_module = array_map(function ($theme) use ($module) {
+      if (in_array($module, $theme['dependencies'])) {
+        return $theme['name'];
+      }
+    }, $installed_themes);
+
+    return array_filter($themes_depending_on_module);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Extension/ThemeExtensionList.php b/core/lib/Drupal/Core/Extension/ThemeExtensionList.php
index 5722d1c2fe..95334a1b5b 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,16 @@ 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 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..4993fa1cad 100644
--- a/core/lib/Drupal/Core/Extension/ThemeInstaller.php
+++ b/core/lib/Drupal/Core/Extension/ThemeInstaller.php
@@ -106,6 +106,8 @@ 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') ?: [];
+    $installed_modules = $this->configFactory->get('core.extension')->get('module') ?: [];
 
     if ($install_dependencies) {
       $theme_list = array_combine($theme_list, $theme_list);
@@ -116,16 +118,25 @@ 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);
+        $missing_module_dependencies = array_diff_key($module_dependencies, $installed_modules);
+
+        // Prevent themes with unmet module dependencies from being installed.
+        if (!empty($missing_module_dependencies)) {
+          $missing_module_dependencies_list = implode(', ', array_keys($missing_module_dependencies));
+          throw new MissingDependencyException("Unable to install theme: '$theme' due to missing module dependencies: '$missing_module_dependencies_list.'");
+        }
+
+        // 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,9 +158,6 @@ 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) {
diff --git a/core/lib/Drupal/Core/Extension/ThemeInstallerInterface.php b/core/lib/Drupal/Core/Extension/ThemeInstallerInterface.php
index ae79b505ea..6f18248cf9 100644
--- a/core/lib/Drupal/Core/Extension/ThemeInstallerInterface.php
+++ b/core/lib/Drupal/Core/Extension/ThemeInstallerInterface.php
@@ -25,6 +25,9 @@ interface ThemeInstallerInterface {
    *
    * @throws \Drupal\Core\Extension\Exception\UnknownExtensionException
    *   Thrown when the theme does not exist.
+   *
+   * @throws \Drupal\Core\Extension\MissingDependencyException
+   *   Thrown when a requested dependency can't be found.
    */
   public function install(array $theme_list, $install_dependencies = TRUE);
 
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/system/src/Controller/SystemController.php b/core/modules/system/src/Controller/SystemController.php
index f27cc01eb1..c848221dd2 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,20 @@ 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;
+    if ($module_extension_list === NULL) {
+      @trigger_error('The extension.list.module service must be passed to ' . __NAMESPACE__ . '\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 +102,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')
     );
   }
 
@@ -231,9 +250,44 @@ 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) {
+        $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 modules must first be enabled.
+          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..45a57d7d12 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.
    *
@@ -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/ModuleDependencyMessageTrait.php b/core/modules/system/src/ModuleDependencyMessageTrait.php
new file mode 100644
index 0000000000..66165089e6
--- /dev/null
+++ b/core/modules/system/src/ModuleDependencyMessageTrait.php
@@ -0,0 +1,53 @@
+<?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 compatible with the installed version of core.
+      if ($modules[$dependency]->info['core_incompatible']) {
+        return $this->t('@module_name (<span class="admin-missing">incompatible with</span> this version of Drupal core)', [
+          '@module_name' => $module_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'],
+        ]);
+      }
+    }
+  }
+
+}
diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc
index 661677b51a..ea33414fe9 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.
@@ -123,7 +124,7 @@ function template_preprocess_system_admin_index(&$variables) {
  *     - version: The version of the module.
  *     - links: Administration links provided by the module.
  *     - #requires: A list of modules that the project requires.
- *     - #required_by: A list of modules that require the project.
+ *     - #required_by: A list of modules and themes that require the project.
  *     - #attributes: A list of attributes for the module wrapper.
  *
  * @see \Drupal\system\Form\ModulesListForm
@@ -131,6 +132,18 @@ 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.
+  // Added here instead of ModuleHandler to avoid recursion.
+  $themes = \Drupal::service('extension.list.theme')->getList();
+  foreach ($themes as $theme) {
+    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') . ')' . (!$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 +304,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 +330,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/Theme/ThemeUiTest.php b/core/modules/system/tests/src/Functional/Theme/ThemeUiTest.php
new file mode 100644
index 0000000000..cdaaea5227
--- /dev/null
+++ b/core/modules/system/tests/src/Functional/Theme/ThemeUiTest.php
@@ -0,0 +1,167 @@
+<?php
+
+namespace Drupal\Tests\system\Functional\Theme;
+
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests the theme UI.
+ *
+ * @group Theme
+ */
+class ThemeUiTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->drupalLogin($this->adminUser = $this->drupalCreateUser([
+      'administer themes',
+      '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([
+      'administer themes',
+    ]));
+    $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 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([
+      'administer themes',
+      '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 module dependencies.
+   *
+   * @dataProvider providerTestThemeInstallWithModuleDependencies
+   */
+  public function testThemeInstallWithModuleDependencies($theme_name) {
+    $assert_session = $this->assertSession();
+    $page = $this->getSession()->getPage();
+    $this->drupalGet('admin/appearance');
+    $expected_requires_list_items = [
+      'Test Module Required by Theme (disabled)',
+      'Test Another Module Required by Theme (disabled)',
+    ];
+    $this->assertUninstallableTheme($expected_requires_list_items, $theme_name);
+
+    // Enable one of the two required modules.
+    $this->drupalPostForm('admin/modules', [
+      'modules[test_module_required_by_theme][enable]' => 1,
+    ], 'Install');
+    $this->assertSession()->elementExists('css', '#edit-modules-test-module-required-by-theme-enable[checked]');
+
+    $this->drupalGet('admin/appearance');
+
+    // Confirm the theme is still uninstallable due to a remaining module
+    // dependency.
+    $expected_requires_list_items = [
+      'Test Another Module Required by Theme (disabled)',
+    ];
+    $this->assertUninstallableTheme($expected_requires_list_items, $theme_name);
+
+    $this->drupalPostForm('admin/modules', [
+      'modules[test_another_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]');
+
+    // The theme should now be installable, so install it.
+    $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');
+
+    // Confirm that the dependee modules can't be uninstalled because an enabled
+    // theme depends on them.
+    $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 the theme: test theme depending on modules');
+    $assert_session->elementTextContains('css', '[data-drupal-selector="edit-test-module-required-by-theme"] .item-list', 'Required by the theme: test theme depending on modules');
+
+    // Uninstall the theme that depends on the modules, and confirm the modules
+    // can now be uninstalled.
+    $this->drupalGet('admin/appearance');
+    $this->clickLink('Uninstall Test Theme Depending on Modules theme');
+    $assert_session->pageTextContains('The Test Theme Depending on Modules theme has been uninstalled.');
+    $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])');
+    $this->drupalPostForm('admin/modules/uninstall', [
+      'uninstall[test_module_required_by_theme]' => 1,
+      'uninstall[test_another_module_required_by_theme]' => 1,
+    ], 'Uninstall');
+    $confirmation_message = 'The following modules will be completely uninstalled from your site, and all data from these modules will be lost!test another module required by themetest module required by themeWould you like to continue with uninstalling the above?';
+    $assert_session->pageTextContains($confirmation_message);
+    $page->pressButton('Uninstall');
+    $assert_session->pageTextContains('The selected modules have been uninstalled.');
+  }
+
+  /**
+   * Data provider for testThemeInstallWithModuleDependencies().
+   */
+  public function providerTestThemeInstallWithModuleDependencies() {
+    return [
+      'test theme depending on modules' => ['Test Theme Depending on Modules'],
+      'test theme with a base theme depending on modules' => ['Test Theme with a Base Theme Depending on Modules'],
+    ];
+  }
+
+  /**
+   * Checks related to uninstallable themes due to module dependencies.
+   *
+   * @param string[] $expected_requires_list_items
+   *   The modules listed as being required to install the theme.
+   * @param string $theme_name
+   *   The name of the theme.
+   */
+  protected function assertUninstallableTheme(array $expected_requires_list_items, $theme_name) {
+    $theme_container = $this->getSession()->getPage()->find('css', "h3:contains(\"$theme_name\")")->getParent();
+    $requires_list_items = $theme_container->find('css', '.theme-info__requires li');
+
+    foreach ($requires_list_items as $key => $item) {
+      $this->assertSame($expected_requires_list_items[$key], $item->getText());
+    }
+
+    $incompatible = $theme_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($theme_container->hasLink('Install Test Theme Depending on Modules theme'));
+  }
+
+  /**
+   * Tests installing a theme with missing module dependencies.
+   */
+  public function testInstallModuleWithMissingDependencies() {
+    $this->drupalGet('admin/appearance');
+    $theme_container = $this->getSession()->getPage()->find('css', 'h3:contains("Test Theme Depending on Nonexisting Module")')->getParent();
+    $this->assertContains('missing', $theme_container->getText());
+    $this->assertContains('This theme requires the listed modules to operate correctly.', $theme_container->getText());
+  }
+
+}
diff --git a/core/modules/system/tests/themes/test_theme_depending_nonexisting_module/test_theme_depending_on_nonexisting_module.info.yml b/core/modules/system/tests/themes/test_theme_depending_nonexisting_module/test_theme_depending_on_nonexisting_module.info.yml
new file mode 100644
index 0000000000..43c8ed477b
--- /dev/null
+++ b/core/modules/system/tests/themes/test_theme_depending_nonexisting_module/test_theme_depending_on_nonexisting_module.info.yml
@@ -0,0 +1,7 @@
+name: Test Theme Depending on Nonexisting Module
+type: theme
+core: 8.x
+base theme: stark
+version: VERSION
+dependencies:
+  - test_module_non_existing
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..4527338d46
--- /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/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..75fcd17ae5
--- /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_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..a20675c742
--- /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/modules/system/tests/themes/test_theme_with_a_base_theme_depending_on_modules/test_theme_with_a_base_theme_depending_on_modules.info.yml b/core/modules/system/tests/themes/test_theme_with_a_base_theme_depending_on_modules/test_theme_with_a_base_theme_depending_on_modules.info.yml
new file mode 100644
index 0000000000..91391fdac8
--- /dev/null
+++ b/core/modules/system/tests/themes/test_theme_with_a_base_theme_depending_on_modules/test_theme_with_a_base_theme_depending_on_modules.info.yml
@@ -0,0 +1,4 @@
+name: Test Theme with a Base Theme Depending on Modules
+type: theme
+core: 8.x
+base theme: test_theme_depending_on_modules
diff --git a/core/tests/Drupal/KernelTests/Core/Theme/ThemeInstallerTest.php b/core/tests/Drupal/KernelTests/Core/Theme/ThemeInstallerTest.php
index c8dd0337a8..e0027056be 100644
--- a/core/tests/Drupal/KernelTests/Core/Theme/ThemeInstallerTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Theme/ThemeInstallerTest.php
@@ -4,6 +4,8 @@
 
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\Extension\ExtensionNameLengthException;
+use Drupal\Core\Extension\MissingDependencyException;
+use Drupal\Core\Extension\ModuleUninstallValidatorException;
 use Drupal\Core\Extension\Exception\UnknownExtensionException;
 use Drupal\KernelTests\KernelTestBase;
 
@@ -104,21 +106,11 @@ public function testInstallSubTheme() {
    */
   public function testInstallNonExisting() {
     $name = 'non_existing_theme';
-
-    $themes = $this->themeHandler()->listInfo();
-    $this->assertEmpty(array_keys($themes));
-
-    try {
-      $message = 'ThemeInstaller::install() throws UnknownExtensionException upon installing a non-existing theme.';
-      $this->themeInstaller()->install([$name]);
-      $this->fail($message);
-    }
-    catch (UnknownExtensionException $e) {
-      $this->pass(get_class($e) . ': ' . $e->getMessage());
-    }
-
     $themes = $this->themeHandler()->listInfo();
     $this->assertEmpty(array_keys($themes));
+    $this->expectException(UnknownExtensionException::class);
+    $this->expectExceptionMessage('Unknown themes: non_existing_theme.');
+    $this->themeInstaller()->install([$name]);
   }
 
   /**
@@ -137,6 +129,34 @@ public function testInstallNameTooLong() {
     }
   }
 
+  /**
+   * Tests installing a theme with unmet module dependencies.
+   */
+  public function testInstallThemeWithUnmetModuleDependencies() {
+    $name = 'test_theme_depending_on_modules';
+    $themes = $this->themeHandler()->listInfo();
+    $this->assertEmpty(array_keys($themes));
+    $this->expectException(MissingDependencyException::class);
+    $this->expectExceptionMessage('Unable to install theme: \'test_theme_depending_on_modules\' due to missing module dependencies: \'test_module_required_by_theme, test_another_module_required_by_theme.');
+    $this->themeInstaller()->install([$name]);
+  }
+
+  /**
+   * Tests installing a theme with module dependencies that are met.
+   */
+  public function testInstallThemeWithMetModuleDependencies() {
+    $name = 'test_theme_depending_on_modules';
+    $themes = $this->themeHandler()->listInfo();
+    $this->assertFalse(isset($themes[$name]));
+    $this->container->get('module_installer')->install(['test_module_required_by_theme', 'test_another_module_required_by_theme']);
+    $this->themeInstaller()->install([$name]);
+    $themes = $this->themeHandler()->listInfo();
+    $this->assertTrue(isset($themes[$name]));
+    $this->expectException(ModuleUninstallValidatorException::class);
+    $this->expectExceptionMessage('The following reasons prevent the modules from being uninstalled: Required by the theme: Test Theme Depending on Modules');
+    $this->container->get('module_installer')->uninstall(['test_module_required_by_theme']);
+  }
+
   /**
    * Tests uninstalling the default 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..71537340a3
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Extension/ModuleRequiredByThemesUninstallValidatorTest.php
@@ -0,0 +1,161 @@
+<?php
+
+namespace Drupal\Tests\Core\Extension;
+
+use Drupal\Core\Extension\ModuleExtensionList;
+use Drupal\Core\Extension\ModuleRequiredByThemesUninstallValidator;
+use Drupal\Core\Extension\ThemeExtensionList;
+use Drupal\Tests\AssertHelperTrait;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Extension\ModuleRequiredByThemesUninstallValidator
+ * @group Extension
+ */
+class ModuleRequiredByThemesUninstallValidatorTest extends UnitTestCase {
+
+  use AssertHelperTrait;
+
+  /**
+   * Instance of ModuleRequiredByThemesUninstallValidator.
+   *
+   * @var \Drupal\Core\Extension\ModuleRequiredByThemesUninstallValidator|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $moduleRequiredByThemeUninstallValidator;
+
+  /**
+   * Mock of ModuleExtensionList.
+   *
+   * @var \Drupal\Core\Extension\ModuleExtensionList|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $moduleExtensionList;
+
+  /**
+   * Mock of ThemeExtensionList.
+   *
+   * @var \Drupal\Core\Extension\ThemeExtensionList|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $themeExtensionList;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->moduleExtensionList = $this->prophesize(ModuleExtensionList::class);
+    $this->themeExtensionList = $this->prophesize(ThemeExtensionList::class);
+    $this->moduleRequiredByThemeUninstallValidator = new ModuleRequiredByThemesUninstallValidator($this->getStringTranslationStub(), $this->moduleExtensionList->reveal(), $this->themeExtensionList->reveal());
+  }
+
+  /**
+   * @covers ::validate
+   */
+  public function testValidateNoThemeDependency() {
+    $this->themeExtensionList->getAllInstalledInfo()->willReturn([
+      'stable' => [
+        'name' => 'Stable',
+        'dependencies' => [],
+      ],
+      'claro' => [
+        'name' => 'Claro',
+        'dependencies' => [],
+      ],
+    ]);
+
+    $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 = 'one_theme';
+    $theme_name = 'One Theme';
+    $this->themeExtensionList->getAllInstalledInfo()->willReturn([
+      'stable' => [
+        'name' => 'Stable',
+        'dependencies' => [],
+      ],
+      'claro' => [
+        'name' => 'Claro',
+        'dependencies' => [],
+      ],
+      $theme => [
+        'name' => $theme_name,
+        'dependencies' => [
+          $module,
+        ],
+      ],
+    ]);
+
+    $this->moduleExtensionList->get($module)->willReturn((object) [
+      'info' => [
+        'name' => $module_name,
+      ],
+    ]);
+
+    $expected = [
+      "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';
+    $theme1 = 'first_theme';
+    $theme2 = 'second_theme';
+    $theme_name_1 = 'First Theme';
+    $theme_name_2 = 'Second Theme';
+    $this->themeExtensionList->getAllInstalledInfo()->willReturn([
+      'stable' => [
+        'name' => 'Stable',
+        'dependencies' => [],
+      ],
+      'claro' => [
+        'name' => 'Claro',
+        'dependencies' => [],
+      ],
+      $theme1 => [
+        'name' => $theme_name_1,
+        'dependencies' => [
+          $module,
+        ],
+      ],
+      $theme2 => [
+        'name' => $theme_name_2,
+        'dependencies' => [
+          $module,
+        ],
+      ],
+    ]);
+
+    $this->moduleExtensionList->get($module)->willReturn((object) [
+      'info' => [
+        'name' => $module_name,
+      ],
+    ]);
+
+    $expected = [
+      "Required by the themes: $theme_name_1, $theme_name_2",
+    ];
+
+    $reasons = $this->moduleRequiredByThemeUninstallValidator->validate($module);
+    $this->assertSame($expected, $this->castSafeStrings($reasons));
+  }
+
+}
+
+if (!defined('DRUPAL_MINIMUM_PHP')) {
+  define('DRUPAL_MINIMUM_PHP', '7.0.8');
+}
diff --git a/core/themes/claro/templates/system-themes-page.html.twig b/core/themes/claro/templates/system-themes-page.html.twig
index 1108c7f2f6..6a340318cc 100644
--- a/core/themes/claro/templates/system-themes-page.html.twig
+++ b/core/themes/claro/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.
  *     - title_id: The unique id of the theme label.
@@ -97,6 +98,9 @@
               </div>
 
               <div class="card__footer">
+                {% if theme.module_dependencies %}
+                  <div class="theme-info__requires">Requires: {{ theme.module_dependencies }}</div>
+                {% endif %}
                 {# Display operation links only if the theme is compatible. #}
                 {% if theme.incompatible %}
                   <small class="incompatible">{{ theme.incompatible }}</small>
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>
