diff --git a/docs/components.md b/docs/components.md
index a6b2239ffc3eb2b36049ebfef811a368c1f52061..eed919398a8fbfd19df23be6365d91880b23b24a 100644
--- a/docs/components.md
+++ b/docs/components.md
@@ -31,6 +31,7 @@ This uses the terms defined above.
 - MUST support `SDC` and `Block` today
   - MUST be evolvable to [support other component types later](https://www.drupal.org/project/experience_builder/issues/3454519)
 - MUST support existing `SDC`s and `Block`s, if they meet certain criteria necessary for XB to provide a good UX
+- MUST support categorization of `component`s
 - MAY require API additions and perhaps even changes to `SDC`s (such as: defining restrictions for `component slot`s, schema references and more) ⚠️ [an overview of what has been identified is constantly updated](https://www.drupal.org/project/experience_builder/issues/3462705) ⚠️
 
 ⚠️ The [supported component modeling approaches](https://www.drupal.org/project/experience_builder/issues/3446083)
@@ -96,3 +97,26 @@ refer to "blocks" and not "block plugins", under the hood, they actually _are_ b
 ### 3.3 Other `component type`s
 
 Nothing yet, this will change when we [support other `component type`s later](https://www.drupal.org/project/experience_builder/issues/3454519).
+
+### 3.4 Categorization
+
+Each `component` can be categorized in order to group them in the UI. Some `component type`s have shared categories, as follows:
+
+```mermaid
+stateDiagram-v2
+classdef source color:white,fill:purple
+
+state "JavaScript component" as JavaScript
+state "Theme component" as Theme
+state "Theme component categories" as ThemeCat
+state "Module component" as Module
+state "Module component categories" as ModuleCat
+state "Element categories" as ElementCat
+state "Block categories" as BlockCat
+
+JavaScript:::source --> ThemeCat
+Theme:::source --> ThemeCat
+Module:::source --> ModuleCat
+Element:::source --> ElementCat
+Block:::source --> BlockCat
+```
diff --git a/openapi.yml b/openapi.yml
index f706aec74ddaa4a0560ab61e134e35685491b4e6..b6434014761f47a34e30456b22995ebe9874438c 100644
--- a/openapi.yml
+++ b/openapi.yml
@@ -91,6 +91,7 @@ paths:
                     'sdc_test:my-cta':
                       id: 'sdc_test:my-cta'
                       name: Call to Action
+                      source: Module component
                       metadata:
                         path: >-
                           core/modules/system/tests/modules/sdc_test/components/my-cta
diff --git a/src/Controller/ApiComponentsController.php b/src/Controller/ApiComponentsController.php
index 849f5b4d75415e66d6ddb07efe39b60e26bc8693..12f499917d4e6c1b419a86a8f43834fc7b24b026 100644
--- a/src/Controller/ApiComponentsController.php
+++ b/src/Controller/ApiComponentsController.php
@@ -19,8 +19,8 @@ use Symfony\Component\DependencyInjection\Attribute\Autowire;
  *
  * @see ui/src/types/Component.ts
  * @phpstan-import-type ComponentConfigEntityId from \Drupal\experience_builder\Entity\Component
- * @phpstan-type ComponentClientSideTypeAny array{'id': string, 'name': string, 'default_markup': string|\Stringable, 'css': string|\Stringable, 'js_header': string|\Stringable, 'js_footer': string|\Stringable}
- * @phpstan-type ComponentClientSideTypeSdc array{'id': string, 'name': string, 'default_markup': string|\Stringable, 'css': string|\Stringable, 'js_header': string|\Stringable, 'js_footer': string|\Stringable, 'metadata': array<string, mixed>, 'field_data': array<string, mixed>, 'dynamic_prop_source_candidates': array<string, mixed>,}
+ * @phpstan-type ComponentClientSideTypeAny array{'id': string, 'name': string, 'source': string, 'default_markup': string|\Stringable, 'css': string|\Stringable, 'js_header': string|\Stringable, 'js_footer': string|\Stringable}
+ * @phpstan-type ComponentClientSideTypeSdc array{'id': string, 'name': string, 'source': string, 'default_markup': string|\Stringable, 'css': string|\Stringable, 'js_header': string|\Stringable, 'js_footer': string|\Stringable, 'metadata': array<string, mixed>, 'field_data': array<string, mixed>, 'dynamic_prop_source_candidates': array<string, mixed>,}
  *
  * This controller provides a critical response for the XB UI. Therefore it
  * should hence be as fast and cacheable as possible. High-cardinality cache
@@ -62,6 +62,7 @@ final class ApiComponentsController {
    *   array of XB Component config entities, with for each:
    *   - `id`: the Component config entity ID
    *   - `name`: human-readable name
+   *   - `source`: human-readable component type
    *   - `default_markup`: without providing user input, this is what
    *     Component's markup would look like — used to preview the Component
    *     prior to placing it
diff --git a/src/Plugin/ExperienceBuilder/ComponentSource/BlockComponent.php b/src/Plugin/ExperienceBuilder/ComponentSource/BlockComponent.php
index 1af6bfe521866c28cb9c4626cbd6f318a0370afa..ed74d617e761fdabc0b3a759304868ac43a46fd1 100644
--- a/src/Plugin/ExperienceBuilder/ComponentSource/BlockComponent.php
+++ b/src/Plugin/ExperienceBuilder/ComponentSource/BlockComponent.php
@@ -151,6 +151,7 @@ final class BlockComponent extends ComponentSourceBase implements ContainerFacto
     return [
       'id' => $component->id(),
       'name' => (string) $definition['admin_label'],
+      'source' => (string) $this->t('Block'),
       // @todo Allow components to pass build arrays back?
       'default_markup' => $this->renderer->render($build),
       // @todo CSS and JS
diff --git a/src/Plugin/ExperienceBuilder/ComponentSource/SingleDirectoryComponent.php b/src/Plugin/ExperienceBuilder/ComponentSource/SingleDirectoryComponent.php
index 7f490f6621e49996f35bb02429a06a9fb2dfd47b..17b0d3292a48e178b717152c015de25926d5314c 100644
--- a/src/Plugin/ExperienceBuilder/ComponentSource/SingleDirectoryComponent.php
+++ b/src/Plugin/ExperienceBuilder/ComponentSource/SingleDirectoryComponent.php
@@ -17,6 +17,7 @@ use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\Theme\Component\ComponentValidator;
 use Drupal\Core\Theme\ComponentPluginManager;
+use Drupal\Core\Theme\ExtensionType;
 use Drupal\experience_builder\AssetRenderer;
 use Drupal\experience_builder\Attribute\ComponentSource;
 use Drupal\experience_builder\ComponentSource\ComponentSourceBase;
@@ -315,6 +316,7 @@ final class SingleDirectoryComponent extends ComponentSourceBase implements Comp
     return [
       'id' => $component->id(),
       'name' => $component_plugin->metadata->name,
+      'source' => (string) $this->getSourceLabel(),
       // A pre-rendered version of the component is provided so no requests
       // are needed when adding it to the layout which includes a default markup,
       // CSS files, JS files in the header and JS files in the footer.
@@ -330,6 +332,26 @@ final class SingleDirectoryComponent extends ComponentSourceBase implements Comp
     ];
   }
 
+  /**
+   * Returns the source label for this component.
+   *
+   * @return \Drupal\Core\StringTranslation\TranslatableMarkup
+   *   The source label.
+   */
+  protected function getSourceLabel(): TranslatableMarkup {
+    $component_plugin = $this->getComponentPlugin();
+    assert(is_array($component_plugin->getPluginDefinition()));
+
+    // The 'extension_type' key is guaranteed to be set.
+    // @see \Drupal\Core\Theme\ComponentPluginManager::alterDefinition()
+    $extension_type = $component_plugin->getPluginDefinition()['extension_type'];
+    assert($extension_type instanceof ExtensionType);
+    return match ($extension_type) {
+      ExtensionType::Module => $this->t('Module component'),
+      ExtensionType::Theme => $this->t('Theme component'),
+    };
+  }
+
   /**
    * Converts an SDC plugin machine name into a config entity ID.
    *