diff --git a/core/includes/update.inc b/core/includes/update.inc
index 4f34dbd4ac..d68765a4c9 100644
--- a/core/includes/update.inc
+++ b/core/includes/update.inc
@@ -9,6 +9,7 @@
  */
 
 use Drupal\Component\Graph\Graph;
+use Drupal\Component\Version\DrupalSemver;
 use Drupal\Core\Update\UpdateKernel;
 use Drupal\Core\Utility\Error;
 
@@ -59,7 +60,7 @@ function update_check_incompatibility($name, $type = 'module') {
   }
   if (!isset($file)
       || !isset($file->info['core'])
-      || $file->info['core'] != \Drupal::CORE_COMPATIBILITY
+      || !DrupalSemver::satisfies(\Drupal::VERSION, $file->info['core'])
       || version_compare(phpversion(), $file->info['php']) < 0) {
     return TRUE;
   }
diff --git a/core/lib/Drupal/Component/Version/DrupalSemver.php b/core/lib/Drupal/Component/Version/DrupalSemver.php
new file mode 100644
index 0000000000..74cdd7be70
--- /dev/null
+++ b/core/lib/Drupal/Component/Version/DrupalSemver.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\Component\Version;
+
+use Composer\Semver\Semver;
+
+/**
+ * A utility class for semantic version comparison.
+ */
+class DrupalSemver {
+
+  /**
+   * Determines if a version satisfies the given constraints.
+   *
+   * This method uses \Composer\Semver\Semver::satisfies() but returns FALSE if
+   * the version or constraints are not valid instead of throwing an exception.
+   *
+   * @param string $version
+   *   The version.
+   * @param string $constraints
+   *   The constraints.
+   *
+   * @return bool
+   *   TRUE if the version satisfies the constraints.
+   *
+   * @see \Composer\Semver\Semver::satisfies()
+   */
+  public static function satisfies($version, $constraints) {
+    try {
+      return Semver::satisfies($version, $constraints);
+    }
+    catch (\UnexpectedValueException $exception) {
+      return FALSE;
+    }
+  }
+
+}
diff --git a/core/modules/system/src/Controller/SystemController.php b/core/modules/system/src/Controller/SystemController.php
index 72c2e8adea..ce033c4e67 100644
--- a/core/modules/system/src/Controller/SystemController.php
+++ b/core/modules/system/src/Controller/SystemController.php
@@ -12,6 +12,7 @@
 use Drupal\Core\Url;
 use Drupal\system\SystemManager;
 use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Component\Version\DrupalSemver;
 
 /**
  * Returns responses for System routes.
@@ -223,7 +224,7 @@ public function themesPage() {
 
       if (empty($theme->status)) {
         // Ensure this theme is compatible with this version of core.
-        $theme->incompatible_core = !isset($theme->info['core']) || ($theme->info['core'] != \DRUPAL::CORE_COMPATIBILITY);
+        $theme->incompatible_core = !isset($theme->info['core']) || !DrupalSemver::satisfies(\Drupal::VERSION, $theme->info['core']);
         // Require the 'content' region to make sure the main page
         // content has a common place in all themes.
         $theme->incompatible_region = !isset($theme->info['regions']['content']);
diff --git a/core/modules/system/src/Form/ModulesListForm.php b/core/modules/system/src/Form/ModulesListForm.php
index cabbe2ebaa..e4ccdfb9b5 100644
--- a/core/modules/system/src/Form/ModulesListForm.php
+++ b/core/modules/system/src/Form/ModulesListForm.php
@@ -3,6 +3,7 @@
 namespace Drupal\system\Form;
 
 use Drupal\Component\Utility\Unicode;
+use Drupal\Component\Version\DrupalSemver;
 use Drupal\Core\Config\PreExistingConfigException;
 use Drupal\Core\Config\UnmetDependenciesException;
 use Drupal\Core\Access\AccessManagerInterface;
@@ -20,6 +21,7 @@
 use Drupal\Core\Url;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
+
 /**
  * Provides module installation interface.
  *
@@ -295,10 +297,15 @@ protected function buildRow(array $modules, Extension $module, $distribution) {
     $reasons = [];
 
     // Check the core compatibility.
-    if ($module->info['core'] != \Drupal::CORE_COMPATIBILITY) {
+    if (!DrupalSemver::satisfies(\Drupal::VERSION, $module->info['core'])) {
       $compatible = FALSE;
       $reasons[] = $this->t('This version is not compatible with Drupal @core_version and should be replaced.', [
-        '@core_version' => \Drupal::CORE_COMPATIBILITY,
+        '@core_version' => \Drupal::VERSION,
+      ]);
+      $row['#requires']['core'] = $this->t('Drupal Core (@core_requirement) (<span class="admin-missing">incompatible with</span> version @core_version)', [
+        '@module' => $module->getName(),
+        '@core_requirement' => $module->info['core'],
+        '@core_version' => \Drupal::VERSION,
       ]);
     }
 
@@ -342,7 +349,7 @@ protected function buildRow(array $modules, Extension $module, $distribution) {
         }
         // Disable the checkbox if the dependency is incompatible with this
         // version of Drupal core.
-        elseif ($modules[$dependency]->info['core'] != \Drupal::CORE_COMPATIBILITY) {
+        elseif (!DrupalSemver::satisfies(\Drupal::VERSION, $modules[$dependency]->info['core'])) {
           $row['#requires'][$dependency] = $this->t('@module (<span class="admin-missing">incompatible with</span> this version of Drupal core)', [
             '@module' => $name,
           ]);
diff --git a/core/modules/system/tests/modules/system_incompatible_core_version_test_9x/system_incompatible_core_version_test_9x.info.yml b/core/modules/system/tests/modules/system_incompatible_core_version_test_9x/system_incompatible_core_version_test_9x.info.yml
new file mode 100644
index 0000000000..2f9f9d5750
--- /dev/null
+++ b/core/modules/system/tests/modules/system_incompatible_core_version_test_9x/system_incompatible_core_version_test_9x.info.yml
@@ -0,0 +1,6 @@
+name: 'System incompatible core 9.x version test'
+type: module
+description: 'Support module for testing system core incompatibility.'
+package: Testing
+version: 1.0.0
+core: 99.x
diff --git a/core/modules/system/tests/modules/system_test/system_test.module b/core/modules/system/tests/modules/system_test/system_test.module
index affc20ea42..e0825d2993 100644
--- a/core/modules/system/tests/modules/system_test/system_test.module
+++ b/core/modules/system/tests/modules/system_test/system_test.module
@@ -62,6 +62,10 @@ function system_test_system_info_alter(&$info, Extension $file, $type) {
     }
   }
 
+  if (($core_requirement = \Drupal::state()->get('dependency_test.core_version_requirement')) && $file->getName() === 'common_test') {
+    $info['core'] = $core_requirement;
+  }
+
   // Make the system_dependencies_test visible by default.
   if ($file->getName() == 'system_dependencies_test') {
     $info['hidden'] = FALSE;
@@ -71,6 +75,7 @@ function system_test_system_info_alter(&$info, Extension $file, $type) {
     'system_incompatible_core_version_dependencies_test',
     'system_incompatible_module_version_test',
     'system_incompatible_core_version_test',
+    'system_incompatible_core_version_test_9x',
   ])) {
     $info['hidden'] = FALSE;
   }
diff --git a/core/modules/system/tests/src/Functional/Module/DependencyTest.php b/core/modules/system/tests/src/Functional/Module/DependencyTest.php
index 2e0e60923c..d5b0a78ee8 100644
--- a/core/modules/system/tests/src/Functional/Module/DependencyTest.php
+++ b/core/modules/system/tests/src/Functional/Module/DependencyTest.php
@@ -102,6 +102,48 @@ public function testIncompatiblePhpVersionDependency() {
     $this->assert(count($checkbox) == 1, 'Checkbox for the module is disabled.');
   }
 
+  /**
+   * Tests enabling modules with different core version specifications.
+   */
+  public function testCoreVersionDependency() {
+    $assert_session = $this->assertSession();
+    list($major, $minor) = explode('.', \Drupal::VERSION);
+
+    $next_minor = $minor + 1;
+    $next_major = $major + 1;
+
+    // Test the next minor release.
+    \Drupal::state()->set('dependency_test.core_version_requirement', "~$major.$next_minor");
+    $this->drupalGet('admin/modules');
+    $assert_session->fieldDisabled('modules[system_incompatible_core_version_test_9x][enable]');
+    $assert_session->fieldDisabled('modules[common_test][enable]');
+
+    // Test either current major or the next one.
+    \Drupal::state()->set('dependency_test.core_version_requirement', "^$major || ^$next_major");
+    $this->drupalGet('admin/modules');
+    $this->assertFalse($assert_session->elementExists('css', '[name="modules[common_test][enable]"]')->hasAttribute('disabled'));
+
+    // Test either a previous major or the next one.
+    \Drupal::state()->set('dependency_test.core_version_requirement', "^1 || ^$next_major");
+    $this->drupalGet('admin/modules');
+    $assert_session->fieldDisabled('modules[common_test][enable]');
+
+    // Test an invalid major.
+    \Drupal::state()->set('dependency_test.core_version_requirement', 'this-string-is-invalid');
+    $this->drupalGet('admin/modules');
+    $assert_session->fieldDisabled('modules[common_test][enable]');
+
+    // Test the current minor.
+    \Drupal::state()->set('dependency_test.core_version_requirement', "~$major.$minor");
+    $this->drupalGet('admin/modules');
+    $this->assertFalse($assert_session->elementExists('css', '[name="modules[common_test][enable]"]')->hasAttribute('disabled'));
+
+    // Ensure the module can actually be installed.
+    $edit['modules[common_test][enable]'] = 'common_test';
+    $this->drupalPostForm('admin/modules', $edit, t('Install'));
+    $this->assertModules(['common_test'], TRUE);
+  }
+
   /**
    * Tests enabling a module that depends on a module which fails hook_requirements().
    */
