diff --git a/core/modules/system/src/EventSubscriber/ConfigCacheTag.php b/core/modules/system/src/EventSubscriber/ConfigCacheTag.php
index 30f7d595ab..68050aa85a 100644
--- a/core/modules/system/src/EventSubscriber/ConfigCacheTag.php
+++ b/core/modules/system/src/EventSubscriber/ConfigCacheTag.php
@@ -60,6 +60,12 @@ public function onSave(ConfigCrudEvent $event) {
       $this->cacheTagsInvalidator->invalidateTags(['rendered']);
     }
 
+    // Library and template overrides potentially change for the default theme
+    // when the admin theme is changed.
+    if ($config_name === 'system.theme' && $event->isChanged('admin')) {
+      $this->cacheTagsInvalidator->invalidateTags(['library_info', 'theme_registry']);
+    }
+
     // Theme-specific settings, check if this matches a theme settings
     // configuration object (THEME_NAME.settings), in that case, clear the
     // rendered cache tag.
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 064afafbce..68f51075fa 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -1250,3 +1250,27 @@ function system_modules_uninstalled($modules) {
     }
   }
 }
+
+/**
+ * Determines if Claro is the admin theme but not the active theme.
+ *
+ * @return bool
+ *   TRUE if Claro is the admin theme but not the active theme.
+ */
+function _system_is_claro_admin_and_not_active() {
+  $admin_theme = \Drupal::configFactory()->get('system.theme')->get('admin');
+  $active_theme = \Drupal::theme()->getActiveTheme()->getName();
+  return $active_theme !== 'claro' && $admin_theme === 'claro';
+}
+
+/**
+ * Implements hook_library_info_alter().
+ */
+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 === '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 44049c582a..d9197ee3c4 100644
--- a/core/themes/claro/claro.info.yml
+++ b/core/themes/claro/claro.info.yml
@@ -69,6 +69,12 @@ libraries-override:
       component:
         assets/vendor/jquery.ui/themes/base/dialog.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:
@@ -105,6 +111,8 @@ libraries-extend:
     - claro/vertical-tabs
   core/jquery.ui:
     - claro/claro.jquery.ui
+  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 02ed758bc1..5a7a2415cd 100644
--- a/core/themes/claro/claro.libraries.yml
+++ b/core/themes/claro/claro.libraries.yml
@@ -186,6 +186,19 @@ checkbox:
   dependencies:
     - core/drupal
 
+contextual-trigger:
+  version: VERSION
+  js:
+    js/contextual.js: {}
+  dependencies:
+    - claro/icon-link
+
+icon-link:
+  version: VERSION
+  css:
+    component:
+      css/components/icon-link.css: {}
+
 dropbutton:
   version: VERSION
   js:
diff --git a/core/themes/claro/claro.theme b/core/themes/claro/claro.theme
index 5d735acb6b..660621663c 100644
--- a/core/themes/claro/claro.theme
+++ b/core/themes/claro/claro.theme
@@ -1413,3 +1413,63 @@ function claro_preprocess_links__media_library_menu(array &$variables) {
     }
   }
 }
+
+/**
+ * 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
+ * Contextual's library config so Claro's Contextual stylesheets are used.
+ *
+ * @see system_library_info_alter()
+ */
+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_contexutal_overrides = $claro_info['libraries-override']['contextual/drupal.contextual-links'];
+    $claro_contexutal_extend = $claro_info['libraries-extend']['contextual/drupal.contextual-links'];
+
+    foreach ($claro_contexutal_overrides['css'] as $concern => $overrides) {
+      foreach ($claro_contexutal_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_contexutal_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);
+      }
+    }
+  }
+}
+
+/**
+ * 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 69dc17481c..08da562244 100644
--- a/core/themes/claro/css/base/variables.css
+++ b/core/themes/claro/css/base/variables.css
@@ -49,4 +49,10 @@
   /**
    * Breadcrumb.
    */
