diff --git a/config/schema/experience_builder.schema.yml b/config/schema/experience_builder.schema.yml
index b3c76f7716f54998b880fc386e508372f6288841..e3352d5bc99ae19fcc9ebf7e126ccfa844f5ab38 100644
--- a/config/schema/experience_builder.schema.yml
+++ b/config/schema/experience_builder.schema.yml
@@ -28,6 +28,15 @@ experience_builder.component.*:
         Length:
           # @see \Drupal\Core\Config\Entity\ConfigEntityStorage::MAX_ID_LENGTH
           max: 166
+    provider:
+      type: string
+      label: 'Name of the module or theme providing this component, or null if provided via something else'
+      nullable: true
+      constraints:
+        NotBlank:
+          allowNull: true
+        ExtensionName: []
+        Callback: ['\Drupal\experience_builder\Entity\Component', providerExists]
     source:
       type: string
       label: 'Source plugin'
diff --git a/docs/components.md b/docs/components.md
index c90582957d78c4ee094deef0480f79653a5c1d5c..6744246213f3860b8df8a03157675b906f880db3 100644
--- a/docs/components.md
+++ b/docs/components.md
@@ -93,7 +93,12 @@ Therefore, it only makes sense to surface _block plugins_ as XB `component`s.
 defined  using config schema (`type: block.settings.<PLUGIN ID>`). Defaults are present as the
 `::defaultConfiguration()` method  on the PHP plugin class.
 