diff --git a/core/tests/Drupal/Tests/Component/Version/DrupalSemverTest.php b/core/tests/Drupal/Tests/Component/Version/DrupalSemverTest.php
new file mode 100644
index 0000000000..44285eaba6
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Version/DrupalSemverTest.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Drupal\Tests\Component\Version;
+
+use Drupal\Component\Version\DrupalSemver;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Component\Version\DrupalSemver
+ * @group Version
+ */
+class DrupalSemverTest extends TestCase {
+
+  /**
+   * @covers ::satisfies
+   * @dataProvider providerSatisfies
+   */
+  public function testSatisfies($version, $constraints, $result) {
+    $this->assertSame($result, DrupalSemver::satisfies($version, $constraints));
+  }
+
+  public function providerSatisfies() {
+    $cases = [
+      ['8.8.0-dev', '8.x', TRUE],
+      ['8.8.0', '8.x', TRUE],
+      ['8.9.9', '8.x', TRUE],
+      ['8.0.0', '8.x', TRUE],
+      ['9.0.0', '8.x', FALSE],
+      ['9.1.0', '8.x', FALSE],
+      ['8.8.0', '~8', TRUE],
+      ['8.8.0', '^8', TRUE],
+      ['8.7.0', '^8.7.6', FALSE],
+      ['8.7.6', '^8.7.6', TRUE],
+      ['8.7.8', '^8.7.6', TRUE],
+      ['9.0.0', '^8.7.6', FALSE],
+      ['8.0.0', '^8 || ^9', TRUE],
+      ['9.1.1', '^8 || ^9', TRUE],
+      ['9.1.1', '^8.7.6 || ^9', TRUE],
+      ['8.7.8', '^8.7.6 || ^9', TRUE],
+      ['8.6.8', '^8.7.6 || ^9', FALSE],
+      ['8.6.8', '^9', FALSE],
+      ['9.1.1', '^9', TRUE],
+      ['9.0.0', '9.x', TRUE],
+      ['8.8.0', '9.x', FALSE],
+      ['8.8.0', '7.x', FALSE],
+      ['a-super-nonsense-string-will-not-throw-an-exception-but-also-will-not-work', '9.x', FALSE],
+      ['a-super-nonsense-string-will-not-throw-an-exception-but-also-will-not-work', '7.x', FALSE],
+      ['a-super-nonsense-string-will-not-throw-an-exception-but-also-will-not-work', '8.x', FALSE],
+    ];
+    $tests = [];
+    foreach ($cases as $case) {
+      $tests[$case[0] . ":" . $case[1]] = $case;
+    }
+    return $tests;
+  }
+
+}