+  /**
+  * Dropbutton.
+  */
+  /**
+   * Icon links.
+   */
 }
diff --git a/core/themes/claro/css/base/variables.pcss.css b/core/themes/claro/css/base/variables.pcss.css
index c0987bb801..0103ef0418 100644
--- a/core/themes/claro/css/base/variables.pcss.css
+++ b/core/themes/claro/css/base/variables.pcss.css
@@ -196,4 +196,32 @@
    * 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);
 }
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
new file mode 100644
index 0000000000..d33f0f7af1
--- /dev/null
+++ b/core/themes/claro/css/components/icon-link.css
@@ -0,0 +1,36 @@
+/*
+ * DO NOT EDIT THIS FILE.
+ * See the following change record for more information,
+ * https://www.drupal.org/node/3084859
+ * @preserve
+ */
+
+/**
+ * @file
+ * Icon link component.
+ */
+
+.icon-link {
+  border: 1px solid #d4d4d8;
+  border-radius: 50%;
+  background-color: #fff;
+}
+
+.icon-link:hover {
+  border-color: rgba(212, 212, 218, 0.8);
+  background-color: #f0f5fd;
+}
+
+.icon-link:focus {
+  box-shadow: 0 0 0 1.5px #fff, 0 0 0 3.5px #26a769;
+}
+
+.icon-link:active,
+.open > .icon-link {
+  border-color: #003cc5;
+  background-color: #003cc5;
+}
+
+.icon-link--small:focus {
+  box-shadow: 0 0 0 1px #fff, 0 0 0 3px #26a769;
+}
diff --git a/core/themes/claro/css/components/icon-link.pcss.css b/core/themes/claro/css/components/icon-link.pcss.css
new file mode 100644
index 0000000000..4ce9efa475
--- /dev/null
+++ b/core/themes/claro/css/components/icon-link.pcss.css
@@ -0,0 +1,31 @@
+/**
+ * @file
+ * Icon link component.
+ */
+
+@import "../base/variables.pcss.css";
+
+.icon-link {
+  border: var(--icon-link-border-size) solid var(--icon-link-border-color);
+  border-radius: 50%;
+  background-color: var(--icon-link-bg-color);
+}
+
+.icon-link:hover {
+  border-color: var(--icon-link--hover-border-color);
+  background-color: var(--icon-link--hover-bg-color);
+}
+
+.icon-link:focus {
+  box-shadow: 0 0 0 1.5px var(--color-white), 0 0 0 3.5px var(--color-focus);
+}
+
+.icon-link:active,
+.open > .icon-link {
+  border-color: var(--icon-link--active-border-color);
+  background-color: var(--icon-link--active-bg-color);
+}
+
+.icon-link--small:focus {
+  box-shadow: 0 0 0 1px var(--color-white), 0 0 0 3px 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..af05022046
--- /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(1rem - var(--icon-link-border-size));
+  left: calc(1rem - var(--icon-link-border-size));
+  width: 1rem;
+  height: 1rem;
+  content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cg%3e%3cpath fill='%23545560' 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='%23545560' 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='%23545560' 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::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: var(--color-white);
+  }
+  .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..dfe0fcf247
--- /dev/null
+++ b/core/themes/claro/css/theme/contextual.icons.theme.pcss.css
@@ -0,0 +1,45 @@
+/**
+ * @file
+ * Styling for contextual module icons.
+ */
+
+: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) - var(--icon-link-border-size));
+  left: calc(var(--contextual-icon-width) - 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/545560/pencil.svg b/core/themes/claro/images/icons/545560/pencil.svg
new file mode 100644
index 0000000000..2808993ddf
--- /dev/null
+++ b/core/themes/claro/images/icons/545560/pencil.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g><path fill="#545560" 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="#545560" x="5.129" y="3.8" transform="matrix(-.707 -.707 .707 -.707 6.189 20.064)" width="4.243" height="9.899"/><path fill="#545560" 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..30ea605779
--- /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