-`Block` `component`s specify the accepted explicit inputs
+`Block` `component`s DO accept implicit inputs, in two ways even:
+1. Logic in the block plugin can fetch data — through database queries, HTTP requests, anything.
+2. Contexts. Not yet supported. (⚠️ handling contexts is still TBD in [#3485502](https://www.drupal.org/project/experience_builder/issues/3485502))
+
+`Block` `component`s specify the accepted explicit inputs. They typically allow influencing the logic in the block
+plugin. These explicit inputs can hence be seen as knobs and levers to adjust what the underlying block plugin does.
 
 `Block` DOES provide an input UX (`BlockPluginInterface::buildConfigurationForm()`), so its `Component Source Plugin`
 simply reuses that.
@@ -128,6 +133,8 @@ refer to "blocks" and not "block plugins", under the hood, they actually _are_ b
 3.1.1 above](#3.1.1)). See [section 3.2 `JavaScriptComponent config entity` in the `XB Config Management`
 doc](config-management.md#3.2) for all details.
 
+`JS` `component`s DO NOT accept implicit inputs.
+
 `JS` DOES NOT provide an input UX, so its `Component Source Plugin` must do so on its behalf; and does so by matching
 available field types against the JSON schema of its explicit inputs ("props"). For details, see the [`XB Shape Matching
 into Field Types` doc](shape-matching-into-field-types.md). (It shares this infrastructure with the `SDC` `Component
diff --git a/experience_builder.module b/experience_builder.module
index ca9833a8d13d74cfa7419bbe51f5833c9f7d890c..4e5efc170f4c6d32dd80164f0bf75966d304c062 100644
--- a/experience_builder.module
+++ b/experience_builder.module
@@ -265,6 +265,7 @@ function experience_builder_block_alter(array &$definitions): void {
         'label' => (string) new TranslatableMarkup('@label block', ['@label' => $definition['admin_label']]),
         'category' => (string) $definition['category'],
         'source' => BlockComponent::SOURCE_PLUGIN_ID,
+        'provider' => $definition['provider'],
         'settings' => [
           'plugin_id' => $id,
           // We are using strict config schema validation, so we need to provide valid default settings for each block.
diff --git a/openapi.yml b/openapi.yml
index 2aa4d5d3b17970056a36c643f8a40feb6ac3cdca..7df1084fd395b4ccc263ab19bd345f7fbb448db4 100644
--- a/openapi.yml
+++ b/openapi.yml
@@ -164,6 +164,7 @@ paths:
                       name: Call to Action
                       source: Module component
                       category: Buttons
+                      library: extension_components
                       transforms:
                         text:
                           extractMainPropertyName:
@@ -1089,6 +1090,7 @@ components:
         - id
         - category
         - default_markup
+        - library
       properties:
         name:
           type: string
@@ -1099,6 +1101,18 @@ components:
         category:
           type: string
           description: The component category
+        library:
+          type: string
+          description: Library ID
+          enum:
+            # Provided by Experience Builder
+            - elements
+            # Provided by Modules
+            - extension_components
+            # Blocks
+            - dynamic_components
+            # SDCs from the Active Theme + JavaScript Code Components
+            - primary_components
         transforms:
           type: object
           description: Transform configuration
diff --git a/src/Attribute/ComponentSource.php b/src/Attribute/ComponentSource.php
index f714ac961193055ac6184954fa449f8f793ec8cb..0c3e17600030f0b3f0ecfbad34eb7a483e097d02 100644
--- a/src/Attribute/ComponentSource.php
+++ b/src/Attribute/ComponentSource.php
@@ -25,6 +25,7 @@ final class ComponentSource extends Plugin {
   public function __construct(
     public readonly string $id,
     public readonly TranslatableMarkup $label,
+    public readonly bool $supportsImplicitInputs,
     public readonly ?string $deriver = NULL,
   ) {
   }
diff --git a/src/Controller/ApiConfigControllers.php b/src/Controller/ApiConfigControllers.php
index 93b303a43fe82bfdb6470cc7ac9c072d72325387..62d02fc2ceee52d74684e5a4a48ea4b85805c940 100644
--- a/src/Controller/ApiConfigControllers.php
+++ b/src/Controller/ApiConfigControllers.php
@@ -84,12 +84,13 @@ final class ApiConfigControllers extends ApiControllerBase {
     if (!$xb_config_entity_type->get('xb_visible_when_disabled')) {
       $query->condition('status', TRUE);
     }
-    /** @var array<\Drupal\experience_builder\Entity\XbHttpApiEligibleConfigEntityInterface> $config_entities */
-    $config_entities = $storage->loadMultiple($query->execute());
 
     $query_cacheability = (new CacheableMetadata())
       ->addCacheContexts($xb_config_entity_type->getListCacheContexts())
       ->addCacheTags($xb_config_entity_type->getListCacheTags());
+    $xb_config_entity_type->getClass()::refineListQuery($query, $query_cacheability);
+    /** @var array<\Drupal\experience_builder\Entity\XbHttpApiEligibleConfigEntityInterface> $config_entities */
+    $config_entities = $storage->loadMultiple($query->execute());
 
     $normalizations = [];
     $normalizations_cacheability = new CacheableMetadata();
diff --git a/src/Entity/AssetLibrary.php b/src/Entity/AssetLibrary.php
index 7cf7104c314580d63acefcebf71d45d15d61933e..727266ced65be4dfa0c6abb357a4a834dc6b3497 100644
--- a/src/Entity/AssetLibrary.php
+++ b/src/Entity/AssetLibrary.php
@@ -4,7 +4,9 @@ declare(strict_types=1);
 
 namespace Drupal\experience_builder\Entity;
 
+use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
 use Drupal\Core\Config\Entity\ConfigEntityBase;
+use Drupal\Core\Entity\Query\QueryInterface;
 use Drupal\experience_builder\ClientSideRepresentation;
 
 /**
@@ -73,4 +75,11 @@ final class AssetLibrary extends ConfigEntityBase implements XbHttpApiEligibleCo
     return $data;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function refineListQuery(QueryInterface &$query, RefinableCacheableDependencyInterface $cacheability): void {
+    // Nothing to do.
+  }
+
 }
diff --git a/src/Entity/Component.php b/src/Entity/Component.php
index 2ab3d8bced00b10d69fe5a6be3391bf3bf5160cb..62fcf3facf16091bd507bf26926c2b13880e2aa1 100644
--- a/src/Entity/Component.php
+++ b/src/Entity/Component.php
@@ -4,7 +4,11 @@ declare(strict_types=1);
 
 namespace Drupal\experience_builder\Entity;
 
+use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
 use Drupal\Core\Config\Entity\ConfigEntityBase;
+use Drupal\Core\Entity\Query\QueryInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Extension\ThemeHandlerInterface;
 use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection;
 use Drupal\Core\Render\Markup;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
@@ -46,6 +50,7 @@ use Drupal\experience_builder\ComponentSource\ComponentSourceManager;
  *      "label",
  *      "id",
  *      "source",
+ *      "provider",
  *      "category",
  *      "settings",
  *    },
@@ -75,6 +80,16 @@ final class Component extends ConfigEntityBase implements ComponentInterface, Xb
    */
   protected string $source;
 
+  /**
+   * The provider of this component: a valid module or theme name, or NULL.
+   *
+   * NULL must be used to signal it's not provided by an extension. This is used
+   * for "code components" for example — which are provided by entities.
+   *
+   * @see \Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\JsComponent
+   */
+  protected ?string $provider;
+
   /**
    * The human-readable category of the component.
    */
@@ -168,10 +183,17 @@ final class Component extends ConfigEntityBase implements ComponentInterface, Xb
   }
 
   /**
-   * {@inheritdoc}
+   * Works around the `ExtensionExists` constraint requiring a fixed type.
+   *
+   * @see \Drupal\Core\Extension\Plugin\Validation\Constraint\ExtensionExistsConstraintValidator
+   * @see https://www.drupal.org/node/3353397
    */
-  protected function providerExists(string $provider): bool {
-    return $this->moduleHandler()->moduleExists($provider) || $this->themeHandler()->themeExists($provider);
+  public static function providerExists(?string $provider): bool {
+    if (is_null($provider)) {
+      return TRUE;
+    }
+    $container = \Drupal::getContainer();
+    return $container->get(ModuleHandlerInterface::class)->moduleExists($provider) || $container->get(ThemeHandlerInterface::class)->themeExists($provider);
   }
 
   /**
@@ -206,18 +228,11 @@ final class Component extends ConfigEntityBase implements ComponentInterface, Xb
     $component_config_entity_uuid = $this->uuid();
     $build['#prefix'] = Markup::create("<!-- xb-start-$component_config_entity_uuid -->");
     $build['#suffix'] = Markup::create("<!-- xb-end-$component_config_entity_uuid -->");
-
-    $info += [
-      'id' => $this->id(),
-      'name' => (string) $this->label(),
-      'category' => (string) $this->getCategory(),
-      'source' => (string) $this->getComponentSource()->getPluginDefinition()['label'],
-    ];
-
     return ClientSideRepresentation::create(
       values: $info + [
         'id' => $this->id(),
         'name' => (string) $this->label(),
+        'library' => $this->computeUiLibrary()->value,
         'category' => (string) $this->getCategory(),
         'source' => (string) $this->getComponentSource()->getPluginDefinition()['label'],
       ],
@@ -225,6 +240,50 @@ final class Component extends ConfigEntityBase implements ComponentInterface, Xb
     )->addCacheableDependency($this);
   }
 
+  /**
+   * Uses heuristics to compute the appropriate "library" in the XB UI.
+   *
+   * Each Component appears in a well-defined "library" in the XB UI. This is a
+   * set of heuristics with a particular decision tree.
+   *
+   * @see https://www.drupal.org/project/experience_builder/issues/3498419#comment-15997505
+   */
+  private function computeUiLibrary(): LibraryEnum {
+    $config = \Drupal::configFactory()->loadMultiple(['core.extension', 'system.theme']);
+    $installed_modules = [
+      'core',
+      ...array_keys($config['core.extension']->get('module')),
+    ];
+    // @see \Drupal\Core\Extension\ThemeHandler::getDefault()
+    $default_theme = $config['system.theme']->get('default');
+
+    // 1. Is the component dynamic (consumes implicit inputs/context or has
+    // logic)?
+    if ($this->getComponentSource()->getPluginDefinition()['supportsImplicitInputs']) {
+      return LibraryEnum::DynamicComponents;
+    }
+
+    // 2. Is the component provided by a module?
+    if (in_array($this->provider, $installed_modules, TRUE)) {
+      return $this->provider === 'experience_builder'
+        // 2.B Is the providing module XB?
+        ? LibraryEnum::Elements
+        : LibraryEnum::ExtensionComponents;
+    }
+
+    // 3. Is the component provided by the default theme (or its base theme)?
+    if ($this->provider === $default_theme) {
+      return LibraryEnum::PrimaryComponents;
+    }
+
+    // 4. Is the component provided by neither a theme nor a module?
+    if ($this->provider === NULL) {
+      return LibraryEnum::PrimaryComponents;
+    }
+
+    throw new \LogicException('A Component is being normalized that belongs in no XB UI library.');
+  }
+
   /**
    * {@inheritdoc}
    *
@@ -234,6 +293,34 @@ final class Component extends ConfigEntityBase implements ComponentInterface, Xb
     throw new \LogicException('Not supported: read-only for the client side, mutable only on the server side.');
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function refineListQuery(QueryInterface &$query, RefinableCacheableDependencyInterface $cacheability): void {
+    $container = \Drupal::getContainer();
+    $theme_handler = $container->get(ThemeHandlerInterface::class);
+    $installed_themes = array_keys($theme_handler->listInfo());
+    $default_theme = $theme_handler->getDefault();
+
+    // Omit Components provided by installed-but-not-default themes. This keeps
+    // all other Components:
+    // - module-provided ones
+    // - default theme-provided
+    // - provided by something else than an extension, such as an entity.
+    $or_group = $query->orConditionGroup()
+      ->condition('provider', operator: 'NOT IN', value: array_diff($installed_themes, [$default_theme]))
+      ->condition('provider', operator: 'IS NULL');
+    $query->condition($or_group);
+
+    // Reflect the conditions added to the query in the cacheability.
+    $cacheability->addCacheTags([
+      // The set of installed themes is stored in the `core.extension` config.
+      'config:core.extension',
+      // The default theme is stored in the `system.theme` config.
+      'config:system.theme',
+    ]);
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/src/Entity/JavaScriptComponent.php b/src/Entity/JavaScriptComponent.php
index b23e6ad96d874272119c12222f99e5f8bedce612..898c6135a8f6dd7b9a56e6c7e6b93c08f0956197 100644
--- a/src/Entity/JavaScriptComponent.php
+++ b/src/Entity/JavaScriptComponent.php
@@ -4,7 +4,9 @@ declare(strict_types=1);
 
 namespace Drupal\experience_builder\Entity;
 
+use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
 use Drupal\Core\Config\Entity\ConfigEntityBase;
+use Drupal\Core\Entity\Query\QueryInterface;
 use Drupal\experience_builder\ClientSideRepresentation;
 
 /**
@@ -139,6 +141,13 @@ final class JavaScriptComponent extends ConfigEntityBase implements XbHttpApiEli
     ];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function refineListQuery(QueryInterface &$query, RefinableCacheableDependencyInterface $cacheability): void {
+    // Nothing to do.
+  }
+
   /**
    * Code components are not Twig-defined but still aim to match SDC closely.
    *
diff --git a/src/Entity/LibraryEnum.php b/src/Entity/LibraryEnum.php
new file mode 100644
index 0000000000000000000000000000000000000000..2d05f9ce4dcea2dc9301d4e43de5d2fb4315d7e8
--- /dev/null
+++ b/src/Entity/LibraryEnum.php
@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\experience_builder\Entity;
+
+/**
+ * @internal
+ * @see \Drupal\experience_builder\Entity\Component::computeUiLibrary()
+ */
+enum LibraryEnum: string {
+  case Elements = 'elements';
+  case ExtensionComponents = 'extension_components';
+  case DynamicComponents = 'dynamic_components';
+  case PrimaryComponents = 'primary_components';
+}
diff --git a/src/Entity/Pattern.php b/src/Entity/Pattern.php
index ddb23dad5f7cce3e90ff6264b640a4f10494c2ea..c5afbd8d1d2c16d7bb3bdbdf6e306014aa6b4093 100644
--- a/src/Entity/Pattern.php
+++ b/src/Entity/Pattern.php
@@ -5,8 +5,10 @@ declare(strict_types=1);
 namespace Drupal\experience_builder\Entity;
 
 use Drupal\Component\Utility\Random;
+use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
 use Drupal\Core\Config\Entity\ConfigEntityBase;
 use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\Query\QueryInterface;
 use Drupal\experience_builder\ClientSideRepresentation;
 use Drupal\experience_builder\Controller\ApiConfigControllers;
 use Drupal\experience_builder\Controller\ClientServerConversionTrait;
@@ -163,4 +165,11 @@ final class Pattern extends ConfigEntityBase implements XbHttpApiEligibleConfigE
     ] + $other_values;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function refineListQuery(QueryInterface &$query, RefinableCacheableDependencyInterface $cacheability): void {
+    // Nothing to do.
+  }
+
 }
diff --git a/src/Entity/XbHttpApiEligibleConfigEntityInterface.php b/src/Entity/XbHttpApiEligibleConfigEntityInterface.php
index 4a9710cbbcb995bf78b1a3fb76e3bd1371489fdb..369433cd007090ec1a5e79b0173ec7a295da1965 100644
--- a/src/Entity/XbHttpApiEligibleConfigEntityInterface.php
+++ b/src/Entity/XbHttpApiEligibleConfigEntityInterface.php
@@ -4,7 +4,9 @@ declare(strict_types=1);
 
 namespace Drupal\experience_builder\Entity;
 
+use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
 use Drupal\Core\Config\Entity\ConfigEntityInterface;
+use Drupal\Core\Entity\Query\QueryInterface;
 use Drupal\experience_builder\ClientSideRepresentation;
 
 /**
@@ -33,4 +35,17 @@ interface XbHttpApiEligibleConfigEntityInterface extends ConfigEntityInterface {
    */
   public static function denormalizeFromClientSide(array $data): array;
 
+  /**
+   * Allows the config entity query that generates the listing to be refined.
+   *
+   * @param \Drupal\Core\Entity\Query\QueryInterface $query
+   *   The config entity query to refine, passed by reference.
+   * @param \Drupal\Core\Cache\RefinableCacheableDependencyInterface $cacheability
+   *   The cacheability of the given query, to be refined to match the
+   *   refinements made to the query.
+   *
+   * @return void
+   */
+  public static function refineListQuery(QueryInterface &$query, RefinableCacheableDependencyInterface $cacheability): void;
+
 }
diff --git a/src/Plugin/ExperienceBuilder/ComponentSource/BlockComponent.php b/src/Plugin/ExperienceBuilder/ComponentSource/BlockComponent.php
index b1a3de55a18c2092885f8503898d5a048a5d286d..a9ae5fadf8bfd46d9c8074a3f95501396ecba219 100644
--- a/src/Plugin/ExperienceBuilder/ComponentSource/BlockComponent.php
+++ b/src/Plugin/ExperienceBuilder/ComponentSource/BlockComponent.php
@@ -39,7 +39,10 @@ use Symfony\Component\Validator\ConstraintViolationListInterface;
  */
 #[ComponentSource(
   id: self::SOURCE_PLUGIN_ID,
-  label: new TranslatableMarkup('Blocks')
+  label: new TranslatableMarkup('Blocks'),
+  // While XB does not support context mappings yet, Block plugins also can
+  // contain logic and perform e.g. database queries that fetch data to present.
+  supportsImplicitInputs: TRUE,
 )]
 final class BlockComponent extends ComponentSourceBase implements ContainerFactoryPluginInterface {
 
diff --git a/src/Plugin/ExperienceBuilder/ComponentSource/JsComponent.php b/src/Plugin/ExperienceBuilder/ComponentSource/JsComponent.php
index 80772366eb57ee0278b57d7640b0539adfa3dae8..19c12514eea946fd70e4edc825b6b35057a716aa 100644
--- a/src/Plugin/ExperienceBuilder/ComponentSource/JsComponent.php
+++ b/src/Plugin/ExperienceBuilder/ComponentSource/JsComponent.php
@@ -20,7 +20,8 @@ use Drupal\experience_builder\Entity\JavaScriptComponent;
  */
 #[ComponentSource(
   id: self::SOURCE_PLUGIN_ID,
-  label: new TranslatableMarkup('Code Components')
+  label: new TranslatableMarkup('Code Components'),
+  supportsImplicitInputs: FALSE,
 )]
 final class JsComponent extends GeneratedFieldExplicitInputUxComponentSourceBase {
 
@@ -146,6 +147,7 @@ final class JsComponent extends GeneratedFieldExplicitInputUxComponentSourceBase
       'id' => self::SOURCE_PLUGIN_ID . '.' . $js_component->id(),
       'label' => $js_component->label(),
       'category' => '@todo',
+      'provider' => NULL,
       'source' => self::SOURCE_PLUGIN_ID,
       'settings' => [
         // @todo rename plugin_id in https://www.drupal.org/project/experience_builder/issues/3502982
diff --git a/src/Plugin/ExperienceBuilder/ComponentSource/SingleDirectoryComponent.php b/src/Plugin/ExperienceBuilder/ComponentSource/SingleDirectoryComponent.php
index 0ce82f540a526e040414f7750c9a55bec774215a..de628fd338af3a2c2cb4d802b053b6c1865e5484 100644
--- a/src/Plugin/ExperienceBuilder/ComponentSource/SingleDirectoryComponent.php
+++ b/src/Plugin/ExperienceBuilder/ComponentSource/SingleDirectoryComponent.php
@@ -27,7 +27,8 @@ use Symfony\Component\Filesystem\Path;
  */
 #[ComponentSource(
   id: self::SOURCE_PLUGIN_ID,
-  label: new TranslatableMarkup('Single-Directory Components')
+  label: new TranslatableMarkup('Single-Directory Components'),
+  supportsImplicitInputs: FALSE,
 )]
 final class SingleDirectoryComponent extends GeneratedFieldExplicitInputUxComponentSourceBase implements UrlRewriteInterface {
 
@@ -219,6 +220,7 @@ final class SingleDirectoryComponent extends GeneratedFieldExplicitInputUxCompon
       'label' => $component_plugin->getPluginDefinition()['name'] ?? $component_plugin->getPluginId(),
       'category' => $component_plugin->getPluginDefinition()['category'],
       'source' => self::SOURCE_PLUGIN_ID,
+      'provider' => $component_plugin->getPluginDefinition()['provider'],
       'settings' => [
         'plugin_id' => $component_plugin->getPluginId(),
         'prop_field_definitions' => $props,
diff --git a/tests/src/Functional/PropSourceEndpointTest.php b/tests/src/Functional/PropSourceEndpointTest.php
index 49bde26665fcb0744ffb20851bc5746681d0a000..1e5b07dcf05ba5db7ddb764b21625e8ede856ac2 100644
--- a/tests/src/Functional/PropSourceEndpointTest.php
+++ b/tests/src/Functional/PropSourceEndpointTest.php
@@ -58,12 +58,14 @@ class PropSourceEndpointTest extends FunctionalTestBase {
       'announcements_feed:feed',
       'comment_list',
       'config:component_list',
+      'config:core.extension',
       'config:search.settings',
       'config:system.menu.account',
       'config:system.menu.admin',
       'config:system.menu.footer',
       'config:system.menu.main',
       'config:system.site',
+      'config:system.theme',
       'config:views.view.comments_recent',
       'config:views.view.content_recent',
       'config:views.view.who_s_new',
@@ -122,6 +124,7 @@ class PropSourceEndpointTest extends FunctionalTestBase {
       $this->assertArrayHasKey('name', $component);
       $this->assertArrayHasKey('category', $component);
       $this->assertArrayHasKey('source', $component);
+      $this->assertArrayHasKey('library', $component, $id);
       $this->assertArrayHasKey('default_markup', $component);
       $this->assertArrayHasKey('css', $component);
       $this->assertArrayHasKey('js_header', $component);
@@ -130,6 +133,10 @@ class PropSourceEndpointTest extends FunctionalTestBase {
     $this->assertStringStartsWith('<!-- xb-start-', $data['block.system_menu_block.main']['default_markup']);
     $this->assertStringContainsString('--><nav role="navigation"', $data['block.system_menu_block.main']['default_markup']);
 
+    // Stark has no SDCs.
+    $this->assertSame('stark', $this->config('system.theme')->get('default'));
+    $this->assertArrayNotHasKey('sdc.olivero.teaser', $data);
+
     $data = array_intersect_key(
       $data,
       [
@@ -163,6 +170,28 @@ class PropSourceEndpointTest extends FunctionalTestBase {
     self::assertEquals($extractValue, $data['sdc.sdc_test_all_props.all-props']['transforms']['test_integer']);
     self::assertEquals(['link' => []], $data['sdc.sdc_test_all_props.all-props']['transforms']['test_string_format_uri']);
     self::assertEquals(['mediaSelection' => [], 'mainProperty' => ['name' => 'target_id']], $data['sdc.sdc_test_all_props.all-props']['transforms']['test_object_drupal_image']);
+
+    // Olivero does have an SDC, and it's enabled, but it is omitted because the
+    // default theme is Stark.
+    $this->assertInstanceOf(Component::class, Component::load('sdc.olivero.teaser'));
+    $this->assertTrue(Component::load('sdc.olivero.teaser')->status());
+    $this->assertSame('olivero', Component::load('sdc.olivero.teaser')->get('provider'));
+
+    // Change the default theme from Stark to Olivero, and observe the impact on
+    // the list of Components returned.
+    \Drupal::configFactory()->getEditable('system.theme')->set('default', 'olivero')->save();
+    $this->rebuildAll();
+    $this->drupalGet('xb/api/config/component');
+    $data = Json::decode($page->getText());
+    $this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'MISS');
+    $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'UNCACHEABLE (request policy)');
+    // Olivero does have an SDC!
+    $this->assertSame('olivero', $this->config('system.theme')->get('default'));
+    $this->assertArrayHasKey('sdc.olivero.teaser', $data);
+    // Repeated request is again a Dynamic Page Cache hit.
+    $this->drupalGet('xb/api/config/component');
+    $this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'HIT');
+    $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'UNCACHEABLE (request policy)');
   }
 
   /**
diff --git a/tests/src/Kernel/Config/ComponentTest.php b/tests/src/Kernel/Config/ComponentTest.php
index 76b468a3dbdded14d0fd4467576b613cd04fc961..bf241b6bc277fb19fea0e4a88d15ede671dedf60 100644
--- a/tests/src/Kernel/Config/ComponentTest.php
+++ b/tests/src/Kernel/Config/ComponentTest.php
@@ -103,6 +103,7 @@ class ComponentTest extends KernelTestBase {
       'sdc' => [
         'component_config_entity_id' => 'sdc.sdc_test.my-cta',
         'source' => SingleDirectoryComponent::SOURCE_PLUGIN_ID,
+        'provider' => 'sdc_test',
         'source_internal_id' => 'sdc_test:my-cta',
         'expected_config_dependencies' => [
           'module' => [
@@ -116,6 +117,7 @@ class ComponentTest extends KernelTestBase {
       'js' => [
         'component_config_entity_id' => 'js.my-cta',
         'source' => JsComponent::SOURCE_PLUGIN_ID,
+        'provider' => NULL,
         'source_internal_id' => 'my-cta',
         'expected_config_dependencies' => [
           'config' => [
@@ -133,7 +135,7 @@ class ComponentTest extends KernelTestBase {
   /**
    * @dataProvider providerComponentCreation
    */
-  public function testComponentCreation(string $component_config_entity_id, string $source, string $source_internal_id, array $expected_config_dependencies): void {
+  public function testComponentCreation(string $component_config_entity_id, string $source, ?string $provider, string $source_internal_id, array $expected_config_dependencies): void {
     if ($source === JsComponent::SOURCE_PLUGIN_ID) {
       $this->assertEmpty(JavaScriptComponent::loadMultiple());
 
@@ -168,6 +170,7 @@ class ComponentTest extends KernelTestBase {
       'label' => self::LABEL,
       'category' => self::LABEL,
       'source' => $source,
+      'provider' => $provider,
       'settings' => [
         'plugin_id' => $source_internal_id,
         'prop_field_definitions' => [
diff --git a/tests/src/Kernel/Config/ComponentValidationTest.php b/tests/src/Kernel/Config/ComponentValidationTest.php
index c802908e044f716dfa5692d996658748a1fb5e73..7680c983a87e9612315f01e5eb1bf9c3170cb58a 100644
--- a/tests/src/Kernel/Config/ComponentValidationTest.php
+++ b/tests/src/Kernel/Config/ComponentValidationTest.php
@@ -49,6 +49,13 @@ class ComponentValidationTest extends ConfigEntityValidationTestBase {
     ],
   ];
 
+  /**
+   * {@inheritdoc}
+   */
+  protected static array $propertiesWithOptionalValues = [
+    'provider',
+  ];
+
   /**
    * {@inheritdoc}
    */
diff --git a/tests/src/Kernel/Entity/JavascriptComponentStorageTest.php b/tests/src/Kernel/Entity/JavascriptComponentStorageTest.php
index 61600aff168ea2317f1d174a542a490dab9e34aa..85b09283d0ae5308521bd5611d888bf2fdced466 100644
--- a/tests/src/Kernel/Entity/JavascriptComponentStorageTest.php
+++ b/tests/src/Kernel/Entity/JavascriptComponentStorageTest.php
@@ -139,6 +139,7 @@ final class JavascriptComponentStorageTest extends KernelTestBase {
 
     $component = Component::load($component_id);
     self::assertInstanceOf(ComponentInterface::class, $component);
+    self::assertNull($component->get('provider'));
     self::assertEquals(['title'], \array_keys($component->getSettings()['prop_field_definitions']));
 
     // Now update the js component and confirm we update the matching component.