diff --git a/core/misc/cspell/dictionary.txt b/core/misc/cspell/dictionary.txt
index 357f8f4bfd..12a6677823 100644
--- a/core/misc/cspell/dictionary.txt
+++ b/core/misc/cspell/dictionary.txt
@@ -540,6 +540,7 @@ eventhandler
 exampleurl
 exitcode
 expirable
+extendee
 extlink
 extraday
 extrasmall
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 40734f7e35..a0856fdf2c 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -1268,8 +1268,9 @@ function _system_is_claro_admin_and_not_active() {
  */
 function system_library_info_alter(&$libraries, $extension) {
   // If Claro is the admin theme but not the active theme, grant Claro the
-  // ability to override the toolbar library with its own assets.
-  if ($extension === 'toolbar' && _system_is_claro_admin_and_not_active()) {
+  // ability to override the toolbar and contextual libraries with its own
+  // assets.
+  if (in_array($extension, ['toolbar', 'contextual']) && _system_is_claro_admin_and_not_active()) {
     require_once DRUPAL_ROOT . '/core/themes/claro/claro.theme';
     claro_system_module_invoked_library_info_alter($libraries, $extension);
   }
diff --git a/core/modules/system/tests/src/Functional/Theme/ContextualClaroOverridesTest.php b/core/modules/system/tests/src/Functional/Theme/ContextualClaroOverridesTest.php
new file mode 100644
index 0000000000..74160bf248
--- /dev/null
+++ b/core/modules/system/tests/src/Functional/Theme/ContextualClaroOverridesTest.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace Drupal\Tests\system\Functional\Theme;
+
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests the loading of Contextual's Claro assets on a non-Claro default theme.
+ *
+ * @group Theme
+ */
+class ContextualClaroOverridesTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['node', 'contextual'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * The theme installer used in this test for enabling themes.
+   *
+   * @var \Drupal\Core\Extension\ThemeInstallerInterface
+   */
+  protected $themeInstaller;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->themeInstaller = $this->container->get('theme_installer');
+    $this->themeManager = $this->container->get('theme.manager');
+    $this->themeInstaller->install(['claro']);
+
+    $this->drupalCreateContentType([
+      'type' => 'page',
+      'name' => 'Basic page',
+      'display_submitted' => FALSE,
+    ]);
+
+    // Create a node.
+    $this->drupalCreateNode();
+
+    $this->drupalLogin($this->drupalCreateUser([
+      'edit any page content',
+      'delete any page content',
+      'access contextual links',
+    ]));
+  }
+
+  /**
+   * Confirm Contextual's Claro assets load on a non-Claro default theme.
+   */
+  public function testClaroAssets() {
+    $default_stylesheets = [
+      'core/modules/contextual/css/contextual.theme.css',
+      'core/modules/contextual/css/contextual.icons.theme.css',
+    ];
+
+    $claro_stylesheets = [
+      'core/themes/claro/css/theme/contextual.theme.css',
+      'core/themes/claro/css/theme/contextual.icons.theme.css',
+      'core/themes/claro/css/components/icon-link.css',
+    ];
+
+    $this->config('system.theme')->set('admin', 'stark')->save();
+    $admin_theme = \Drupal::configFactory()->get('system.theme')->get('admin');
+    $default_theme = \Drupal::configFactory()->get('system.theme')->get('default');
+    $this->assertEquals('stark', $admin_theme);
+    $this->assertEquals('stark', $default_theme);
+
+    $this->drupalGet('node/1');
+    $head = $this->getSession()->getPage()->find('css', 'head')->getHtml();
+
+    // Confirm that Claro's Contextual assets are not loading, and the ones they
+    // would override if Claro was the admin theme are still loading.
+    $stylesheet_positions = [];
+    foreach ($default_stylesheets as $stylesheet) {
+      $this->assertStringContainsString($stylesheet, $head);
+      $stylesheet_positions[] = strpos($head, $stylesheet);
+    }
+    $sorted_stylesheet_positions = $stylesheet_positions;
+    sort($sorted_stylesheet_positions);
+    $this->assertEquals($sorted_stylesheet_positions, $stylesheet_positions);
+
+    foreach ($claro_stylesheets as $stylesheet) {
+      $this->assertStringNotContainsString($stylesheet, $head);
+    }
+    $this->assertSession()->elementNotExists('css', 'script[src*="core/themes/claro/js/contextual.js"]');
+
+    $this->config('system.theme')->set('admin', 'claro')->save();
+    $admin_theme = \Drupal::configFactory()->get('system.theme')->get('admin');
+    $default_theme = \Drupal::configFactory()->get('system.theme')->get('default');
+    $this->assertEquals('claro', $admin_theme);
+    $this->assertEquals('stark', $default_theme);
+
+    $this->drupalGet('node/1');
+    $head = $this->getSession()->getPage()->find('css', 'head')->getHtml();
+
+    // Confirm that Claro's Contextual assets are loading, and the ones they
+    // override are not loading.
+    $stylesheet_positions = [];
+    foreach ($claro_stylesheets as $stylesheet) {
+      $this->assertStringContainsString($stylesheet, $head);
+      $stylesheet_positions[] = strpos($head, $stylesheet);
+    }
+    $sorted_stylesheet_positions = $stylesheet_positions;
+    sort($sorted_stylesheet_positions);
+    $this->assertEquals($sorted_stylesheet_positions, $stylesheet_positions);
+
+    foreach ($default_stylesheets as $stylesheet) {
+      $this->assertStringNotContainsString($stylesheet, $head);
+    }
+
+    $this->assertSession()->elementExists('css', 'script[src*="core/themes/claro/js/contextual.js"]');
+  }
+
+}
diff --git a/core/themes/claro/claro.info.yml b/core/themes/claro/claro.info.yml
index 5490c17b07..13b2925275 100644
--- a/core/themes/claro/claro.info.yml
+++ b/core/themes/claro/claro.info.yml
@@ -76,6 +76,12 @@ libraries-override:
       theme:
         assets/vendor/jquery.ui/themes/base/theme.css: false
 
+  contextual/drupal.contextual-links:
+    css:
+      theme:
+        css/contextual.theme.css: css/theme/contextual.theme.css
+        css/contextual.icons.theme.css: css/theme/contextual.icons.theme.css
+
   user/drupal.user:
     css:
       component:
@@ -138,6 +144,8 @@ libraries-extend:
     - claro/claro.jquery.ui
   core/drupal.vertical-tabs:
     - claro/vertical-tabs
+  contextual/drupal.contextual-links:
+    - claro/contextual-trigger
   file/drupal.file:
     - claro/file
   filter/drupal.filter.admin:
diff --git a/core/themes/claro/claro.libraries.yml b/core/themes/claro/claro.libraries.yml
index a40b1625ca..1958587600 100644
--- a/core/themes/claro/claro.libraries.yml
+++ b/core/themes/claro/claro.libraries.yml
@@ -192,6 +192,13 @@ icon-link:
     component:
       css/components/icon-link.css: {}
 
+contextual-trigger:
+  version: VERSION
+  js:
+    js/contextual.js: {}
+  dependencies:
+      - claro/icon-link
+
 dropbutton:
   version: VERSION
   js:
diff --git a/core/themes/claro/claro.theme b/core/themes/claro/claro.theme
index dd4497f169..a40e6210e8 100644
--- a/core/themes/claro/claro.theme
+++ b/core/themes/claro/claro.theme
@@ -1631,7 +1631,8 @@ function claro_preprocess_toolbar(&$variables, $hook, $info) {
  * Called by system.module via its hook_library_info_alter().
  *
  * If the active theme is not Claro, but Claro is the admin theme, this alters
- * the toolbar library config so Claro's toolbar stylesheets are used.
+ * the library config for specific extensions so Claro's toolbar stylesheets are
+ * used.
  *
  * @see system_library_info_alter()
  */
@@ -1656,6 +1657,29 @@ function claro_system_module_invoked_library_info_alter(&$libraries, $extension)
       }
     }
   }
+
+  if ($extension === 'contextual') {
+    $claro_libraries = \Drupal::service('library.discovery')->getLibrariesByExtension('claro');
+    $claro_info = \Drupal::service('theme_handler')->listInfo()['claro']->info;
+    $path_prefix = '/core/themes/claro/';
+    $claro_contextual_overrides = $claro_info['libraries-override']['contextual/drupal.contextual-links'];
+    $claro_contextual_extend = $claro_info['libraries-extend']['contextual/drupal.contextual-links'];
+
+    foreach ($claro_contextual_overrides['css'] as $concern => $overrides) {
+      foreach ($claro_contextual_overrides['css'][$concern] as $key => $value) {
+        $config = $libraries['drupal.contextual-links']['css'][$concern][$key];
+        $libraries['drupal.contextual-links']['css'][$concern][$path_prefix . $value] = $config;
+        unset($libraries['drupal.contextual-links']['css'][$concern][$key]);
+      }
+    }
+
+    foreach ($claro_contextual_extend as $library => $extendee_library_name) {
+      $extendee_library = $claro_libraries[str_replace('claro/', '', $extendee_library_name)] ?? FALSE;
+      if ($extendee_library) {
+        extend_library($libraries['drupal.contextual-links'], $extendee_library_name, $extendee_library, $claro_libraries);
+      }
+    }
+  }
 }
 
 /**
@@ -1673,3 +1697,30 @@ function claro_system_module_invoked_theme_registry_alter(array &$theme_registry
     }
   }
 }
+
+/**
+ * Adds library dependencies.
+ *
+ * @param array $library_to_extend
+ *   The library being extended.
+ * @param string $extendee_library_name
+ *   The name of the library being added.
+ * @param array $extendee_library
+ *   The library being added.
+ * @param array $claro_libraries
+ *   All libraries defined by Claro.
+ */
+function extend_library(array &$library_to_extend, $extendee_library_name, array $extendee_library, array $claro_libraries) {
+  $library_to_extend['dependencies'][] = $extendee_library_name;
+
+  // If this library has other Claro dependencies, recursively call this
+  // function to add the assets from those libraries as well.
+  if (isset($extendee_library['dependencies'])) {
+    foreach ($extendee_library['dependencies'] as $dependee_library_name) {
+      $dependee_library = $claro_libraries[str_replace('claro/', '', $dependee_library_name)] ?? FALSE;
+      if ($dependee_library) {
+        extend_library($library_to_extend, $dependee_library_name, $dependee_library, $claro_libraries);
+      }
+    }
+  }
+}
diff --git a/core/themes/claro/css/base/variables.css b/core/themes/claro/css/base/variables.css
index aa94d6c938..fbec368def 100644
--- a/core/themes/claro/css/base/variables.css
+++ b/core/themes/claro/css/base/variables.css
@@ -52,6 +52,12 @@
   /**
    * Breadcrumb.
    */
+  /**
+  * Dropbutton.
+  */
+  /**
+   * Icon links.
+   */
   /**
     * Vertical Tabs.
     */
diff --git a/core/themes/claro/css/base/variables.pcss.css b/core/themes/claro/css/base/variables.pcss.css
index 01a44956c4..a8a68a3d26 100644
--- a/core/themes/claro/css/base/variables.pcss.css
+++ b/core/themes/claro/css/base/variables.pcss.css
@@ -206,6 +206,34 @@
    * Breadcrumb.
    */
   --breadcrumb-height: 1.25rem;
+  /**
+  * Dropbutton.
+  */
+  --dropbutton-spacing-size: var(--space-m);
+  --dropbutton-font-size: var(--font-size-base);
+  --dropbutton-line-height: var(--space-m);
+  --dropbutton-toggle-size: 3rem;
+  --dropbutton-border-size: 1px;
+  --dropbutton-toggle-size-spacing: var(--dropbutton-border-size);
+  --dropbutton-border-radius-size: 2px;
+  --dropbutton-small-spacing-size: 0.625rem;
+  --dropbutton-small-font-size: var(--font-size-xs);
+  --dropbutton-small-line-height: 0.75rem;
+  --dropbutton-small-toggle-size: calc((2 * var(--dropbutton-small-spacing-size)) + var(--dropbutton-small-line-height));
+  --dropbutton-extrasmall-spacing-size: 0.375rem;
+  --dropbutton-extrasmall-font-size: var(--font-size-xs);
+  --dropbutton-extrasmall-line-height: 0.75rem;
+  --dropbutton-extrasmall-toggle-size: calc((2 * var(--dropbutton-extrasmall-spacing-size)) + var(--dropbutton-extrasmall-line-height));
+  /**
+   * Icon links.
+   */
+  --icon-link-bg-color: var(--color-white);
+  --icon-link-border-color: var(--color-lightgray);
+  --icon-link-border-size: 1px;
+  --icon-link--active-bg-color: var(--color-absolutezero);
+  --icon-link--active-border-color: var(--color-absolutezero);
+  --icon-link--hover-bg-color: var(--color-bgblue-hover);
+  --icon-link--hover-border-color: var(--color-lightgray-o-80);
   /**
     * Vertical Tabs.
     */
diff --git a/core/themes/claro/css/components/dropbutton.css b/core/themes/claro/css/components/dropbutton.css
index 6d297e2f73..7757aa17e5 100644
--- a/core/themes/claro/css/components/dropbutton.css
+++ b/core/themes/claro/css/components/dropbutton.css
@@ -14,14 +14,6 @@
  *    contrast mode Firefox.
  */
 
-:root {
-  /**
-  * Dropbutton
-  */
-  /* Variant variables: small. */
-  /* Variant variables: extra small. */
-}
-
 .dropbutton-wrapper {
   display: inline-flex;
   border-radius: 2px;
diff --git a/core/themes/claro/css/components/dropbutton.pcss.css b/core/themes/claro/css/components/dropbutton.pcss.css
index 2da904bd59..ca8d5a1b67 100644
--- a/core/themes/claro/css/components/dropbutton.pcss.css
+++ b/core/themes/claro/css/components/dropbutton.pcss.css
@@ -9,29 +9,6 @@
 
 @import "../base/variables.pcss.css";
 
-:root {
-  /**
-  * Dropbutton
-  */
-  --dropbutton-spacing-size: var(--space-m);
-  --dropbutton-font-size: var(--font-size-base);
-  --dropbutton-line-height: var(--space-m);
-  --dropbutton-toggle-size: 3rem;
-  --dropbutton-border-size: 1px;
-  --dropbutton-toggle-size-spacing: var(--dropbutton-border-size);
-  --dropbutton-border-radius-size: 2px;
-  /* Variant variables: small. */
-  --dropbutton-small-spacing-size: 0.625rem;
-  --dropbutton-small-font-size: var(--font-size-xs);
-  --dropbutton-small-line-height: 0.75rem;
-  --dropbutton-small-toggle-size: calc((2 * var(--dropbutton-small-spacing-size)) + var(--dropbutton-small-line-height));
-  /* Variant variables: extra small. */
-  --dropbutton-extrasmall-spacing-size: 0.375rem;
-  --dropbutton-extrasmall-font-size: var(--font-size-xs);
-  --dropbutton-extrasmall-line-height: 0.75rem;
-  --dropbutton-extrasmall-toggle-size: calc((2 * var(--dropbutton-extrasmall-spacing-size)) + var(--dropbutton-extrasmall-line-height));
-}
-
 .dropbutton-wrapper {
   display: inline-flex;
   border-radius: var(--button-border-radius-size);
diff --git a/core/themes/claro/css/components/icon-link.css b/core/themes/claro/css/components/icon-link.css
index 0d1b84eade..edc6b57409 100644
--- a/core/themes/claro/css/components/icon-link.css
+++ b/core/themes/claro/css/components/icon-link.css
@@ -30,6 +30,7 @@
 }
 
 .icon-link:focus {
+  outline: none;
   box-shadow: 0 0 0 1.5px #fff, 0 0 0 3.5px #26a769;
 }
 
diff --git a/core/themes/claro/css/components/icon-link.pcss.css b/core/themes/claro/css/components/icon-link.pcss.css
index cdbfc8e685..48a27b48e2 100644
--- a/core/themes/claro/css/components/icon-link.pcss.css
+++ b/core/themes/claro/css/components/icon-link.pcss.css
@@ -31,6 +31,7 @@
 }
 
 .icon-link:focus {
+  outline: none;
   box-shadow: 0 0 0 1.5px var(--color-white), 0 0 0 3.5px var(--color-focus);
 }
 
diff --git a/core/themes/claro/css/theme/contextual.icons.theme.css b/core/themes/claro/css/theme/contextual.icons.theme.css
new file mode 100644
index 0000000000..632b2b44ca
--- /dev/null
+++ b/core/themes/claro/css/theme/contextual.icons.theme.css
@@ -0,0 +1,45 @@
+/*
+ * DO NOT EDIT THIS FILE.
+ * See the following change record for more information,
+ * https://www.drupal.org/node/3084859
+ * @preserve
+ */
+
+/**
+ * @file
+ * Styling for contextual module icons.
+ */
+
+.contextual__trigger,
+.contextual__trigger:focus {
+  width: 2rem;
+  height: 2rem;
+}
+
+.contextual__trigger::after {
+  position: absolute;
+  top: calc(0.5rem - 1px);
+  left: calc(0.5rem - 1px);
+  width: 1rem;
+  height: 1rem;
+  content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cg fill='%23545560'%3e%3cpath d='M14.545 3.042l-1.586-1.585a1.003 1.003 0 00-1.414 0L10.252 2.75l3 3 1.293-1.293a1.004 1.004 0 000-1.415zM5.25 13.751l-3-3 6.998-6.998 3 3zM.908 14.775c-.087.262.055.397.316.312l2.001-.667-1.65-1.646-.667 2.001z'/%3e%3c/g%3e%3c/svg%3e");
+}
+
+.contextual__trigger:active::after,
+.open > .contextual__trigger::after {
+  content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cg%3e%3cpath fill='%23ffffff' d='M14.545 3.042l-1.586-1.585c-.389-.389-1.025-.389-1.414 0l-1.293 1.293 3 3 1.293-1.293c.389-.389.389-1.026 0-1.415z'/%3e%3crect fill='%23ffffff' x='5.129' y='3.8' transform='matrix(-.707 -.707 .707 -.707 6.189 20.064)' width='4.243' height='9.899'/%3e%3cpath fill='%23ffffff' d='M.908 14.775c-.087.262.055.397.316.312l2.001-.667-1.65-1.646-.667 2.001z'/%3e%3c/g%3e%3c/svg%3e");
+}
+
+@media screen and (-ms-high-contrast: active) {
+  .contextual__trigger::after {
+    content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cg%3e%3cpath fill='%23ffffff' d='M14.545 3.042l-1.586-1.585c-.389-.389-1.025-.389-1.414 0l-1.293 1.293 3 3 1.293-1.293c.389-.389.389-1.026 0-1.415z'/%3e%3crect fill='%23ffffff' x='5.129' y='3.8' transform='matrix(-.707 -.707 .707 -.707 6.189 20.064)' width='4.243' height='9.899'/%3e%3cpath fill='%23ffffff' d='M.908 14.775c-.087.262.055.397.316.312l2.001-.667-1.65-1.646-.667 2.001z'/%3e%3c/g%3e%3c/svg%3e");
+  }
+  .contextual__trigger:active,
+  .open > .contextual__trigger {
+    background-color: #fff;
+  }
+  .contextual__trigger:active::after,
+  .open > .contextual__trigger::after {
+    content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cg%3e%3cpath fill='%23000000' d='M14.545 3.042l-1.586-1.585c-.389-.389-1.025-.389-1.414 0l-1.293 1.293 3 3 1.293-1.293c.389-.389.389-1.026 0-1.415z'/%3e%3crect fill='%23000000' x='5.129' y='3.8' transform='matrix(-.707 -.707 .707 -.707 6.189 20.064)' width='4.243' height='9.899'/%3e%3cpath fill='%23000000' d='M.908 14.775c-.087.262.055.397.316.312l2.001-.667-1.65-1.646-.667 2.001z'/%3e%3c/g%3e%3c/svg%3e");
+  }
+}
diff --git a/core/themes/claro/css/theme/contextual.icons.theme.pcss.css b/core/themes/claro/css/theme/contextual.icons.theme.pcss.css
new file mode 100644
index 0000000000..5696fba33b
--- /dev/null
+++ b/core/themes/claro/css/theme/contextual.icons.theme.pcss.css
@@ -0,0 +1,47 @@
+/**
+ * @file
+ * Styling for contextual module icons.
+ */
+
+@import "../base/variables.pcss.css";
+
+:root {
+  --contextual-trigger-width: 2rem;
+  --contextual-trigger-height: 2rem;
+  --contextual-icon-width: calc(var(--contextual-trigger-width) / 2);
+  --contextual-icon-height: calc(var(--contextual-trigger-height) / 2);
+}
+
+.contextual__trigger,
+.contextual__trigger:focus {
+  width: var(--contextual-trigger-width);
+  height: var(--contextual-trigger-height);
+}
+
+.contextual__trigger::after {
+  position: absolute;
+  top: calc(var(--contextual-icon-height) / 2 - var(--icon-link-border-size));
+  left: calc(var(--contextual-icon-width) / 2 - var(--icon-link-border-size));
+  width: var(--contextual-icon-width);
+  height: var(--contextual-icon-height);
+  content: url(../../images/icons/545560/pencil.svg);
+}
+
+.contextual__trigger:active::after,
+.open > .contextual__trigger::after {
+  content: url(../../images/icons/ffffff/pencil.svg);
+}
+
+@media screen and (-ms-high-contrast: active) {
+  .contextual__trigger::after {
+    content: url(../../images/icons/ffffff/pencil.svg);
+  }
+  .contextual__trigger:active,
+  .open > .contextual__trigger {
+    background-color: var(--color-white);
+  }
+  .contextual__trigger:active::after,
+  .open > .contextual__trigger::after {
+    content: url(../../images/icons/000000/pencil.svg);
+  }
+}
diff --git a/core/themes/claro/css/theme/contextual.theme.css b/core/themes/claro/css/theme/contextual.theme.css
new file mode 100644
index 0000000000..20e518c493
--- /dev/null
+++ b/core/themes/claro/css/theme/contextual.theme.css
@@ -0,0 +1,116 @@
+/*
+ * DO NOT EDIT THIS FILE.
+ * See the following change record for more information,
+ * https://www.drupal.org/node/3084859
+ * @preserve
+ */
+
+/**
+ * @file
+ * Styling for contextual module.
+ */
+
+:root { /* 4px */
+}
+
+.contextual {
+  position: absolute;
+  z-index: 500;
+  top: 0.25rem;
+  right: 0.25rem;
+}
+
+[dir="rtl"] .contextual {
+  right: auto;
+  left: 0.25rem;
+}
+
+.contextual.open {
+  z-index: 501;
+}
+
+.contextual-region.focus {
+  outline: 1px dashed #8e929c;
+  outline-offset: 1px;
+}
+
+.contextual__trigger {
+  position: relative;
+  float: right; /* LTR */
+  overflow: hidden;
+  cursor: pointer;
+}
+
+[dir="rtl"] .contextual__trigger {
+  float: left;
+}
+
+.open > .contextual__trigger {
+  z-index: 2;
+}
+
+.contextual-links {
+  position: relative;
+  top: 0.125rem; /* 2px */
+  right: 0; /* LTR */
+  float: right; /* LTR */
+  clear: both;
+  margin: 0;
+  white-space: nowrap;
+  border: 1px solid #d4d4d8;
+  border-radius: 2px;
+  background: #fff;
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
+}
+
+[dir="rtl"] .contextual-links {
+  right: auto;
+  left: 0;
+  float: left;
+}
+
+.contextual-links a {
+  position: relative;
+  display: block;
+  box-sizing: border-box;
+  width: 100%;
+  padding: calc(1rem - 1px);
+  color: #545560;
+  border-radius: 2px;
+  background: #fff;
+  font-size: 1rem;
+  line-height: 1rem;
+  -webkit-font-smoothing: antialiased;
+}
+
+[dir="rtl"] .contextual-links a {
+  text-align: right;
+}
+
+.contextual-links a,
+.contextual-links a:hover {
+  text-decoration: none;
+}
+
+.touchevents .contextual-links a {
+  font-size: large;
+}
+
+.contextual-links li {
+  list-style: none;
+}
+
+.contextual-links li a:hover {
+  color: #222330;
+  background: #f3f4f9;
+}
+
+.contextual-links li a:focus {
+  z-index: 2;
+  box-shadow: inset 0 0 0 1px #26a769, 0 0 0 1px #26a769;
+}
+
+.contextual-links li a:active,
+.contextual-links li [aria-pressed="true"] {
+  color: #545560;
+}
diff --git a/core/themes/claro/css/theme/contextual.theme.pcss.css b/core/themes/claro/css/theme/contextual.theme.pcss.css
new file mode 100644
index 0000000000..390eb91559
--- /dev/null
+++ b/core/themes/claro/css/theme/contextual.theme.pcss.css
@@ -0,0 +1,110 @@
+/**
+ * @file
+ * Styling for contextual module.
+ */
+
+@import "../base/variables.pcss.css";
+
+:root {
+  --contextual-z-index: 500;
+  --contextual-icon-spacing-size: 0.25rem; /* 4px */
+  --contextual-outline-bg-color: var(--color-grayblue);
+  --contextual-links-spacing-size: var(--dropbutton-spacing-size);
+  --contextual-links-border-size: var(--dropbutton-border-size);
+  --contextual-links-border-radius-size: var(--dropbutton-border-radius-size);
+  --contextual-links-font-size: var(--dropbutton-font-size);
+  --contextual-links-line-height: var(--dropbutton-line-height);
+}
+
+.contextual {
+  position: absolute;
+  z-index: var(--contextual-z-index);
+  top: var(--contextual-icon-spacing-size);
+  right: var(--contextual-icon-spacing-size);
+}
+[dir="rtl"] .contextual {
+  right: auto;
+  left: var(--contextual-icon-spacing-size);
+}
+
+.contextual.open {
+  z-index: calc(var(--contextual-z-index) + 1);
+}
+
+.contextual-region.focus {
+  outline: 1px dashed var(--contextual-outline-bg-color);
+  outline-offset: 1px;
+}
+
+.contextual__trigger {
+  position: relative;
+  float: right; /* LTR */
+  overflow: hidden;
+  cursor: pointer;
+}
+[dir="rtl"] .contextual__trigger {
+  float: left;
+}
+.open > .contextual__trigger {
+  z-index: 2;
+}
+
+.contextual-links {
+  position: relative;
+  top: 0.125rem; /* 2px */
+  right: 0; /* LTR */
+  float: right; /* LTR */
+  clear: both;
+  margin: 0;
+  white-space: nowrap;
+  border: var(--contextual-links-border-size) solid var(--color-lightgray);
+  border-radius: var(--dropbutton-border-radius-size);
+  background: var(--color-white);
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
+}
+[dir="rtl"] .contextual-links {
+  right: auto;
+  left: 0;
+  float: left;
+}
+
+.contextual-links a {
+  position: relative;
+  display: block;
+  box-sizing: border-box;
+  width: 100%;
+  padding: calc(var(--contextual-links-spacing-size) - var(--contextual-links-border-size));
+  color: var(--color-davysgray);
+  border-radius: var(--contextual-links-border-radius-size);
+  background: var(--color-white);
+  font-size: var(--contextual-links-font-size);
+  line-height: var(--contextual-links-line-height);
+  -webkit-font-smoothing: antialiased;
+}
+[dir="rtl"] .contextual-links a {
+  text-align: right;
+}
+.contextual-links a,
+.contextual-links a:hover {
+  text-decoration: none;
+}
+.touchevents .contextual-links a {
+  font-size: large;
+}
+
+.contextual-links li {
+  list-style: none;
+}
+
+.contextual-links li a:hover {
+  color: var(--color-text);
+  background: var(--color-whitesmoke);
+}
+.contextual-links li a:focus {
+  z-index: 2;
+  box-shadow: inset 0 0 0 1px var(--color-focus), 0 0 0 1px var(--color-focus);
+}
+.contextual-links li a:active,
+.contextual-links li [aria-pressed="true"] {
+  color: var(--color-davysgray);
+}
diff --git a/core/themes/claro/images/icons/000000/pencil.svg b/core/themes/claro/images/icons/000000/pencil.svg
new file mode 100644
index 0000000000..c3d7892796
--- /dev/null
+++ b/core/themes/claro/images/icons/000000/pencil.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><g><path fill="#000000" d="M14.545 3.042l-1.586-1.585c-.389-.389-1.025-.389-1.414 0l-1.293 1.293 3 3 1.293-1.293c.389-.389.389-1.026 0-1.415z"/><rect fill="#000000" x="5.129" y="3.8" transform="matrix(-.707 -.707 .707 -.707 6.189 20.064)" width="4.243" height="9.899"/><path fill="#000000" d="M.908 14.775c-.087.262.055.397.316.312l2.001-.667-1.65-1.646-.667 2.001z"/></g></svg>
diff --git a/core/themes/claro/images/icons/ffffff/pencil.svg b/core/themes/claro/images/icons/ffffff/pencil.svg
new file mode 100644
index 0000000000..229e480913
--- /dev/null
+++ b/core/themes/claro/images/icons/ffffff/pencil.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g><path fill="#ffffff" d="M14.545 3.042l-1.586-1.585c-.389-.389-1.025-.389-1.414 0l-1.293 1.293 3 3 1.293-1.293c.389-.389.389-1.026 0-1.415z"/><rect fill="#ffffff" x="5.129" y="3.8" transform="matrix(-.707 -.707 .707 -.707 6.189 20.064)" width="4.243" height="9.899"/><path fill="#ffffff" d="M.908 14.775c-.087.262.055.397.316.312l2.001-.667-1.65-1.646-.667 2.001z"/></g></svg>
diff --git a/core/themes/claro/js/contextual.es6.js b/core/themes/claro/js/contextual.es6.js
new file mode 100644
index 0000000000..442b4c5273
--- /dev/null
+++ b/core/themes/claro/js/contextual.es6.js
@@ -0,0 +1,52 @@
+/**
+ * @file
+ * Theme overrides for contextual trigger.
+ */
+
+((Drupal) => {
+  /**
+   * Override Contextual's AuralView render() so contextual trigger text can be
+   * processed with a theme function.
+   *
+   * @todo this entire override can be removed after
+   *   https://drupal.org/node/3172956
+   */
+  Drupal.contextual.AuralView = Drupal.contextual.AuralView.extend({
+    render: function render() {
+      const isOpen = this.model.get('isOpen');
+      this.$el.find('.contextual-links').prop('hidden', !isOpen);
+      const triggerText = Drupal.t('@action @title configuration options', {
+        '@action': !isOpen
+          ? this.options.strings.open
+          : this.options.strings.close,
+        '@title': this.model.get('title'),
+      });
+
+      this.$el
+        .find('.trigger')
+        .html(Drupal.theme('contextualTriggerText', triggerText))
+        .attr('aria-pressed', isOpen);
+    },
+  });
+
+  /**
+   * Constructs a contextual trigger element.
+   *
+   * @return {string}
+   *   A string representing a DOM fragment.
+   */
+  Drupal.theme.contextualTrigger = () =>
+    `<button class="contextual__trigger trigger visually-hidden focusable icon-link icon-link--small" type="button"></button>`;
+
+  /**
+   * Contextual link trigger text, typically seen only by screenreaders.
+   *
+   * @param {string} text
+   *   The contextual link trigger text.
+   *
+   * @return {string}
+   *   A string representing a DOM fragment.
+   */
+  Drupal.theme.contextualTriggerText = (text) =>
+    `<span class="visually-hidden">${text}</span>`;
+})(Drupal, Backbone);
diff --git a/core/themes/claro/js/contextual.js b/core/themes/claro/js/contextual.js
new file mode 100644
index 0000000000..e8ae50a9b7
--- /dev/null
+++ b/core/themes/claro/js/contextual.js
@@ -0,0 +1,28 @@
+/**
+* DO NOT EDIT THIS FILE.
+* See the following change record for more information,
+* https://www.drupal.org/node/2815083
+* @preserve
+**/
+
+(function (Drupal) {
+  Drupal.contextual.AuralView = Drupal.contextual.AuralView.extend({
+    render: function render() {
+      var isOpen = this.model.get('isOpen');
+      this.$el.find('.contextual-links').prop('hidden', !isOpen);
+      var triggerText = Drupal.t('@action @title configuration options', {
+        '@action': !isOpen ? this.options.strings.open : this.options.strings.close,
+        '@title': this.model.get('title')
+      });
+      this.$el.find('.trigger').html(Drupal.theme('contextualTriggerText', triggerText)).attr('aria-pressed', isOpen);
+    }
+  });
+
+  Drupal.theme.contextualTrigger = function () {
+    return "<button class=\"contextual__trigger trigger visually-hidden focusable icon-link icon-link--small\" type=\"button\"></button>";
+  };
+
+  Drupal.theme.contextualTriggerText = function (text) {
+    return "<span class=\"visually-hidden\">".concat(text, "</span>");
+  };
+})(Drupal, Backbone);
\ No newline at end of file
