diff --git a/core/lib/Drupal/Component/Plugin/Attribute/AttributeBase.php b/core/lib/Drupal/Component/Plugin/Attribute/AttributeBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..7ebc93e69c428a2d3eb65593108bef29a1c4a1d9
--- /dev/null
+++ b/core/lib/Drupal/Component/Plugin/Attribute/AttributeBase.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Drupal\Component\Plugin\Attribute;
+
+/**
+ * Provides a base class for classed attributes.
+ */
+abstract class AttributeBase implements AttributeInterface {
+
+  /**
+   * The class used for this attribute class.
+   *
+   * @var class-string
+   */
+  protected string $class;
+
+  /**
+   * The provider of the attribute class.
+   */
+  protected string|null $provider = NULL;
+
+  /**
+   * @param string $id
+   *   The attribute class ID.
+   */
+  public function __construct(
+    protected readonly string $id
+  ) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getProvider(): ?string {
+    return $this->provider;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setProvider(string $provider): void {
+    $this->provider = $provider;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getId(): string {
+    return $this->id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getClass(): string {
+    return $this->class;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setClass(string $class): void {
+    $this->class = $class;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function get(): array|object {
+    return array_filter(get_object_vars($this) + [
+      'class' => $this->getClass(),
+      'provider' => $this->getProvider(),
+    ], function ($value, $key) {
+      return !($value === NULL && ($key === 'deriver' || $key === 'provider'));
+    }, ARRAY_FILTER_USE_BOTH);
+  }
+
+}
diff --git a/core/lib/Drupal/Component/Plugin/Attribute/AttributeInterface.php b/core/lib/Drupal/Component/Plugin/Attribute/AttributeInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..30964398a90f0e071f712f0a8254005b58280452
--- /dev/null
+++ b/core/lib/Drupal/Component/Plugin/Attribute/AttributeInterface.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace Drupal\Component\Plugin\Attribute;
+
+/**
+ * Defines a common interface for classed attributes.
+ */
+interface AttributeInterface {
+
+  /**
+   * Gets the value of an attribute.
+   */
+  public function get(): mixed;
+
+  /**
+   * Gets the name of the provider of the attribute class.
+   *
+   * @return string|null
+   */
+  public function getProvider(): ?string;
+
+  /**
+   * Sets the name of the provider of the attribute class.
+   *
+   * @param string $provider
+   *   The provider of the annotated class.
+   */
+  public function setProvider(string $provider): void;
+
+  /**
+   * Gets the unique ID for this attribute class.
+   *
+   * @return string
+   */
+  public function getId(): string;
+
+  /**
+   * Gets the class of the attribute class.
+   *
+   * @return class-string|null
+   */
+  public function getClass(): ?string;
+
+  /**
+   * Sets the class of the attributed class.
+   *
+   * @param class-string $class
+   *   The class of the attributed class.
+   */
+  public function setClass(string $class): void;
+
+}
diff --git a/core/lib/Drupal/Component/Plugin/Attribute/Plugin.php b/core/lib/Drupal/Component/Plugin/Attribute/Plugin.php
new file mode 100644
index 0000000000000000000000000000000000000000..af98c93c6a257e0d3441a024fcf35cea2045f290
--- /dev/null
+++ b/core/lib/Drupal/Component/Plugin/Attribute/Plugin.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Drupal\Component\Plugin\Attribute;
+
+/**
+ * Defines a Plugin attribute object.
+ *
+ * Attributes in plugin classes can use this class in order to pass various
+ * metadata about the plugin through the parser to
+ * DiscoveryInterface::getDefinitions() calls.
+ *
+ * @ingroup plugin_api
+ */
+#[\Attribute(\Attribute::TARGET_CLASS)]
+class Plugin extends AttributeBase {
+
+  /**
+   * Constructs a plugin attribute object.
+   *
+   * @param string $id
+   *   The attribute class ID.
+   * @param class-string|null $deriver
+   *   (optional) The deriver class.
+   */
+  public function __construct(
+    public readonly string $id,
+    public readonly ?string $deriver = NULL
+  ) {}
+
+}
diff --git a/core/lib/Drupal/Component/Plugin/Attribute/PluginID.php b/core/lib/Drupal/Component/Plugin/Attribute/PluginID.php
new file mode 100644
index 0000000000000000000000000000000000000000..66368d0546dc348a733ee84b56497f0206237c1b
--- /dev/null
+++ b/core/lib/Drupal/Component/Plugin/Attribute/PluginID.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Drupal\Component\Plugin\Attribute;
+
+/**
+ * Defines a Plugin attribute object that just contains an ID.
+ */
+#[\Attribute(\Attribute::TARGET_CLASS)]
+class PluginID extends AttributeBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function get(): array {
+    return [
+      'id' => $this->getId(),
+      'class' => $this->getClass(),
+      'provider' => $this->getProvider(),
+    ];
+  }
+
+}
diff --git a/core/lib/Drupal/Component/Plugin/Discovery/AttributeBridgeDecorator.php b/core/lib/Drupal/Component/Plugin/Discovery/AttributeBridgeDecorator.php
new file mode 100644
index 0000000000000000000000000000000000000000..3f354693e95b065f2cba16a1eabdaf529674d44b
--- /dev/null
+++ b/core/lib/Drupal/Component/Plugin/Discovery/AttributeBridgeDecorator.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Drupal\Component\Plugin\Discovery;
+
+/**
+ * Ensures that all definitions are run through the attribute process.
+ */
+class AttributeBridgeDecorator implements DiscoveryInterface {
+
+  use DiscoveryTrait;
+
+  /**
+   * AttributeBridgeDecorator constructor.
+   *
+   * @param \Drupal\Component\Plugin\Discovery\DiscoveryInterface $decorated
+   *   The discovery object that is being decorated.
+   * @param string $pluginDefinitionAttributeName
+   *   The name of the attribute that contains the plugin definition. The class
+   *   corresponding to this name must implement
+   *   \Drupal\Component\Plugin\Attribute\AttributeInterface.
+   */
+  public function __construct(
+    protected readonly DiscoveryInterface $decorated,
+    protected readonly string $pluginDefinitionAttributeName
+  ) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefinitions() {
+    $definitions = $this->decorated->getDefinitions();
+    foreach ($definitions as $id => $definition) {
+      // Attribute constructors expect an array of values. If the definition is
+      // not an array, it usually means it has been processed already and can be
+      // ignored.
+      if (is_array($definition)) {
+        $class = $definition['class'] ?? NULL;
+        $provider = $definition['provider'] ?? NULL;
+        unset($definition['class'], $definition['provider']);
+        /** @var \Drupal\Component\Plugin\Attribute\AttributeInterface $attribute */
+        $attribute = new $this->pluginDefinitionAttributeName(...$definition);
+        if (isset($class)) {
+          $attribute->setClass($class);
+        }
+        if (isset($provider)) {
+          $attribute->setProvider($provider);
+        }
+        $definitions[$id] = $attribute->get();
+      }
+    }
+    return $definitions;
+  }
+
+  /**
+   * Passes through all unknown calls onto the decorated object.
+   *
+   * @param string $method
+   *   The method to call on the decorated plugin discovery.
+   * @param array $args
+   *   The arguments to send to the method.
+   *
+   * @return mixed
+   *   The method result.
+   */
+  public function __call($method, $args) {
+    return $this->decorated->{$method}(...$args);
+  }
+
+}
diff --git a/core/lib/Drupal/Component/Plugin/Discovery/AttributeClassDiscovery.php b/core/lib/Drupal/Component/Plugin/Discovery/AttributeClassDiscovery.php
new file mode 100644
index 0000000000000000000000000000000000000000..9accdefd55e1c02da794521dd0a1b3bbfb94177f
--- /dev/null
+++ b/core/lib/Drupal/Component/Plugin/Discovery/AttributeClassDiscovery.php
@@ -0,0 +1,160 @@
+<?php
+
+namespace Drupal\Component\Plugin\Discovery;
+
+use Drupal\Component\Plugin\Attribute\AttributeInterface;
+use Drupal\Component\Plugin\Attribute\Plugin;
+use Drupal\Component\FileCache\FileCacheFactory;
+use Drupal\Component\FileCache\FileCacheInterface;
+
+/**
+ * Defines a discovery mechanism to find plugins with attributes.
+ */
+class AttributeClassDiscovery implements DiscoveryInterface {
+
+  use DiscoveryTrait;
+
+  /**
+   * The file cache object.
+   */
+  protected FileCacheInterface $fileCache;
+
+  /**
+   * Constructs a new instance.
+   *
+   * @param string[] $pluginNamespaces
+   *   (optional) An array of namespace that may contain plugin implementations.
+   *   Defaults to an empty array.
+   * @param string $pluginDefinitionAttributeName
+   *   (optional) The name of the attribute that contains the plugin definition.
+   *   Defaults to 'Drupal\Component\Plugin\Attribute\Plugin'.
+   */
+  public function __construct(
+    protected readonly array $pluginNamespaces = [],
+    protected readonly string $pluginDefinitionAttributeName = Plugin::class
+  ) {
+    $file_cache_suffix = str_replace('\\', '_', $this->pluginDefinitionAttributeName);
+    $this->fileCache = FileCacheFactory::get('attribute_discovery:' . $this->getFileCacheSuffix($file_cache_suffix));
+  }
+
+  /**
+   * Gets the file cache suffix.
+   *
+   * This method allows classes that extend this class to add additional
+   * information to the file cache collection name.
+   *
+   * @param string $default_suffix
+   *   The default file cache suffix.
+   *
+   * @return string
+   *   The file cache suffix.
+   */
+  protected function getFileCacheSuffix(string $default_suffix): string {
+    return $default_suffix;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefinitions() {
+    $definitions = [];
+
+    // Search for classes within all PSR-4 namespace locations.
+    foreach ($this->getPluginNamespaces() as $namespace => $dirs) {
+      foreach ($dirs as $dir) {
+        if (file_exists($dir)) {
+          $iterator = new \RecursiveIteratorIterator(
+            new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS)
+          );
+          foreach ($iterator as $fileinfo) {
+            assert($fileinfo instanceof \SplFileInfo);
+            if ($fileinfo->getExtension() === 'php') {
+              if ($cached = $this->fileCache->get($fileinfo->getPathName())) {
+                if (isset($cached['id'])) {
+                  // Explicitly unserialize this to create a new object instance.
+                  $definitions[$cached['id']] = unserialize($cached['content']);
+                }
+                continue;
+              }
+
+              $sub_path = $iterator->getSubIterator()->getSubPath();
+              $sub_path = $sub_path ? str_replace(DIRECTORY_SEPARATOR, '\\', $sub_path) . '\\' : '';
+              $class = $namespace . '\\' . $sub_path . $fileinfo->getBasename('.php');
+
+              ['id' => $id, 'content' => $content] = $this->parseClass($class, $fileinfo);
+
+              if ($id) {
+                $definitions[$id] = $content;
+                // Explicitly serialize this to create a new object instance.
+                $this->fileCache->set($fileinfo->getPathName(), ['id' => $id, 'content' => serialize($content)]);
+              }
+              else {
+                // Store a NULL object, so the file is not reparsed again.
+                $this->fileCache->set($fileinfo->getPathName(), [NULL]);
+              }
+            }
+          }
+        }
+      }
+    }
+
+    // Plugin discovery is a memory expensive process due to reflection and the
+    // number of files involved. Collect cycles at the end of discovery to be as
+    // efficient as possible.
+    gc_collect_cycles();
+    return $definitions;
+  }
+
+  /**
+   * Parses attributes from a class.
+   *
+   * @param class-string $class
+   *   The class to parse.
+   * @param \SplFileInfo $fileinfo
+   *   The SPL file information for the class.
+   *
+   * @return array
+   *   An array with the keys 'id' and 'content'. The 'id' is the plugin ID and
+   *   'content' is the plugin definition.
+   *
+   * @throws \ReflectionException
+   */
+  protected function parseClass(string $class, \SplFileInfo $fileinfo): array {
+    // @todo Consider performance improvements over using reflection.
+    // @see https://www.drupal.org/project/drupal/issues/3395260.
+    $reflection_class = new \ReflectionClass($class);
+
+    $id = $content = NULL;
+    if ($attributes = $reflection_class->getAttributes($this->pluginDefinitionAttributeName, \ReflectionAttribute::IS_INSTANCEOF)) {
+      /** @var \Drupal\Component\Plugin\Attribute\AttributeInterface $attribute */
+      $attribute = $attributes[0]->newInstance();
+      $this->prepareAttributeDefinition($attribute, $class);
+
+      $id = $attribute->getId();
+      $content = $attribute->get();
+    }
+    return ['id' => $id, 'content' => $content];
+  }
+
+  /**
+   * Prepares the attribute definition.
+   *
+   * @param \Drupal\Component\Plugin\Attribute\AttributeInterface $attribute
+   *   The attribute derived from the plugin.
+   * @param string $class
+   *   The class used for the plugin.
+   */
+  protected function prepareAttributeDefinition(AttributeInterface $attribute, string $class): void {
+    $attribute->setClass($class);
+  }
+
+  /**
+   * Gets an array of PSR-4 namespaces to search for plugin classes.
+   *
+   * @return string[][]
+   */
+  protected function getPluginNamespaces(): array {
+    return $this->pluginNamespaces;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Action/ActionManager.php b/core/lib/Drupal/Core/Action/ActionManager.php
index fda253b5a9fb252754cc16af0ffb5d504ef6ba28..39a8c7520b3aa6b9f6d31dabff2e6686a896636f 100644
--- a/core/lib/Drupal/Core/Action/ActionManager.php
+++ b/core/lib/Drupal/Core/Action/ActionManager.php
@@ -3,6 +3,7 @@
 namespace Drupal\Core\Action;
 
 use Drupal\Component\Plugin\CategorizingPluginManagerInterface;
+use Drupal\Core\Action\Attribute\Action;
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Plugin\CategorizingPluginManagerTrait;
@@ -32,7 +33,7 @@ class ActionManager extends DefaultPluginManager implements CategorizingPluginMa
    *   The module handler to invoke the alter hook with.
    */
   public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
-    parent::__construct('Plugin/Action', $namespaces, $module_handler, 'Drupal\Core\Action\ActionInterface', 'Drupal\Core\Annotation\Action');
+    parent::__construct('Plugin/Action', $namespaces, $module_handler, 'Drupal\Core\Action\ActionInterface', Action::class, 'Drupal\Core\Annotation\Action');
     $this->alterInfo('action_info');
     $this->setCacheBackend($cache_backend, 'action_info');
   }
diff --git a/core/lib/Drupal/Core/Action/Attribute/Action.php b/core/lib/Drupal/Core/Action/Attribute/Action.php
new file mode 100644
index 0000000000000000000000000000000000000000..eab816f670075f758149d4212e1d0351a24848ad
--- /dev/null
+++ b/core/lib/Drupal/Core/Action/Attribute/Action.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Drupal\Core\Action\Attribute;
+
+use Drupal\Component\Plugin\Attribute\Plugin;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+
+/**
+ * Defines an Action attribute object.
+ *
+ * Plugin Namespace: Plugin\Action
+ *
+ * @see \Drupal\Core\Action\ActionInterface
+ * @see \Drupal\Core\Action\ActionManager
+ * @see \Drupal\Core\Action\ActionBase
+ * @see \Drupal\Core\Action\Plugin\Action\UnpublishAction
+ * @see plugin_api
+ *
+ * @Annotation
+ */
+#[\Attribute(\Attribute::TARGET_CLASS)]
+class Action extends Plugin {
+
+  /**
+   * Constructs an Action attribute.
+   *
+   * @param string $id
+   *   The plugin ID.
+   * @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $label
+   *   The label of the action.
+   * @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $action_label
+   *   (optional) A label that can be used by the action deriver.
+   * @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $category
+   *   (optional) The category under which the action should be listed in the
+   *   UI.
+   * @param string|null $deriver
+   *   (optional) The deriver class.
+   * @param string|null $confirm_form_route_name
+   *   (optional) The route name for a confirmation form for this action.
+   * @param string|null $type
+   *   (optional) The entity type the action can apply to.
+   */
+  public function __construct(
+    public readonly string $id,
+    public readonly ?TranslatableMarkup $label = NULL,
+    public readonly ?TranslatableMarkup $action_label = NULL,
+    public readonly ?TranslatableMarkup $category = NULL,
+    public readonly ?string $deriver = NULL,
+    public readonly ?string $confirm_form_route_name = NULL,
+    public readonly ?string $type = NULL
+  ) {}
+
+}
diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/DeleteAction.php b/core/lib/Drupal/Core/Action/Plugin/Action/DeleteAction.php
index 28744b6913a1e7aee91588db8fa2f1aae6ccf7c0..6a7994a977abb9ce1156686cb56e68bb98730942 100644
--- a/core/lib/Drupal/Core/Action/Plugin/Action/DeleteAction.php
+++ b/core/lib/Drupal/Core/Action/Plugin/Action/DeleteAction.php
@@ -2,20 +2,22 @@
 
 namespace Drupal\Core\Action\Plugin\Action;
 
+use Drupal\Core\Action\Plugin\Action\Derivative\EntityDeleteActionDeriver;
+use Drupal\Core\Action\Attribute\Action;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\TempStore\PrivateTempStoreFactory;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Redirects to an entity deletion form.
- *
- * @Action(
- *   id = "entity:delete_action",
- *   action_label = @Translation("Delete"),
- *   deriver = "Drupal\Core\Action\Plugin\Action\Derivative\EntityDeleteActionDeriver",
- * )
  */
+#[Action(
+  id: 'entity:delete_action',
+  action_label: new TranslatableMarkup('Delete'),
+  deriver: EntityDeleteActionDeriver::class
+)]
 class DeleteAction extends EntityActionBase {
 
   /**
diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/EmailAction.php b/core/lib/Drupal/Core/Action/Plugin/Action/EmailAction.php
index 78af30dccf9fcb6d1192877a4d53b27c3996b349..e0f6d0896be66f0778e54d456abc21d04ade04e0 100644
--- a/core/lib/Drupal/Core/Action/Plugin/Action/EmailAction.php
+++ b/core/lib/Drupal/Core/Action/Plugin/Action/EmailAction.php
@@ -6,25 +6,26 @@
 use Drupal\Component\Utility\EmailValidatorInterface;
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Action\ConfigurableActionBase;
+use Drupal\Core\Action\Attribute\Action;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
 use Drupal\Core\Mail\MailManagerInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\Utility\Token;
 use Psr\Log\LoggerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Sends an email message.
- *
- * @Action(
- *   id = "action_send_email_action",
- *   label = @Translation("Send email"),
- *   type = "system"
- * )
  */
+#[Action(
+  id: 'action_send_email_action',
+  label: new TranslatableMarkup('Send email'),
+  type: 'system'
+)]
 class EmailAction extends ConfigurableActionBase implements ContainerFactoryPluginInterface {
 
   /**
diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/GotoAction.php b/core/lib/Drupal/Core/Action/Plugin/Action/GotoAction.php
index befc21f6a529fece81bf15dfef331143999eb881..565529af7223a988abaa2794b2347c083b83b5e6 100644
--- a/core/lib/Drupal/Core/Action/Plugin/Action/GotoAction.php
+++ b/core/lib/Drupal/Core/Action/Plugin/Action/GotoAction.php
@@ -5,9 +5,11 @@
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Action\ConfigurableActionBase;
+use Drupal\Core\Action\Attribute\Action;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\Utility\UnroutedUrlAssemblerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
@@ -16,13 +18,12 @@
 
 /**
  * Redirects to a different URL.
- *
- * @Action(
- *   id = "action_goto_action",
- *   label = @Translation("Redirect to URL"),
- *   type = "system"
- * )
  */
+#[Action(
+  id: 'action_goto_action',
+  label: new TranslatableMarkup('Redirect to URL'),
+  type: 'system'
+)]
 class GotoAction extends ConfigurableActionBase implements ContainerFactoryPluginInterface {
 
   /**
diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/MessageAction.php b/core/lib/Drupal/Core/Action/Plugin/Action/MessageAction.php
index 19575278cc177456e5749ec5ba2939ffaa0d2aec..7a9dd005f60db195f5d493632e0967eb54c33800 100644
--- a/core/lib/Drupal/Core/Action/Plugin/Action/MessageAction.php
+++ b/core/lib/Drupal/Core/Action/Plugin/Action/MessageAction.php
@@ -4,23 +4,24 @@
 
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Action\ConfigurableActionBase;
+use Drupal\Core\Action\Attribute\Action;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Messenger\MessengerInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\Utility\Token;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Sends a message to the current user's screen.
- *
- * @Action(
- *   id = "action_message_action",
- *   label = @Translation("Display a message to the user"),
- *   type = "system"
- * )
  */
+#[Action(
+  id: 'action_message_action',
+  label: new TranslatableMarkup('Display a message to the user'),
+  type: 'system'
+)]
 class MessageAction extends ConfigurableActionBase implements ContainerFactoryPluginInterface {
 
   /**
diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/PublishAction.php b/core/lib/Drupal/Core/Action/Plugin/Action/PublishAction.php
index 8d0cf756b47c308abab7c88e2f2c6bd55754a7ec..0c79430a3f0702de1d7771db8b29dc585bee09f5 100644
--- a/core/lib/Drupal/Core/Action/Plugin/Action/PublishAction.php
+++ b/core/lib/Drupal/Core/Action/Plugin/Action/PublishAction.php
@@ -2,17 +2,19 @@
 
 namespace Drupal\Core\Action\Plugin\Action;
 
+use Drupal\Core\Action\Plugin\Action\Derivative\EntityPublishedActionDeriver;
+use Drupal\Core\Action\Attribute\Action;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Publishes an entity.
- *
- * @Action(
- *   id = "entity:publish_action",
- *   action_label = @Translation("Publish"),
- *   deriver = "Drupal\Core\Action\Plugin\Action\Derivative\EntityPublishedActionDeriver",
- * )
  */
+#[Action(
+  id: 'entity:publish_action',
+  action_label: new TranslatableMarkup('Publish'),
+  deriver: EntityPublishedActionDeriver::class
+)]
 class PublishAction extends EntityActionBase {
 
   /**
diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/SaveAction.php b/core/lib/Drupal/Core/Action/Plugin/Action/SaveAction.php
index fc63e5c16e3bd16a1597547c7aed0295b82884c4..e9fbbd62e728aa435e405361c6f1d06212634a07 100644
--- a/core/lib/Drupal/Core/Action/Plugin/Action/SaveAction.php
+++ b/core/lib/Drupal/Core/Action/Plugin/Action/SaveAction.php
@@ -3,19 +3,21 @@
 namespace Drupal\Core\Action\Plugin\Action;
 
 use Drupal\Component\Datetime\TimeInterface;
+use Drupal\Core\Action\Plugin\Action\Derivative\EntityChangedActionDeriver;
+use Drupal\Core\Action\Attribute\Action;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Provides an action that can save any entity.
- *
- * @Action(
- *   id = "entity:save_action",
- *   action_label = @Translation("Save"),
- *   deriver = "Drupal\Core\Action\Plugin\Action\Derivative\EntityChangedActionDeriver",
- * )
  */
+#[Action(
+  id: 'entity:save_action',
+  action_label: new TranslatableMarkup('Save'),
+  deriver: EntityChangedActionDeriver::class
+)]
 class SaveAction extends EntityActionBase {
 
   /**
diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/UnpublishAction.php b/core/lib/Drupal/Core/Action/Plugin/Action/UnpublishAction.php
index bb3f6c2d98e772124c2598cb7f0dfdc2d787a967..b8ef0f3db66ebde5721355b44605bf4ea21b44f6 100644
--- a/core/lib/Drupal/Core/Action/Plugin/Action/UnpublishAction.php
+++ b/core/lib/Drupal/Core/Action/Plugin/Action/UnpublishAction.php
@@ -2,17 +2,19 @@
 
 namespace Drupal\Core\Action\Plugin\Action;
 
+use Drupal\Core\Action\Plugin\Action\Derivative\EntityPublishedActionDeriver;
+use Drupal\Core\Action\Attribute\Action;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Unpublishes an entity.
- *
- * @Action(
- *   id = "entity:unpublish_action",
- *   action_label = @Translation("Unpublish"),
- *   deriver = "Drupal\Core\Action\Plugin\Action\Derivative\EntityPublishedActionDeriver",
- * )
  */
+#[Action(
+  id: 'entity:unpublish_action',
+  action_label: new TranslatableMarkup('Unpublish'),
+  deriver: EntityPublishedActionDeriver::class
+)]
 class UnpublishAction extends EntityActionBase {
 
   /**
diff --git a/core/lib/Drupal/Core/Block/Attribute/Block.php b/core/lib/Drupal/Core/Block/Attribute/Block.php
new file mode 100644
index 0000000000000000000000000000000000000000..3e5fbc6be88c76107e31755d2e330c89372f0b57
--- /dev/null
+++ b/core/lib/Drupal/Core/Block/Attribute/Block.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Drupal\Core\Block\Attribute;
+
+use Drupal\Component\Plugin\Attribute\Plugin;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+
+/**
+ * The Block attribute.
+ */
+#[\Attribute(\Attribute::TARGET_CLASS)]
+class Block extends Plugin {
+
+  /**
+   * Constructs a Block attribute.
+   *
+   * @param string $id
+   *   The plugin ID.
+   * @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $admin_label
+   *   The administrative label of the block.
+   * @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $category
+   *   (optional) The category in the admin UI where the block will be listed.
+   * @param \Drupal\Core\Annotation\ContextDefinition[] $context_definitions
+   *   (optional) An array of context definitions describing the context used by
+   *   the plugin. The array is keyed by context names.
+   * @param string|null $deriver
+   *   (optional) The deriver class.
+   * @param string[] $forms
+   *   (optional) An array of form class names keyed by a string.
+   */
+  public function __construct(
+    public readonly string $id,
+    public readonly ?TranslatableMarkup $admin_label = NULL,
+    public readonly ?TranslatableMarkup $category = NULL,
+    public readonly array $context_definitions = [],
+    public readonly ?string $deriver = NULL,
+    public readonly array $forms = []
+  ) {}
+
+}
diff --git a/core/lib/Drupal/Core/Block/BlockManager.php b/core/lib/Drupal/Core/Block/BlockManager.php
index 026d810fc058b3330376d67532d94fd42abd7559..0741e1579403205f70e799d0b2bf0dae31029b25 100644
--- a/core/lib/Drupal/Core/Block/BlockManager.php
+++ b/core/lib/Drupal/Core/Block/BlockManager.php
@@ -3,6 +3,7 @@
 namespace Drupal\Core\Block;
 
 use Drupal\Component\Plugin\FallbackPluginManagerInterface;
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Plugin\CategorizingPluginManagerTrait;
@@ -45,7 +46,7 @@ class BlockManager extends DefaultPluginManager implements BlockManagerInterface
    *   The logger.
    */
   public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, LoggerInterface $logger) {
-    parent::__construct('Plugin/Block', $namespaces, $module_handler, 'Drupal\Core\Block\BlockPluginInterface', 'Drupal\Core\Block\Annotation\Block');
+    parent::__construct('Plugin/Block', $namespaces, $module_handler, 'Drupal\Core\Block\BlockPluginInterface', Block::class, 'Drupal\Core\Block\Annotation\Block');
 
     $this->alterInfo($this->getType());
     $this->setCacheBackend($cache_backend, 'block_plugins');
diff --git a/core/lib/Drupal/Core/Block/Plugin/Block/Broken.php b/core/lib/Drupal/Core/Block/Plugin/Block/Broken.php
index 0d09afb85044003f42b73a0d268ec716c22eda58..2d92c161d94db0c52171c9bafa5fb5923e78ab06 100644
--- a/core/lib/Drupal/Core/Block/Plugin/Block/Broken.php
+++ b/core/lib/Drupal/Core/Block/Plugin/Block/Broken.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Core\Block\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockPluginInterface;
 use Drupal\Core\Block\BlockPluginTrait;
 use Drupal\Core\Cache\CacheableDependencyTrait;
@@ -9,17 +10,17 @@
 use Drupal\Core\Plugin\PluginBase;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Defines a fallback plugin for missing block plugins.
- *
- * @Block(
- *   id = "broken",
- *   admin_label = @Translation("Broken/Missing"),
- *   category = @Translation("Block"),
- * )
  */
+#[Block(
+  id: "broken",
+  admin_label: new TranslatableMarkup("Broken/Missing"),
+  category: new TranslatableMarkup("Block")
+)]
 class Broken extends PluginBase implements BlockPluginInterface, ContainerFactoryPluginInterface {
 
   use BlockPluginTrait;
diff --git a/core/lib/Drupal/Core/Block/Plugin/Block/PageTitleBlock.php b/core/lib/Drupal/Core/Block/Plugin/Block/PageTitleBlock.php
index af1fb6ee374be3714f64dcf23485365070be64ec..2689acae4a3a89a94f7e7d43ad51ea13ef7e147b 100644
--- a/core/lib/Drupal/Core/Block/Plugin/Block/PageTitleBlock.php
+++ b/core/lib/Drupal/Core/Block/Plugin/Block/PageTitleBlock.php
@@ -2,20 +2,21 @@
 
 namespace Drupal\Core\Block\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Block\TitleBlockPluginInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Provides a block to display the page title.
- *
- * @Block(
- *   id = "page_title_block",
- *   admin_label = @Translation("Page title"),
- *   forms = {
- *     "settings_tray" = FALSE,
- *   },
- * )
  */
+#[Block(
+  id: "page_title_block",
+  admin_label: new TranslatableMarkup("Page title"),
+  forms: [
+    'settings_tray' => FALSE,
+  ]
+)]
 class PageTitleBlock extends BlockBase implements TitleBlockPluginInterface {
 
   /**
diff --git a/core/lib/Drupal/Core/Menu/Plugin/Block/LocalActionsBlock.php b/core/lib/Drupal/Core/Menu/Plugin/Block/LocalActionsBlock.php
index 70ef668bf8590b01df16972b7297602eff7cf4b8..27591d4a3f39747097011d838aa107226cd28f78 100644
--- a/core/lib/Drupal/Core/Menu/Plugin/Block/LocalActionsBlock.php
+++ b/core/lib/Drupal/Core/Menu/Plugin/Block/LocalActionsBlock.php
@@ -2,20 +2,21 @@
 
 namespace Drupal\Core\Menu\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Menu\LocalActionManagerInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
 
 /**
  * Provides a block to display the local actions.
- *
- * @Block(
- *   id = "local_actions_block",
- *   admin_label = @Translation("Primary admin actions")
- * )
  */
+#[Block(
+  id: "local_actions_block",
+  admin_label: new TranslatableMarkup("Primary admin actions")
+)]
 class LocalActionsBlock extends BlockBase implements ContainerFactoryPluginInterface {
 
   /**
diff --git a/core/lib/Drupal/Core/Menu/Plugin/Block/LocalTasksBlock.php b/core/lib/Drupal/Core/Menu/Plugin/Block/LocalTasksBlock.php
index 6a518ef3eff0514365f6abf5272a8cfe9d3e4153..0d7cf1c3a2d796565794ec2dad1884254e7bdff6 100644
--- a/core/lib/Drupal/Core/Menu/Plugin/Block/LocalTasksBlock.php
+++ b/core/lib/Drupal/Core/Menu/Plugin/Block/LocalTasksBlock.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Core\Menu\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Form\FormStateInterface;
@@ -9,16 +10,16 @@
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Provides a "Tabs" block to display the local tasks.
- *
- * @Block(
- *   id = "local_tasks_block",
- *   admin_label = @Translation("Tabs"),
- * )
  */
+#[Block(
+  id: "local_tasks_block",
+  admin_label: new TranslatableMarkup("Tabs")
+)]
 class LocalTasksBlock extends BlockBase implements ContainerFactoryPluginInterface {
 
   /**
diff --git a/core/lib/Drupal/Core/Plugin/Context/ContextDefinition.php b/core/lib/Drupal/Core/Plugin/Context/ContextDefinition.php
index bfc234b958e6107d5751e396fc6898f38005935d..e68a4c0a40718c8dac914f27acb0eb33bc4c64ed 100644
--- a/core/lib/Drupal/Core/Plugin/Context/ContextDefinition.php
+++ b/core/lib/Drupal/Core/Plugin/Context/ContextDefinition.php
@@ -104,14 +104,20 @@ public static function create($data_type = 'any') {
    *   The description of this context definition for the UI.
    * @param mixed $default_value
    *   The default value of this definition.
+   * @param array $constraints
+   *   An array of constraints keyed by the constraint name and a value of an
+   *   array constraint options or a NULL.
    */
-  public function __construct($data_type = 'any', $label = NULL, $required = TRUE, $multiple = FALSE, $description = NULL, $default_value = NULL) {
+  public function __construct($data_type = 'any', $label = NULL, $required = TRUE, $multiple = FALSE, $description = NULL, $default_value = NULL, array $constraints = []) {
     $this->dataType = $data_type;
     $this->label = $label;
     $this->isRequired = $required;
     $this->isMultiple = $multiple;
     $this->description = $description;
     $this->defaultValue = $default_value;
+    foreach ($constraints as $constraint_name => $options) {
+      $this->addConstraint($constraint_name, $options);
+    }
 
     assert(!str_starts_with($data_type, 'entity:') || $this instanceof EntityContextDefinition);
   }
diff --git a/core/lib/Drupal/Core/Plugin/Context/EntityContextDefinition.php b/core/lib/Drupal/Core/Plugin/Context/EntityContextDefinition.php
index abdcf05116f37e495a6bf40ec8770481ddaad8f4..dfb8d0019bf06fb5d5880af6ac71c2829ace743f 100644
--- a/core/lib/Drupal/Core/Plugin/Context/EntityContextDefinition.php
+++ b/core/lib/Drupal/Core/Plugin/Context/EntityContextDefinition.php
@@ -17,13 +17,13 @@ class EntityContextDefinition extends ContextDefinition {
   /**
    * {@inheritdoc}
    */
-  public function __construct($data_type = 'any', $label = NULL, $required = TRUE, $multiple = FALSE, $description = NULL, $default_value = NULL) {
+  public function __construct($data_type = 'any', $label = NULL, $required = TRUE, $multiple = FALSE, $description = NULL, $default_value = NULL, array $constraints = []) {
     // Prefix the data type with 'entity:' so that this class can be constructed
     // like so: new EntityContextDefinition('node')
     if (!str_starts_with($data_type, 'entity:')) {
       $data_type = "entity:$data_type";
     }
-    parent::__construct($data_type, $label, $required, $multiple, $description, $default_value);
+    parent::__construct($data_type, $label, $required, $multiple, $description, $default_value, $constraints);
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php b/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php
index 1a4a7daee1207476e777edf35f07b6c40c644c29..fd3d280ff700cf44c56db008a5f6e0e7df142634 100644
--- a/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php
+++ b/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php
@@ -3,12 +3,15 @@
 namespace Drupal\Core\Plugin;
 
 use Drupal\Component\Assertion\Inspector;
+use Drupal\Component\Plugin\Attribute\AttributeInterface;
 use Drupal\Component\Plugin\Definition\PluginDefinitionInterface;
 use Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface;
 use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Cache\UseCacheBackendTrait;
 use Drupal\Component\Plugin\Discovery\DiscoveryCachedTrait;
+use Drupal\Core\Plugin\Discovery\AttributeClassDiscovery;
+use Drupal\Core\Plugin\Discovery\AttributeDiscoveryWithAnnotations;
 use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
 use Drupal\Component\Plugin\PluginManagerBase;
 use Drupal\Component\Plugin\PluginManagerInterface;
@@ -82,6 +85,13 @@ class DefaultPluginManager extends PluginManagerBase implements PluginManagerInt
    */
   protected $pluginDefinitionAnnotationName;
 
+  /**
+   * The name of the attribute that contains the plugin definition.
+   *
+   * @var string
+   */
+  protected $pluginDefinitionAttributeName;
+
   /**
    * The interface each plugin should implement.
    *
@@ -121,19 +131,33 @@ class DefaultPluginManager extends PluginManagerBase implements PluginManagerInt
    *   The module handler.
    * @param string|null $plugin_interface
    *   (optional) The interface each plugin should implement.
-   * @param string $plugin_definition_annotation_name
+   * @param string|null $plugin_definition_attribute_name
+   *   (optional) The name of the attribute that contains the plugin definition.
+   * @param string|array|null $plugin_definition_annotation_name
    *   (optional) The name of the annotation that contains the plugin definition.
    *   Defaults to 'Drupal\Component\Annotation\Plugin'.
    * @param string[] $additional_annotation_namespaces
    *   (optional) Additional namespaces to scan for annotation definitions.
+   *
+   * @todo $plugin_definition_attribute_name should default to
+   * 'Drupal\Component\Plugin\Attribute\Plugin' once annotations are no longer
+   * supported.
    */
-  public function __construct($subdir, \Traversable $namespaces, ModuleHandlerInterface $module_handler, $plugin_interface = NULL, $plugin_definition_annotation_name = 'Drupal\Component\Annotation\Plugin', array $additional_annotation_namespaces = []) {
+  public function __construct($subdir, \Traversable $namespaces, ModuleHandlerInterface $module_handler, $plugin_interface = NULL, ?string $plugin_definition_attribute_name = NULL, string|array $plugin_definition_annotation_name = NULL, array $additional_annotation_namespaces = []) {
     $this->subdir = $subdir;
     $this->namespaces = $namespaces;
-    $this->pluginDefinitionAnnotationName = $plugin_definition_annotation_name;
-    $this->pluginInterface = $plugin_interface;
     $this->moduleHandler = $module_handler;
-    $this->additionalAnnotationNamespaces = $additional_annotation_namespaces;
+    $this->pluginInterface = $plugin_interface;
+    if (is_subclass_of($plugin_definition_attribute_name, AttributeInterface::class)) {
+      $this->pluginDefinitionAttributeName = $plugin_definition_attribute_name;
+      $this->pluginDefinitionAnnotationName = $plugin_definition_annotation_name;
+      $this->additionalAnnotationNamespaces = $additional_annotation_namespaces;
+    }
+    else {
+      // Backward compatibility.
+      $this->pluginDefinitionAnnotationName = $plugin_definition_attribute_name ?? 'Drupal\Component\Annotation\Plugin';
+      $this->additionalAnnotationNamespaces = $plugin_definition_annotation_name ?? [];
+    }
   }
 
   /**
@@ -265,7 +289,15 @@ public function processDefinition(&$definition, $plugin_id) {
    */
   protected function getDiscovery() {
     if (!$this->discovery) {
-      $discovery = new AnnotatedClassDiscovery($this->subdir, $this->namespaces, $this->pluginDefinitionAnnotationName, $this->additionalAnnotationNamespaces);
+      if (isset($this->pluginDefinitionAttributeName) && isset($this->pluginDefinitionAnnotationName)) {
+        $discovery = new AttributeDiscoveryWithAnnotations($this->subdir, $this->namespaces, $this->pluginDefinitionAttributeName, $this->pluginDefinitionAnnotationName, $this->additionalAnnotationNamespaces);
+      }
+      elseif (isset($this->pluginDefinitionAttributeName)) {
+        $discovery = new AttributeClassDiscovery($this->subdir, $this->namespaces, $this->pluginDefinitionAttributeName);
+      }
+      else {
+        $discovery = new AnnotatedClassDiscovery($this->subdir, $this->namespaces, $this->pluginDefinitionAnnotationName, $this->additionalAnnotationNamespaces);
+      }
       $this->discovery = new ContainerDerivativeDiscoveryDecorator($discovery);
     }
     return $this->discovery;
diff --git a/core/lib/Drupal/Core/Plugin/Discovery/AttributeClassDiscovery.php b/core/lib/Drupal/Core/Plugin/Discovery/AttributeClassDiscovery.php
new file mode 100644
index 0000000000000000000000000000000000000000..38b0ff715b034e6ecefd4390c81564024a227890
--- /dev/null
+++ b/core/lib/Drupal/Core/Plugin/Discovery/AttributeClassDiscovery.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace Drupal\Core\Plugin\Discovery;
+
+use Drupal\Component\Plugin\Attribute\AttributeInterface;
+use Drupal\Component\Plugin\Discovery\AttributeClassDiscovery as ComponentAttributeClassDiscovery;
+
+/**
+ * Defines a discovery mechanism to find plugins using attributes.
+ */
+class AttributeClassDiscovery extends ComponentAttributeClassDiscovery {
+
+  /**
+   * A suffix to append to each PSR-4 directory associated with a base namespace.
+   *
+   * This suffix is used to form the directories where plugins are found.
+   *
+   * @var string
+   */
+  protected $directorySuffix = '';
+
+  /**
+   * A suffix to append to each base namespace.
+   *
+   * This suffix is used to obtain the namespaces where plugins are found.
+   *
+   * @var string
+   */
+  protected $namespaceSuffix = '';
+
+  /**
+   * Constructs an AttributeClassDiscovery object.
+   *
+   * @param string $subdir
+   *   Either the plugin's subdirectory, for example 'Plugin/views/filter', or
+   *   empty string if plugins are located at the top level of the namespace.
+   * @param \Traversable $rootNamespacesIterator
+   *   An object that implements \Traversable which contains the root paths
+   *   keyed by the corresponding namespace to look for plugin implementations.
+   *   If $subdir is not an empty string, it will be appended to each namespace.
+   * @param string $pluginDefinitionAttributeName
+   *   (optional) The name of the attribute that contains the plugin definition.
+   *   Defaults to 'Drupal\Component\Plugin\Attribute\Plugin'.
+   */
+  public function __construct(
+    string $subdir,
+    protected \Traversable $rootNamespacesIterator,
+    string $pluginDefinitionAttributeName = 'Drupal\Component\Plugin\Attribute\Plugin',
+  ) {
+    if ($subdir) {
+      // Prepend a directory separator to $subdir,
+      // if it does not already have one.
+      if ('/' !== $subdir[0]) {
+        $subdir = '/' . $subdir;
+      }
+      $this->directorySuffix = $subdir;
+      $this->namespaceSuffix = str_replace('/', '\\', $subdir);
+    }
+    parent::__construct([], $pluginDefinitionAttributeName);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function prepareAttributeDefinition(AttributeInterface $attribute, string $class): void {
+    parent::prepareAttributeDefinition($attribute, $class);
+
+    if (!$attribute->getProvider()) {
+      $attribute->setProvider($this->getProviderFromNamespace($class));
+    }
+  }
+
+  /**
+   * Extracts the provider name from a Drupal namespace.
+   *
+   * @param string $namespace
+   *   The namespace to extract the provider from.
+   *
+   * @return string|null
+   *   The matching provider name, or NULL otherwise.
+   */
+  protected function getProviderFromNamespace(string $namespace): ?string {
+    preg_match('|^Drupal\\\\(?<provider>[\w]+)\\\\|', $namespace, $matches);
+
+    if (isset($matches['provider'])) {
+      return mb_strtolower($matches['provider']);
+    }
+
+    return NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getPluginNamespaces(): array {
+    $plugin_namespaces = [];
+    if ($this->namespaceSuffix) {
+      foreach ($this->rootNamespacesIterator as $namespace => $dirs) {
+        // Append the namespace suffix to the base namespace, to obtain the
+        // plugin namespace; for example, 'Drupal\views' may become
+        // 'Drupal\views\Plugin\Block'.
+        $namespace .= $this->namespaceSuffix;
+        foreach ((array) $dirs as $dir) {
+          // Append the directory suffix to the PSR-4 base directory, to obtain
+          // the directory where plugins are found. For example,
+          // DRUPAL_ROOT . '/core/modules/views/src' may become
+          // DRUPAL_ROOT . '/core/modules/views/src/Plugin/Block'.
+          $plugin_namespaces[$namespace][] = $dir . $this->directorySuffix;
+        }
+      }
+    }
+    else {
+      // Both the namespace suffix and the directory suffix are empty,
+      // so the plugin namespaces and directories are the same as the base
+      // directories.
+      foreach ($this->rootNamespacesIterator as $namespace => $dirs) {
+        $plugin_namespaces[$namespace] = (array) $dirs;
+      }
+    }
+
+    return $plugin_namespaces;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Plugin/Discovery/AttributeDiscoveryWithAnnotations.php b/core/lib/Drupal/Core/Plugin/Discovery/AttributeDiscoveryWithAnnotations.php
new file mode 100644
index 0000000000000000000000000000000000000000..f0ddae339329b9eab1b9bbe709c524003e7c3fc2
--- /dev/null
+++ b/core/lib/Drupal/Core/Plugin/Discovery/AttributeDiscoveryWithAnnotations.php
@@ -0,0 +1,150 @@
+<?php
+
+namespace Drupal\Core\Plugin\Discovery;
+
+use Doctrine\Common\Annotations\AnnotationRegistry;
+use Drupal\Component\Annotation\AnnotationInterface;
+use Drupal\Component\Annotation\Doctrine\SimpleAnnotationReader;
+use Drupal\Component\Annotation\Doctrine\StaticReflectionParser;
+use Drupal\Component\Annotation\Reflection\MockFileFinder;
+use Drupal\Component\Utility\Crypt;
+
+/**
+ * Enables both attribute and annotation discovery for plugin definitions.
+ */
+class AttributeDiscoveryWithAnnotations extends AttributeClassDiscovery {
+
+  /**
+   * The doctrine annotation reader.
+   *
+   * @var \Doctrine\Common\Annotations\Reader
+   */
+  protected $annotationReader;
+
+  /**
+   * Constructs an AttributeDiscoveryWithAnnotations object.
+   *
+   * @param string $subdir
+   *   Either the plugin's subdirectory, for example 'Plugin/views/filter', or
+   *   empty string if plugins are located at the top level of the namespace.
+   * @param \Traversable $rootNamespaces
+   *   An object that implements \Traversable which contains the root paths
+   *   keyed by the corresponding namespace to look for plugin implementations.
+   *   If $subdir is not an empty string, it will be appended to each namespace.
+   * @param string $pluginDefinitionAttributeName
+   *   (optional) The name of the attribute that contains the plugin definition.
+   *   Defaults to 'Drupal\Component\Plugin\Attribute\Plugin'.
+   * @param string $pluginDefinitionAnnotationName
+   *   (optional) The name of the attribute that contains the plugin definition.
+   *   Defaults to 'Drupal\Component\Annotation\Plugin'.
+   * @param string[] $additionalNamespaces
+   *   (optional) Additional namespaces to scan for attribute definitions.
+   */
+  public function __construct(
+    string $subdir,
+    \Traversable $rootNamespaces,
+    string $pluginDefinitionAttributeName = 'Drupal\Component\Plugin\Attribute\Plugin',
+    protected readonly string $pluginDefinitionAnnotationName = 'Drupal\Component\Annotation\Plugin',
+    protected readonly array $additionalNamespaces = [],
+  ) {
+    parent::__construct($subdir, $rootNamespaces, $pluginDefinitionAttributeName);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getFileCacheSuffix(string $default_suffix):string {
+    return $default_suffix . ':' . Crypt::hashBase64(serialize($this->additionalNamespaces)) . ':' . str_replace('\\', '_', $this->pluginDefinitionAnnotationName);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefinitions() {
+    // Clear the annotation loaders of any previous annotation classes.
+    AnnotationRegistry::reset();
+    // Register the namespaces of classes that can be used for annotations.
+    // @phpstan-ignore-next-line
+    AnnotationRegistry::registerLoader('class_exists');
+
+    $definitions = parent::getDefinitions();
+
+    $this->annotationReader = NULL;
+
+    return $definitions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function parseClass(string $class, \SplFileInfo $fileinfo): array {
+    // The filename is already known, so there is no need to find the
+    // file. However, StaticReflectionParser needs a finder, so use a
+    // mock version.
+    $finder = MockFileFinder::create($fileinfo->getPathName());
+    $parser = new StaticReflectionParser($class, $finder, TRUE);
+
+    // @todo Handle deprecating definitions discovery via annotations in
+    // https://www.drupal.org/project/drupal/issues/3265945.
+    /** @var \Drupal\Component\Annotation\AnnotationInterface $annotation */
+    if ($annotation = $this->getAnnotationReader()->getClassAnnotation($parser->getReflectionClass(), $this->pluginDefinitionAnnotationName)) {
+      $this->prepareAnnotationDefinition($annotation, $class);
+      return ['id' => $annotation->getId(), 'content' => $annotation->get()];
+    }
+
+    return parent::parseClass($class, $fileinfo);
+  }
+
+  /**
+   * Prepares the annotation definition.
+   *
+   * This is a copy of the prepareAnnotationDefinition method from annotated
+   * class discovery.
+   *
+   * @param \Drupal\Component\Annotation\AnnotationInterface $annotation
+   *   The annotation derived from the plugin.
+   * @param class-string $class
+   *   The class used for the plugin.
+   *
+   * @see \Drupal\Component\Annotation\Plugin\Discovery\AnnotatedClassDiscovery::prepareAnnotationDefinition()
+   * @see \Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery::prepareAnnotationDefinition()
+   */
+  private function prepareAnnotationDefinition(AnnotationInterface $annotation, string $class): void {
+    $annotation->setClass($class);
+    if (!$annotation->getProvider()) {
+      $annotation->setProvider($this->getProviderFromNamespace($class));
+    }
+  }
+
+  /**
+   * Gets the used doctrine annotation reader.
+   *
+   * This is a copy of the getAnnotationReader method from annotated class
+   * discovery.
+   *
+   * @return \Drupal\Component\Annotation\Doctrine\SimpleAnnotationReader
+   *   The annotation reader.
+   *
+   * @see \Drupal\Component\Annotation\Plugin\Discovery\AnnotatedClassDiscovery::getAnnotationReader()
+   * @see \Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery::getAnnotationReader()
+   */
+  private function getAnnotationReader() : SimpleAnnotationReader {
+    if (!isset($this->annotationReader)) {
+      $this->annotationReader = new SimpleAnnotationReader();
+
+      // Add the namespaces from the main plugin annotation, like @EntityType.
+      $namespace = substr($this->pluginDefinitionAnnotationName, 0, strrpos($this->pluginDefinitionAnnotationName, '\\'));
+      $this->annotationReader->addNamespace($namespace);
+
+      // Register additional namespaces to be scanned for annotations.
+      foreach ($this->additionalNamespaces as $namespace) {
+        $this->annotationReader->addNamespace($namespace);
+      }
+
+      // Add the Core annotation classes like @Translation.
+      $this->annotationReader->addNamespace('Drupal\Core\Annotation');
+    }
+    return $this->annotationReader;
+  }
+
+}
diff --git a/core/modules/action/tests/action_form_ajax_test/src/Plugin/Action/ActionAjaxTest.php b/core/modules/action/tests/action_form_ajax_test/src/Plugin/Action/ActionAjaxTest.php
index 8afc98cf54110cf1646e16c1d07fdeb59a0b8822..827438c69645949888f008bac275610c69a79f9d 100644
--- a/core/modules/action/tests/action_form_ajax_test/src/Plugin/Action/ActionAjaxTest.php
+++ b/core/modules/action/tests/action_form_ajax_test/src/Plugin/Action/ActionAjaxTest.php
@@ -4,18 +4,19 @@
 
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Action\ConfigurableActionBase;
+use Drupal\Core\Action\Attribute\Action;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Plugin used for testing AJAX in action config entity forms.
- *
- * @Action(
- *   id = "action_form_ajax_test",
- *   label = @Translation("action_form_ajax_test"),
- *   type = "system"
- * )
  */
+#[Action(
+  id: 'action_form_ajax_test',
+  label: new TranslatableMarkup('action_form_ajax_test'),
+  type: 'system'
+)]
 class ActionAjaxTest extends ConfigurableActionBase {
 
   /**
diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestAccessBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestAccessBlock.php
index 4bb9f24cc0fd00811dd9261b36a89f78d87255f9..f663f701f2c4a4f20485a6f789164c0f7c25c13c 100644
--- a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestAccessBlock.php
+++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestAccessBlock.php
@@ -3,21 +3,22 @@
 namespace Drupal\block_test\Plugin\Block;
 
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\State\StateInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Provides a block to test access.
- *
- * @Block(
- *   id = "test_access",
- *   admin_label = @Translation("Test block access")
- * )
  */
+#[Block(
+  id: "test_access",
+  admin_label: new TranslatableMarkup("Test block access"),
+)]
 class TestAccessBlock extends BlockBase implements ContainerFactoryPluginInterface {
 
   /**
diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestBlockInstantiation.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestBlockInstantiation.php
index 0d146390c518573757ec71bb3f1f0dfaa030e607..77df48e8d995ed3252d031097f79339c38b9133a 100644
--- a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestBlockInstantiation.php
+++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestBlockInstantiation.php
@@ -3,18 +3,19 @@
 namespace Drupal\block_test\Plugin\Block;
 
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Provides a basic block for testing block instantiation and configuration.
- *
- * @Block(
- *   id = "test_block_instantiation",
- *   admin_label = @Translation("Display message")
- * )
  */
+#[Block(
+  id: "test_block_instantiation",
+  admin_label: new TranslatableMarkup("Display message")
+)]
 class TestBlockInstantiation extends BlockBase {
 
   /**
diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestCacheBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestCacheBlock.php
index 5fec14ccc7719f1c0045651bb1717531bb5b5aa8..dc27b08ef09c44838b75ae37decabbb16c51d0f4 100644
--- a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestCacheBlock.php
+++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestCacheBlock.php
@@ -2,16 +2,17 @@
 
 namespace Drupal\block_test\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Provides a block to test caching.
- *
- * @Block(
- *   id = "test_cache",
- *   admin_label = @Translation("Test block caching")
- * )
  */
+#[Block(
+  id: "test_cache",
+  admin_label: new TranslatableMarkup("Test block caching")
+)]
 class TestCacheBlock extends BlockBase {
 
   /**
diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareBlock.php
index e1df7485107df1ae15b8f5645f66e6f832b422b8..fd78dafdc8ffcb5683483085de9350cafb22cb9d 100644
--- a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareBlock.php
+++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareBlock.php
@@ -2,23 +2,30 @@
 
 namespace Drupal\block_test\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
+use Drupal\Core\Plugin\Context\EntityContextDefinition;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\user\UserInterface;
 
 /**
  * Provides a context-aware block.
- *
- * @Block(
- *   id = "test_context_aware",
- *   admin_label = @Translation("Test context-aware block"),
- *   context_definitions = {
- *     "user" = @ContextDefinition("entity:user", required = FALSE,
- *       label = @Translation("User Context"), constraints = { "NotNull" = {} }
- *     ),
- *   }
- * )
  */
+#[Block(
+  id: "test_context_aware",
+  admin_label: new TranslatableMarkup("Test context-aware block"),
+  context_definitions: [
+    'user' => new EntityContextDefinition(
+      data_type: 'entity:user',
+      label: new TranslatableMarkup("User Context"),
+      required: FALSE,
+      constraints: [
+        "NotNull" => [],
+      ]
+    ),
+  ]
+)]
 class TestContextAwareBlock extends BlockBase {
 
   /**
diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareNoValidContextOptionsBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareNoValidContextOptionsBlock.php
index 234bf9a3015d763b17699ba432a0bd51c0f47b2d..31944bed464143833d2559ccb4bd3b999ac4993e 100644
--- a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareNoValidContextOptionsBlock.php
+++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareNoValidContextOptionsBlock.php
@@ -2,19 +2,21 @@
 
 namespace Drupal\block_test\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
+use Drupal\Core\Plugin\Context\ContextDefinition;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Provides a context-aware block that uses a not-passed, non-required context.
- *
- * @Block(
- *   id = "test_context_aware_no_valid_context_options",
- *   admin_label = @Translation("Test context-aware block - no valid context options"),
- *   context_definitions = {
- *     "email" = @ContextDefinition("email", required = FALSE)
- *   }
- * )
  */
+#[Block(
+  id: "test_context_aware_no_valid_context_options",
+  admin_label: new TranslatableMarkup("Test context-aware block - no valid context options"),
+  context_definitions: [
+    'user' => new ContextDefinition(data_type: 'email', required: FALSE),
+  ]
+)]
 class TestContextAwareNoValidContextOptionsBlock extends BlockBase {
 
   /**
diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareUnsatisfiedBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareUnsatisfiedBlock.php
index d0789e37a7d1d12b48bda1dc70a861273cdb93e5..74dc183794561c358d9f653a822693065f89a19c 100644
--- a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareUnsatisfiedBlock.php
+++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareUnsatisfiedBlock.php
@@ -2,19 +2,21 @@
 
 namespace Drupal\block_test\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
+use Drupal\Core\Plugin\Context\EntityContextDefinition;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Provides a context-aware block.
- *
- * @Block(
- *   id = "test_context_aware_unsatisfied",
- *   admin_label = @Translation("Test context-aware unsatisfied block"),
- *   context_definitions = {
- *     "user" = @ContextDefinition("entity:foobar")
- *   }
- * )
  */
+#[Block(
+  id: "test_context_aware_unsatisfied",
+  admin_label: new TranslatableMarkup("Test context-aware unsatisfied block"),
+  context_definitions: [
+    'user' => new EntityContextDefinition('entity:foobar'),
+  ]
+)]
 class TestContextAwareUnsatisfiedBlock extends BlockBase {
 
   /**
diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestFormBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestFormBlock.php
index e499fa88c90e035e6f27f4836254d565e637e75d..c4eef85d0ee01d538d0a7d8a00236ea69b75e32f 100644
--- a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestFormBlock.php
+++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestFormBlock.php
@@ -2,16 +2,17 @@
 
 namespace Drupal\block_test\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Provides a block to test caching.
- *
- * @Block(
- *   id = "test_form_in_block",
- *   admin_label = @Translation("Test form block caching")
- * )
  */
+#[Block(
+  id: "test_form_in_block",
+  admin_label: new TranslatableMarkup("Test form block caching"),
+)]
 class TestFormBlock extends BlockBase {
 
   /**
diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestHtmlBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestHtmlBlock.php
index 35f08c4b041cd6a471d85fd864be67ac0206c82f..5644c3b5cc1416f543e0d44a09abfa4a8ed3ed12 100644
--- a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestHtmlBlock.php
+++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestHtmlBlock.php
@@ -2,16 +2,17 @@
 
 namespace Drupal\block_test\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Provides a block to test HTML.
- *
- * @Block(
- *   id = "test_html",
- *   admin_label = @Translation("Test HTML block")
- * )
  */
+#[Block(
+  id: "test_html",
+  admin_label: new TranslatableMarkup("Test HTML block"),
+)]
 class TestHtmlBlock extends BlockBase {
 
   /**
diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestMultipleFormsBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestMultipleFormsBlock.php
index 64b70b22c04ef34b8d520495c9e7719d07e9eb8a..f227f337b6dadd057587d49b604af9bfaf23369d 100644
--- a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestMultipleFormsBlock.php
+++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestMultipleFormsBlock.php
@@ -2,19 +2,21 @@
 
 namespace Drupal\block_test\Plugin\Block;
 
+use Drupal\block_test\PluginForm\EmptyBlockForm;
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Provides a block with multiple forms.
- *
- * @Block(
- *   id = "test_multiple_forms_block",
- *   forms = {
- *     "secondary" = "\Drupal\block_test\PluginForm\EmptyBlockForm"
- *   },
- *   admin_label = @Translation("Multiple forms test block")
- * )
  */
+#[Block(
+  id: "test_multiple_forms_block",
+  forms: [
+    'secondary' => EmptyBlockForm::class,
+  ],
+  admin_label: new TranslatableMarkup("Multiple forms test block"),
+)]
 class TestMultipleFormsBlock extends BlockBase {
 
   /**
diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestSettingsValidationBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestSettingsValidationBlock.php
index d1f16d7fda40c1e9d981b8a6eb2348a9e18c0886..cad100b00734cf55a476db32d2bb4cb1e74bcc3c 100644
--- a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestSettingsValidationBlock.php
+++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestSettingsValidationBlock.php
@@ -2,17 +2,18 @@
 
 namespace Drupal\block_test\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Provides a test settings validation block.
- *
- * @Block(
- *  id = "test_settings_validation",
- *  admin_label = @Translation("Test settings validation block"),
- * )
  */
+#[Block(
+  id: "test_settings_validation",
+  admin_label: new TranslatableMarkup("Test settings validation block"),
+)]
 class TestSettingsValidationBlock extends BlockBase {
 
   /**
diff --git a/core/modules/block_content/src/Plugin/Block/BlockContentBlock.php b/core/modules/block_content/src/Plugin/Block/BlockContentBlock.php
index 1b2a9f19a161e3129443d7e7c7018bd6ea6c4f75..7c862f499fd418c8047d14f7f8a815c03b8c9f33 100644
--- a/core/modules/block_content/src/Plugin/Block/BlockContentBlock.php
+++ b/core/modules/block_content/src/Plugin/Block/BlockContentBlock.php
@@ -3,7 +3,9 @@
 namespace Drupal\block_content\Plugin\Block;
 
 use Drupal\block_content\BlockContentUuidLookup;
+use Drupal\block_content\Plugin\Derivative\BlockContent;
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Block\BlockManagerInterface;
 use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
@@ -12,18 +14,18 @@
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Routing\UrlGeneratorInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Defines a generic block type.
- *
- * @Block(
- *  id = "block_content",
- *  admin_label = @Translation("Content block"),
- *  category = @Translation("Content block"),
- *  deriver = "Drupal\block_content\Plugin\Derivative\BlockContent"
- * )
  */
+#[Block(
+  id: "block_content",
+  admin_label: new TranslatableMarkup("Content block"),
+  category: new TranslatableMarkup("Content block"),
+  deriver: BlockContent::class
+)]
 class BlockContentBlock extends BlockBase implements ContainerFactoryPluginInterface {
 
   /**
diff --git a/core/modules/book/src/Plugin/Block/BookNavigationBlock.php b/core/modules/book/src/Plugin/Block/BookNavigationBlock.php
index 86d957739f038ceca24ee55f724615cf14ed4602..578ea3655d0d4ee9cc24abe3fbd37670297df855 100644
--- a/core/modules/book/src/Plugin/Block/BookNavigationBlock.php
+++ b/core/modules/book/src/Plugin/Block/BookNavigationBlock.php
@@ -2,11 +2,13 @@
 
 namespace Drupal\book\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\book\BookManagerInterface;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\node\NodeInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
@@ -14,13 +16,12 @@
 
 /**
  * Provides a 'Book navigation' block.
- *
- * @Block(
- *   id = "book_navigation",
- *   admin_label = @Translation("Book navigation"),
- *   category = @Translation("Menus")
- * )
  */
+#[Block(
+  id: "book_navigation",
+  admin_label: new TranslatableMarkup("Book navigation"),
+  category: new TranslatableMarkup("Menus")
+)]
 class BookNavigationBlock extends BlockBase implements ContainerFactoryPluginInterface {
 
   /**
diff --git a/core/modules/comment/src/Plugin/Action/UnpublishByKeywordComment.php b/core/modules/comment/src/Plugin/Action/UnpublishByKeywordComment.php
index 054196509900f2433a4cfbda9604b42a558f4147..891ae25631445d297bc9ab1051bab7c10ece8f10 100644
--- a/core/modules/comment/src/Plugin/Action/UnpublishByKeywordComment.php
+++ b/core/modules/comment/src/Plugin/Action/UnpublishByKeywordComment.php
@@ -4,22 +4,23 @@
 
 use Drupal\Component\Utility\Tags;
 use Drupal\Core\Action\ConfigurableActionBase;
+use Drupal\Core\Action\Attribute\Action;
 use Drupal\Core\Entity\EntityViewBuilderInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Unpublishes a comment containing certain keywords.
- *
- * @Action(
- *   id = "comment_unpublish_by_keyword_action",
- *   label = @Translation("Unpublish comment containing keyword(s)"),
- *   type = "comment"
- * )
  */
+#[Action(
+  id: 'comment_unpublish_by_keyword_action',
+  label: new TranslatableMarkup('Unpublish comment containing keyword(s)'),
+  type: 'comment'
+)]
 class UnpublishByKeywordComment extends ConfigurableActionBase implements ContainerFactoryPluginInterface {
 
   /**
diff --git a/core/modules/editor/tests/src/Functional/EditorDialogAccessTest.php b/core/modules/editor/tests/src/Functional/EditorDialogAccessTest.php
index f12dca0bbf49f020bfa502030261daffaead48bd..dc8b8d7a6588a59f61072ec73c5340b5d0262d9a 100644
--- a/core/modules/editor/tests/src/Functional/EditorDialogAccessTest.php
+++ b/core/modules/editor/tests/src/Functional/EditorDialogAccessTest.php
@@ -18,7 +18,7 @@ class EditorDialogAccessTest extends BrowserTestBase {
    *
    * @var array
    */
-  protected static $modules = ['editor', 'filter', 'editor_test'];
+  protected static $modules = ['editor', 'filter', 'text', 'editor_test'];
 
   /**
    * {@inheritdoc}
diff --git a/core/modules/forum/src/Plugin/Block/ActiveTopicsBlock.php b/core/modules/forum/src/Plugin/Block/ActiveTopicsBlock.php
index 921e7ed04b685d0179515c1c3d60de334e2763ba..c4b4bf0ef212e36abbaca23560623001a42a3dde 100644
--- a/core/modules/forum/src/Plugin/Block/ActiveTopicsBlock.php
+++ b/core/modules/forum/src/Plugin/Block/ActiveTopicsBlock.php
@@ -2,17 +2,18 @@
 
 namespace Drupal\forum\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Database\Database;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Provides an 'Active forum topics' block.
- *
- * @Block(
- *   id = "forum_active_block",
- *   admin_label = @Translation("Active forum topics"),
- *   category = @Translation("Lists (Views)")
- * )
  */
+#[Block(
+  id: "forum_active_block",
+  admin_label: new TranslatableMarkup("Active forum topics"),
+  category: new TranslatableMarkup("Lists (Views)")
+)]
 class ActiveTopicsBlock extends ForumBlockBase {
 
   /**
diff --git a/core/modules/forum/src/Plugin/Block/NewTopicsBlock.php b/core/modules/forum/src/Plugin/Block/NewTopicsBlock.php
index e1d2d1c1afbb44315c85bd3a446b2398590e4817..13d3bf0dc03ed3aa0d0c719f514e1e87e77a71dd 100644
--- a/core/modules/forum/src/Plugin/Block/NewTopicsBlock.php
+++ b/core/modules/forum/src/Plugin/Block/NewTopicsBlock.php
@@ -2,17 +2,18 @@
 
 namespace Drupal\forum\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Database\Database;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Provides a 'New forum topics' block.
- *
- * @Block(
- *   id = "forum_new_block",
- *   admin_label = @Translation("New forum topics"),
- *   category = @Translation("Lists (Views)")
- * )
  */
+#[Block(
+  id: "forum_new_block",
+  admin_label: new TranslatableMarkup("New forum topics"),
+  category: new TranslatableMarkup("Lists (Views)")
+)]
 class NewTopicsBlock extends ForumBlockBase {
 
   /**
diff --git a/core/modules/help/src/Plugin/Block/HelpBlock.php b/core/modules/help/src/Plugin/Block/HelpBlock.php
index 24cd3f3c505743696dba66b71ad3004f6ec427cb..8a593842a231e8073a1341d641f825bf4843aaf8 100644
--- a/core/modules/help/src/Plugin/Block/HelpBlock.php
+++ b/core/modules/help/src/Plugin/Block/HelpBlock.php
@@ -2,25 +2,24 @@
 
 namespace Drupal\help\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
 
 /**
  * Provides a 'Help' block.
- *
- * @Block(
- *   id = "help_block",
- *   admin_label = @Translation("Help"),
- *   forms = {
- *     "settings_tray" = FALSE,
- *   },
- * )
  */
+#[Block(
+  id: "help_block",
+  admin_label: new TranslatableMarkup("Help"),
+  forms: ['settings_tray' => FALSE]
+)]
 class HelpBlock extends BlockBase implements ContainerFactoryPluginInterface {
 
   /**
diff --git a/core/modules/language/src/Plugin/Block/LanguageBlock.php b/core/modules/language/src/Plugin/Block/LanguageBlock.php
index bc64f05b1db1f6ea1aae5c73619f6028a91afa9a..e672ce6583d785ea02753c74e8b995b1ca9e7f2e 100644
--- a/core/modules/language/src/Plugin/Block/LanguageBlock.php
+++ b/core/modules/language/src/Plugin/Block/LanguageBlock.php
@@ -3,24 +3,26 @@
 namespace Drupal\language\Plugin\Block;
 
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Path\PathMatcherInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\Url;
 use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\language\Plugin\Derivative\LanguageBlock as LanguageBlockDeriver;
 
 /**
  * Provides a 'Language switcher' block.
- *
- * @Block(
- *   id = "language_block",
- *   admin_label = @Translation("Language switcher"),
- *   category = @Translation("System"),
- *   deriver = "Drupal\language\Plugin\Derivative\LanguageBlock"
- * )
  */
+#[Block(
+  id: "language_block",
+  admin_label: new TranslatableMarkup("Language switcher"),
+  category: new TranslatableMarkup("System"),
+  deriver: LanguageBlockDeriver::class
+)]
 class LanguageBlock extends BlockBase implements ContainerFactoryPluginInterface {
 
   /**
diff --git a/core/modules/layout_builder/src/Plugin/Block/ExtraFieldBlock.php b/core/modules/layout_builder/src/Plugin/Block/ExtraFieldBlock.php
index 35c9ee905d893aa586eeddb4eb428f4654374e9a..cf6596077f1dbed71929aa8a40f4c42f7b0df8c4 100644
--- a/core/modules/layout_builder/src/Plugin/Block/ExtraFieldBlock.php
+++ b/core/modules/layout_builder/src/Plugin/Block/ExtraFieldBlock.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\layout_builder\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Entity\EntityFieldManagerInterface;
@@ -11,6 +12,7 @@
 use Drupal\Core\Render\Element;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\layout_builder\Plugin\Derivative\ExtraFieldBlockDeriver;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -23,14 +25,13 @@
  *   This block plugin handles all other field entities not provided by
  *   hook_entity_extra_field_info().
  *
- * @Block(
- *   id = "extra_field_block",
- *   deriver = "\Drupal\layout_builder\Plugin\Derivative\ExtraFieldBlockDeriver",
- * )
- *
  * @internal
  *   Plugin classes are internal.
  */
+#[Block(
+  id: "extra_field_block",
+  deriver: ExtraFieldBlockDeriver::class
+)]
 class ExtraFieldBlock extends BlockBase implements ContextAwarePluginInterface, ContainerFactoryPluginInterface {
 
   /**
diff --git a/core/modules/layout_builder/src/Plugin/Block/FieldBlock.php b/core/modules/layout_builder/src/Plugin/Block/FieldBlock.php
index 4318ebee2492a5af941668be66191d2f363286bc..578f9348eb72c8d4e2b1f5b954f75e8e552732a3 100644
--- a/core/modules/layout_builder/src/Plugin/Block/FieldBlock.php
+++ b/core/modules/layout_builder/src/Plugin/Block/FieldBlock.php
@@ -5,6 +5,7 @@
 use Drupal\Component\Plugin\Factory\DefaultFactory;
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Entity\EntityDisplayBase;
@@ -21,6 +22,7 @@
 use Drupal\Core\Plugin\ContextAwarePluginInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\layout_builder\Plugin\Derivative\FieldBlockDeriver;
 use Psr\Log\LoggerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Drupal\field\FieldLabelOptionsTrait;
@@ -28,14 +30,13 @@
 /**
  * Provides a block that renders a field from an entity.
  *
- * @Block(
- *   id = "field_block",
- *   deriver = "\Drupal\layout_builder\Plugin\Derivative\FieldBlockDeriver",
- * )
- *
  * @internal
  *   Plugin classes are internal.
  */
+#[Block(
+  id: "field_block",
+  deriver: FieldBlockDeriver::class
+)]
 class FieldBlock extends BlockBase implements ContextAwarePluginInterface, ContainerFactoryPluginInterface {
 
   use FieldLabelOptionsTrait;
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_fieldblock_test/src/Plugin/Block/FieldBlock.php b/core/modules/layout_builder/tests/modules/layout_builder_fieldblock_test/src/Plugin/Block/FieldBlock.php
index 07a00f5412b70fa258605683591f5130cc5a879f..bb7ac692ee7e1f62dadc7dffb6ddd2fc68900150 100644
--- a/core/modules/layout_builder/tests/modules/layout_builder_fieldblock_test/src/Plugin/Block/FieldBlock.php
+++ b/core/modules/layout_builder/tests/modules/layout_builder_fieldblock_test/src/Plugin/Block/FieldBlock.php
@@ -2,7 +2,9 @@
 
 namespace Drupal\layout_builder_fieldblock_test\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\layout_builder\Plugin\Block\FieldBlock as LayoutBuilderFieldBlock;
+use Drupal\layout_builder\Plugin\Derivative\FieldBlockDeriver;
 
 /**
  * Provides test field block to test with Block UI.
@@ -14,14 +16,13 @@
  * testing, this plugin uses the same deriver but each derivative will have a
  * different provider.
  *
- * @Block(
- *   id = "field_block_test",
- *   deriver = "\Drupal\layout_builder\Plugin\Derivative\FieldBlockDeriver",
- * )
- *
  * @see \Drupal\Tests\layout_builder\FunctionalJavascript\FieldBlockTest
  * @see layout_builder_plugin_filter_block__block_ui_alter()
  */
+#[Block(
+  id: "field_block_test",
+  deriver: FieldBlockDeriver::class
+)]
 class FieldBlock extends LayoutBuilderFieldBlock {
 
 }
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_form_block_test/src/Plugin/Block/TestFormApiFormBlock.php b/core/modules/layout_builder/tests/modules/layout_builder_form_block_test/src/Plugin/Block/TestFormApiFormBlock.php
index 791ccbba9b710203c9888ca780c819244d1394f6..970b24f4896c3edf7d1e7be67e4e0cf24055949c 100644
--- a/core/modules/layout_builder/tests/modules/layout_builder_form_block_test/src/Plugin/Block/TestFormApiFormBlock.php
+++ b/core/modules/layout_builder/tests/modules/layout_builder_form_block_test/src/Plugin/Block/TestFormApiFormBlock.php
@@ -2,22 +2,23 @@
 
 namespace Drupal\layout_builder_form_block_test\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Form\FormBuilderInterface;
 use Drupal\Core\Form\FormInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Provides a block containing a Form API form for use in Layout Builder tests.
- *
- * @Block(
- *   id = "layout_builder_form_block_test_form_api_form_block",
- *   admin_label = @Translation("Layout Builder form block test form api form block"),
- *   category = @Translation("Layout Builder form block test")
- * )
  */
+#[Block(
+  id: "layout_builder_form_block_test_form_api_form_block",
+  admin_label: new TranslatableMarkup("Layout Builder form block test form api form block"),
+  category: new TranslatableMarkup("Layout Builder form block test")
+)]
 class TestFormApiFormBlock extends BlockBase implements ContainerFactoryPluginInterface, FormInterface {
 
   /**
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_form_block_test/src/Plugin/Block/TestInlineTemplateFormBlock.php b/core/modules/layout_builder/tests/modules/layout_builder_form_block_test/src/Plugin/Block/TestInlineTemplateFormBlock.php
index c93f55d3cd4f022634dd0d2b7867eb4900e742a9..4d5582c088cae9c5701e51acd9561458861bf7cc 100644
--- a/core/modules/layout_builder/tests/modules/layout_builder_form_block_test/src/Plugin/Block/TestInlineTemplateFormBlock.php
+++ b/core/modules/layout_builder/tests/modules/layout_builder_form_block_test/src/Plugin/Block/TestInlineTemplateFormBlock.php
@@ -2,19 +2,20 @@
 
 namespace Drupal\layout_builder_form_block_test\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Provides a block containing inline template with <form> tag.
  *
  * For use in Layout Builder tests.
- *
- * @Block(
- *   id = "layout_builder_form_block_test_inline_template_form_block",
- *   admin_label = @Translation("Layout Builder form block test inline template form block"),
- *   category = @Translation("Layout Builder form block test")
- * )
  */
+#[Block(
+  id: "layout_builder_form_block_test_inline_template_form_block",
+  admin_label: new TranslatableMarkup("Layout Builder form block test inline template form block"),
+  category: new TranslatableMarkup("Layout Builder form block test")
+)]
 class TestInlineTemplateFormBlock extends BlockBase {
 
   /**
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Block/IHaveRuntimeContexts.php b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Block/IHaveRuntimeContexts.php
index e3832a60f77cc095963be31cf1938d0093f2cf1f..d24b8dd5c628d59021a6cac96e8b8f538565bb7c 100644
--- a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Block/IHaveRuntimeContexts.php
+++ b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Block/IHaveRuntimeContexts.php
@@ -2,20 +2,22 @@
 
 namespace Drupal\layout_builder_test\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
+use Drupal\Core\Plugin\Context\ContextDefinition;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Defines a class for a context-aware block.
- *
- * @Block(
- *   id = "i_have_runtime_contexts",
- *   admin_label = "Can I have runtime contexts",
- *   category = "Test",
- *   context_definitions = {
- *     "runtime_contexts" = @ContextDefinition("string", label = "Do you have runtime contexts")
- *   }
- * )
  */
+#[Block(
+  id: "i_have_runtime_contexts",
+  admin_label: new TranslatableMarkup("Can I have runtime contexts"),
+  category: new TranslatableMarkup("Test"),
+  context_definitions: [
+    'runtime_contexts' => new ContextDefinition('string', 'Do you have runtime contexts'),
+  ]
+)]
 class IHaveRuntimeContexts extends BlockBase {
 
   /**
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Block/TestAjaxBlock.php b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Block/TestAjaxBlock.php
index 1f1eeb0d7fc366261f8f94250a4dca6f02d849a3..71ac45c6afb5416a243a6762937b877046faf833 100644
--- a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Block/TestAjaxBlock.php
+++ b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Block/TestAjaxBlock.php
@@ -2,18 +2,19 @@
 
 namespace Drupal\layout_builder_test\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Provides a 'TestAjax' block.
- *
- * @Block(
- *   id = "layout_builder_test_testajax",
- *   admin_label = @Translation("TestAjax"),
- *   category = @Translation("Test")
- * )
  */
+#[Block(
+  id: "layout_builder_test_testajax",
+  admin_label: new TranslatableMarkup("TestAjax"),
+  category: new TranslatableMarkup("Test")
+)]
 class TestAjaxBlock extends BlockBase {
 
   /**
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Block/TestAttributesBlock.php b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Block/TestAttributesBlock.php
index 877fc3bfe7fa51a421893a05bc37a5246dad02ce..57b5cbf55982f055a2a04c160ee96ed05219a293 100644
--- a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Block/TestAttributesBlock.php
+++ b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Block/TestAttributesBlock.php
@@ -2,18 +2,19 @@
 
 namespace Drupal\layout_builder_test\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Provides a 'TestAttributes' block.
- *
- * @Block(
- *   id = "layout_builder_test_test_attributes",
- *   admin_label = @Translation("Test Attributes"),
- *   category = @Translation("Test")
- * )
  */
+#[Block(
+  id: "layout_builder_test_test_attributes",
+  admin_label: new TranslatableMarkup("Test Attributes"),
+  category: new TranslatableMarkup("Test")
+)]
 class TestAttributesBlock extends BlockBase {
 
   /**
diff --git a/core/modules/node/src/Plugin/Action/AssignOwnerNode.php b/core/modules/node/src/Plugin/Action/AssignOwnerNode.php
index 287dcb01898f54ccb4d6c8b12569456c5a0a188c..e764f91a478a6e1dd7c2a8eec9b1bf4cf6b5c051 100644
--- a/core/modules/node/src/Plugin/Action/AssignOwnerNode.php
+++ b/core/modules/node/src/Plugin/Action/AssignOwnerNode.php
@@ -3,22 +3,23 @@
 namespace Drupal\node\Plugin\Action;
 
 use Drupal\Core\Action\ConfigurableActionBase;
+use Drupal\Core\Action\Attribute\Action;
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\user\Entity\User;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Assigns ownership of a node to a user.
- *
- * @Action(
- *   id = "node_assign_owner_action",
- *   label = @Translation("Change the author of content"),
- *   type = "node"
- * )
  */
+#[Action(
+  id: 'node_assign_owner_action',
+  label: new TranslatableMarkup('Change the author of content'),
+  type: 'node'
+)]
 class AssignOwnerNode extends ConfigurableActionBase implements ContainerFactoryPluginInterface {
 
   /**
diff --git a/core/modules/node/src/Plugin/Action/DemoteNode.php b/core/modules/node/src/Plugin/Action/DemoteNode.php
index efacdb420e950afe099f4f6a2fb20fddb7f0d569..d3de76dfcce80e05e1b11d2896780c9fb4939506 100644
--- a/core/modules/node/src/Plugin/Action/DemoteNode.php
+++ b/core/modules/node/src/Plugin/Action/DemoteNode.php
@@ -2,18 +2,19 @@
 
 namespace Drupal\node\Plugin\Action;
 
+use Drupal\Core\Action\Attribute\Action;
 use Drupal\Core\Field\FieldUpdateActionBase;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\node\NodeInterface;
 
 /**
  * Demotes a node.
- *
- * @Action(
- *   id = "node_unpromote_action",
- *   label = @Translation("Demote selected content from front page"),
- *   type = "node"
- * )
  */
+#[Action(
+  id: 'node_unpromote_action',
+  label: new TranslatableMarkup('Demote selected content from front page'),
+  type: 'node'
+)]
 class DemoteNode extends FieldUpdateActionBase {
 
   /**
diff --git a/core/modules/node/src/Plugin/Action/PromoteNode.php b/core/modules/node/src/Plugin/Action/PromoteNode.php
index 1d0e61695db77bbe9ef14132eae0b891e8c577ca..5a539d7bbe2c3c03ec3168c544ab0305e80d58bd 100644
--- a/core/modules/node/src/Plugin/Action/PromoteNode.php
+++ b/core/modules/node/src/Plugin/Action/PromoteNode.php
@@ -2,18 +2,19 @@
 
 namespace Drupal\node\Plugin\Action;
 
+use Drupal\Core\Action\Attribute\Action;
 use Drupal\Core\Field\FieldUpdateActionBase;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\node\NodeInterface;
 
 /**
  * Promotes a node.
- *
- * @Action(
- *   id = "node_promote_action",
- *   label = @Translation("Promote selected content to front page"),
- *   type = "node"
- * )
  */
+#[Action(
+  id: 'node_promote_action',
+  label: new TranslatableMarkup('Promote selected content to front page'),
+  type: 'node'
+)]
 class PromoteNode extends FieldUpdateActionBase {
 
   /**
diff --git a/core/modules/node/src/Plugin/Action/StickyNode.php b/core/modules/node/src/Plugin/Action/StickyNode.php
index 679c3c019bb8492412c26c2a4b937bacc4441986..67090c7895f0d57812750ec7b428fc8e5648f0ad 100644
--- a/core/modules/node/src/Plugin/Action/StickyNode.php
+++ b/core/modules/node/src/Plugin/Action/StickyNode.php
@@ -2,18 +2,19 @@
 
 namespace Drupal\node\Plugin\Action;
 
+use Drupal\Core\Action\Attribute\Action;
 use Drupal\Core\Field\FieldUpdateActionBase;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\node\NodeInterface;
 
 /**
  * Makes a node sticky.
- *
- * @Action(
- *   id = "node_make_sticky_action",
- *   label = @Translation("Make selected content sticky"),
- *   type = "node"
- * )
  */
+#[Action(
+  id: 'node_make_sticky_action',
+  label: new TranslatableMarkup('Make selected content sticky'),
+  type: 'node'
+)]
 class StickyNode extends FieldUpdateActionBase {
 
   /**
diff --git a/core/modules/node/src/Plugin/Action/UnpublishByKeywordNode.php b/core/modules/node/src/Plugin/Action/UnpublishByKeywordNode.php
index dcca814629b7a8aea181447d47a15b557e7f7218..ffe0e5e255b49a3592ce473731310ad71515de92 100644
--- a/core/modules/node/src/Plugin/Action/UnpublishByKeywordNode.php
+++ b/core/modules/node/src/Plugin/Action/UnpublishByKeywordNode.php
@@ -4,18 +4,19 @@
 
 use Drupal\Component\Utility\Tags;
 use Drupal\Core\Action\ConfigurableActionBase;
+use Drupal\Core\Action\Attribute\Action;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Unpublishes a node containing certain keywords.
- *
- * @Action(
- *   id = "node_unpublish_by_keyword_action",
- *   label = @Translation("Unpublish content containing keyword(s)"),
- *   type = "node"
- * )
  */
+#[Action(
+  id: 'node_unpublish_by_keyword_action',
+  label: new TranslatableMarkup('Unpublish content containing keyword(s)'),
+  type: 'node'
+)]
 class UnpublishByKeywordNode extends ConfigurableActionBase {
 
   /**
diff --git a/core/modules/node/src/Plugin/Action/UnstickyNode.php b/core/modules/node/src/Plugin/Action/UnstickyNode.php
index c074fe9e5a3df8e59abc67d232da3073d3b39e35..0a85dfc772011dee8a9d90df94c0f3686d70f802 100644
--- a/core/modules/node/src/Plugin/Action/UnstickyNode.php
+++ b/core/modules/node/src/Plugin/Action/UnstickyNode.php
@@ -2,18 +2,19 @@
 
 namespace Drupal\node\Plugin\Action;
 
+use Drupal\Core\Action\Attribute\Action;
 use Drupal\Core\Field\FieldUpdateActionBase;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\node\NodeInterface;
 
 /**
  * Makes a node not sticky.
- *
- * @Action(
- *   id = "node_make_unsticky_action",
- *   label = @Translation("Make selected content not sticky"),
- *   type = "node"
- * )
  */
+#[Action(
+  id: 'node_make_unsticky_action',
+  label: new TranslatableMarkup('Make selected content not sticky'),
+  type: 'node'
+)]
 class UnstickyNode extends FieldUpdateActionBase {
 
   /**
diff --git a/core/modules/node/src/Plugin/Block/SyndicateBlock.php b/core/modules/node/src/Plugin/Block/SyndicateBlock.php
index 90af1d54b46787b1049df9496c376607b73bca18..8dea77dc11b6b403d4803b33647a7f4f85a04295 100644
--- a/core/modules/node/src/Plugin/Block/SyndicateBlock.php
+++ b/core/modules/node/src/Plugin/Block/SyndicateBlock.php
@@ -3,8 +3,10 @@
 namespace Drupal\node\Plugin\Block;
 
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Session\AccountInterface;
@@ -12,13 +14,12 @@
 
 /**
  * Provides a 'Syndicate' block that links to the site's RSS feed.
- *
- * @Block(
- *   id = "node_syndicate_block",
- *   admin_label = @Translation("Syndicate"),
- *   category = @Translation("System")
- * )
  */
+#[Block(
+  id: "node_syndicate_block",
+  admin_label: new TranslatableMarkup("Syndicate"),
+  category: new TranslatableMarkup("System")
+)]
 class SyndicateBlock extends BlockBase implements ContainerFactoryPluginInterface {
 
 
diff --git a/core/modules/node/tests/modules/node_block_test/src/Plugin/Block/NodeContextTestBlock.php b/core/modules/node/tests/modules/node_block_test/src/Plugin/Block/NodeContextTestBlock.php
index 01d157d9e90b944739cf44f1bd1b7ea74759ff3e..1c303f8d0ab82f2e6ef1ee9e133d68db2a74ddd3 100644
--- a/core/modules/node/tests/modules/node_block_test/src/Plugin/Block/NodeContextTestBlock.php
+++ b/core/modules/node/tests/modules/node_block_test/src/Plugin/Block/NodeContextTestBlock.php
@@ -2,19 +2,21 @@
 
 namespace Drupal\node_block_test\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
+use Drupal\Core\Plugin\Context\EntityContextDefinition;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Provides a 'Node Context Test' block.
- *
- * @Block(
- *   id = "node_block_test_context",
- *   label = @Translation("Node Context Test"),
- *   context_definitions = {
- *     "node" = @ContextDefinition("entity:node", label = @Translation("Node"))
- *   }
- * )
  */
+#[Block(
+  id: "node_block_test_context",
+  admin_label: new TranslatableMarkup("Node Context Test"),
+  context_definitions: [
+    'node' => new EntityContextDefinition('entity:node', new TranslatableMarkup("Node")),
+  ]
+)]
 class NodeContextTestBlock extends BlockBase {
 
   /**
diff --git a/core/modules/search/src/Plugin/Block/SearchBlock.php b/core/modules/search/src/Plugin/Block/SearchBlock.php
index d1a1f249fd22f7d9333f4bf41ba8c8aef980fa48..5afe788baaa410f6f035b797154aec4c85af9755 100644
--- a/core/modules/search/src/Plugin/Block/SearchBlock.php
+++ b/core/modules/search/src/Plugin/Block/SearchBlock.php
@@ -3,24 +3,25 @@
 namespace Drupal\search\Plugin\Block;
 
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Form\FormBuilderInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\search\Form\SearchBlockForm;
 use Drupal\search\SearchPageRepositoryInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Provides a 'Search form' block.
- *
- * @Block(
- *   id = "search_form_block",
- *   admin_label = @Translation("Search form"),
- *   category = @Translation("Forms")
- * )
  */
+#[Block(
+  id: "search_form_block",
+  admin_label: new TranslatableMarkup("Search form"),
+  category: new TranslatableMarkup("Forms"),
+)]
 class SearchBlock extends BlockBase implements ContainerFactoryPluginInterface {
 
   /**
diff --git a/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/SettingsTrayFormAnnotationIsClassBlock.php b/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/SettingsTrayFormAnnotationIsClassBlock.php
index 699a09a501b04415823966882f2c4b0a5bb01e4f..78ab6957a028ed5c7f213b51e8626babd39449f8 100644
--- a/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/SettingsTrayFormAnnotationIsClassBlock.php
+++ b/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/SettingsTrayFormAnnotationIsClassBlock.php
@@ -2,19 +2,21 @@
 
 namespace Drupal\settings_tray_test\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\settings_tray_test\Form\SettingsTrayFormAnnotationIsClassBlockForm;
 
 /**
  * Block that explicitly provides a "settings_tray" form class.
- *
- * @Block(
- *   id = "settings_tray_test_class",
- *   admin_label = "Settings Tray test block: forms[settings_tray]=class",
- *   forms = {
- *     "settings_tray" = "\Drupal\settings_tray_test\Form\SettingsTrayFormAnnotationIsClassBlockForm",
- *   },
- * )
  */
+#[Block(
+  id: "settings_tray_test_class",
+  admin_label: new TranslatableMarkup("Settings Tray test block: forms[settings_tray]=class"),
+  forms: [
+    'settings_tray' => SettingsTrayFormAnnotationIsClassBlockForm::class,
+  ]
+)]
 class SettingsTrayFormAnnotationIsClassBlock extends BlockBase {
 
   /**
diff --git a/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/SettingsTrayFormAnnotationIsFalseBlock.php b/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/SettingsTrayFormAnnotationIsFalseBlock.php
index 47bb6113bd369a0f0e4447045f482bd8dbf4e3b2..65661416c327f5a75dd090abf7279e25bebcdd36 100644
--- a/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/SettingsTrayFormAnnotationIsFalseBlock.php
+++ b/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/SettingsTrayFormAnnotationIsFalseBlock.php
@@ -2,19 +2,20 @@
 
 namespace Drupal\settings_tray_test\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Block that explicitly provides no "settings_tray" form, thus opting out.
- *
- * @Block(
- *   id = "settings_tray_test_false",
- *   admin_label = "Settings Tray test block: forms[settings_tray]=FALSE",
- *   forms = {
- *     "settings_tray" = FALSE,
- *   },
- * )
  */
+#[Block(
+  id: "settings_tray_test_false",
+  admin_label: new TranslatableMarkup("Settings Tray test block: forms[settings_tray]=FALSE"),
+  forms: [
+    'settings_tray' => FALSE,
+  ]
+)]
 class SettingsTrayFormAnnotationIsFalseBlock extends BlockBase {
 
   /**
diff --git a/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/SettingsTrayFormAnnotationNoneBlock.php b/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/SettingsTrayFormAnnotationNoneBlock.php
index 5b6eb923683065d68fd7712028afe0ba71115d17..1302646bf0bb6521b2ed14f9e41e864cf5600639 100644
--- a/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/SettingsTrayFormAnnotationNoneBlock.php
+++ b/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/SettingsTrayFormAnnotationNoneBlock.php
@@ -2,16 +2,17 @@
 
 namespace Drupal\settings_tray_test\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Block that does nothing explicit for Settings Tray.
- *
- * @Block(
- *   id = "settings_tray_test_none",
- *   admin_label = "Settings Tray test block: forms[settings_tray] is not specified",
- * )
  */
+#[Block(
+  id: "settings_tray_test_none",
+  admin_label: new TranslatableMarkup("Settings Tray test block: forms[settings_tray] is not specified")
+)]
 class SettingsTrayFormAnnotationNoneBlock extends BlockBase {
 
   /**
diff --git a/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/ValidationErrorBlock.php b/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/ValidationErrorBlock.php
index 280e8c7f64f9eac6dfb367ab6f27aad7d90a132c..fd5f1d1533b2f88b03e34f5713f954c35c64b299 100644
--- a/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/ValidationErrorBlock.php
+++ b/core/modules/settings_tray/tests/modules/settings_tray_test/src/Plugin/Block/ValidationErrorBlock.php
@@ -2,17 +2,18 @@
 
 namespace Drupal\settings_tray_test\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Provides a 'Block with validation error' test block.
- *
- * @Block(
- *   id = "settings_tray_test_validation",
- *   admin_label = @Translation("Block with validation error")
- * )
  */
+#[Block(
+  id: "settings_tray_test_validation",
+  admin_label: new TranslatableMarkup("Block with validation error"),
+)]
 class ValidationErrorBlock extends BlockBase {
 
   /**
diff --git a/core/modules/shortcut/src/Plugin/Block/ShortcutsBlock.php b/core/modules/shortcut/src/Plugin/Block/ShortcutsBlock.php
index ab2d57e414db0f343298bc94d5b90c2515f3259b..f11f836b43694124e13104682d8a6c2ae4595eae 100644
--- a/core/modules/shortcut/src/Plugin/Block/ShortcutsBlock.php
+++ b/core/modules/shortcut/src/Plugin/Block/ShortcutsBlock.php
@@ -3,18 +3,19 @@
 namespace Drupal\shortcut\Plugin\Block;
 
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Provides a 'Shortcut' block.
- *
- * @Block(
- *   id = "shortcuts",
- *   admin_label = @Translation("Shortcuts"),
- *   category = @Translation("Menus")
- * )
  */
+#[Block(
+  id: "shortcuts",
+  admin_label: new TranslatableMarkup("Shortcuts"),
+  category: new TranslatableMarkup("Menus")
+)]
 class ShortcutsBlock extends BlockBase {
 
   /**
diff --git a/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
index 072c72736f404476d794b7f1c055a90fa5f6df5a..82eeb68552e2fcfdec998dabf8e6163fe9ac60be 100644
--- a/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
+++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
@@ -3,24 +3,25 @@
 namespace Drupal\statistics\Plugin\Block;
 
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Entity\EntityRepositoryInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\statistics\StatisticsStorageInterface;
 
 /**
  * Provides a 'Popular content' block.
- *
- * @Block(
- *   id = "statistics_popular_block",
- *   admin_label = @Translation("Popular content")
- * )
  */
+#[Block(
+  id: "statistics_popular_block",
+  admin_label: new TranslatableMarkup("Popular content"),
+)]
 class StatisticsPopularBlock extends BlockBase implements ContainerFactoryPluginInterface {
 
   /**
diff --git a/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php b/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php
index 91eaf97dc28bd69afa58dc4d4132cd7a5f07a058..560afe5f57d4193ebf82ffe73b12ed2b5e17f618 100644
--- a/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php
+++ b/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php
@@ -2,25 +2,25 @@
 
 namespace Drupal\system\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\Url;
+use Drupal\system\Form\SystemBrandingOffCanvasForm;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Provides a block to display 'Site branding' elements.
- *
- * @Block(
- *   id = "system_branding_block",
- *   admin_label = @Translation("Site branding"),
- *   forms = {
- *     "settings_tray" = "Drupal\system\Form\SystemBrandingOffCanvasForm",
- *   },
- * )
  */
+#[Block(
+  id: "system_branding_block",
+  admin_label: new TranslatableMarkup("Site branding"),
+  forms: ['settings_tray' => SystemBrandingOffCanvasForm::class]
+)]
 class SystemBrandingBlock extends BlockBase implements ContainerFactoryPluginInterface {
 
   /**
diff --git a/core/modules/system/src/Plugin/Block/SystemBreadcrumbBlock.php b/core/modules/system/src/Plugin/Block/SystemBreadcrumbBlock.php
index a08738636c051f70869685b679e41526401746ee..f57d4eb643e8b2d2631a4f6f643addc53e1f9569 100644
--- a/core/modules/system/src/Plugin/Block/SystemBreadcrumbBlock.php
+++ b/core/modules/system/src/Plugin/Block/SystemBreadcrumbBlock.php
@@ -2,20 +2,21 @@
 
 namespace Drupal\system\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Provides a block to display the breadcrumbs.
- *
- * @Block(
- *   id = "system_breadcrumb_block",
- *   admin_label = @Translation("Breadcrumbs")
- * )
  */
+#[Block(
+  id: "system_breadcrumb_block",
+  admin_label: new TranslatableMarkup("Breadcrumbs")
+)]
 class SystemBreadcrumbBlock extends BlockBase implements ContainerFactoryPluginInterface {
 
   /**
diff --git a/core/modules/system/src/Plugin/Block/SystemMainBlock.php b/core/modules/system/src/Plugin/Block/SystemMainBlock.php
index 92f443081284016ae51797aadbbe9d76bafc3e12..beb6b5982353d715c605ee3a31b51fc2e269d727 100644
--- a/core/modules/system/src/Plugin/Block/SystemMainBlock.php
+++ b/core/modules/system/src/Plugin/Block/SystemMainBlock.php
@@ -2,20 +2,21 @@
 
 namespace Drupal\system\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Block\MainContentBlockPluginInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Provides a 'Main page content' block.
- *
- * @Block(
- *   id = "system_main_block",
- *   admin_label = @Translation("Main page content"),
- *   forms = {
- *     "settings_tray" = FALSE,
- *   },
- * )
  */
+#[Block(
+  id: "system_main_block",
+  admin_label: new TranslatableMarkup("Main page content"),
+  forms: [
+    'settings_tray' => FALSE,
+  ]
+)]
 class SystemMainBlock extends BlockBase implements MainContentBlockPluginInterface {
 
   /**
diff --git a/core/modules/system/src/Plugin/Block/SystemMenuBlock.php b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php
index 463b6800b8ccf261137949e9968840df87e313d3..8918d4b2afaee87f0dabe737cd3ab1cf0973a795 100644
--- a/core/modules/system/src/Plugin/Block/SystemMenuBlock.php
+++ b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\system\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Form\FormStateInterface;
@@ -9,21 +10,23 @@
 use Drupal\Core\Menu\MenuLinkTreeInterface;
 use Drupal\Core\Menu\MenuTreeParameters;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\system\Form\SystemMenuOffCanvasForm;
+use Drupal\system\Plugin\Derivative\SystemMenuBlock as SystemMenuBlockDeriver;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Provides a generic Menu block.
- *
- * @Block(
- *   id = "system_menu_block",
- *   admin_label = @Translation("Menu"),
- *   category = @Translation("Menus"),
- *   deriver = "Drupal\system\Plugin\Derivative\SystemMenuBlock",
- *   forms = {
- *     "settings_tray" = "\Drupal\system\Form\SystemMenuOffCanvasForm",
- *   },
- * )
  */
+#[Block(
+  id: "system_menu_block",
+  admin_label: new TranslatableMarkup("Menu"),
+  category: new TranslatableMarkup("Menus"),
+  deriver: SystemMenuBlockDeriver::class,
+  forms: [
+    'settings_tray' => SystemMenuOffCanvasForm::class,
+  ]
+)]
 class SystemMenuBlock extends BlockBase implements ContainerFactoryPluginInterface {
 
   /**
diff --git a/core/modules/system/src/Plugin/Block/SystemMessagesBlock.php b/core/modules/system/src/Plugin/Block/SystemMessagesBlock.php
index f737b8fd5db6ce603afe0aae3862b7e51094b35c..d6f1a9f22758baaf01177e671cd1788d6ea72810 100644
--- a/core/modules/system/src/Plugin/Block/SystemMessagesBlock.php
+++ b/core/modules/system/src/Plugin/Block/SystemMessagesBlock.php
@@ -2,20 +2,21 @@
 
 namespace Drupal\system\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Block\MessagesBlockPluginInterface;
 use Drupal\Core\Cache\Cache;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Provides a block to display the messages.
  *
  * @see @see \Drupal\Core\Messenger\MessengerInterface
- *
- * @Block(
- *   id = "system_messages_block",
- *   admin_label = @Translation("Messages")
- * )
  */
+#[Block(
+  id: "system_messages_block",
+  admin_label: new TranslatableMarkup("Messages")
+)]
 class SystemMessagesBlock extends BlockBase implements MessagesBlockPluginInterface {
 
   /**
diff --git a/core/modules/system/src/Plugin/Block/SystemPoweredByBlock.php b/core/modules/system/src/Plugin/Block/SystemPoweredByBlock.php
index c1e0ba8b8c172e35346d8554389ccff9f62963af..513bb11f7f9a870400f9560f7e8f669c0590b34c 100644
--- a/core/modules/system/src/Plugin/Block/SystemPoweredByBlock.php
+++ b/core/modules/system/src/Plugin/Block/SystemPoweredByBlock.php
@@ -3,15 +3,16 @@
 namespace Drupal\system\Plugin\Block;
 
 use Drupal\Core\Block\BlockBase;
+use Drupal\Core\Block\Attribute\Block;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Provides a 'Powered by Drupal' block.
- *
- * @Block(
- *   id = "system_powered_by_block",
- *   admin_label = @Translation("Powered by Drupal")
- * )
  */
+#[Block(
+  id: "system_powered_by_block",
+  admin_label: new TranslatableMarkup("Powered by Drupal")
+)]
 class SystemPoweredByBlock extends BlockBase {
 
   /**
diff --git a/core/modules/system/tests/modules/action_test/src/Plugin/Action/NoType.php b/core/modules/system/tests/modules/action_test/src/Plugin/Action/NoType.php
index 1fc39932bcb00d96ce3d7aee01f2103e8413b4e6..57bcec1ef3be55a23ce731f22b3a9dd544d8452f 100644
--- a/core/modules/system/tests/modules/action_test/src/Plugin/Action/NoType.php
+++ b/core/modules/system/tests/modules/action_test/src/Plugin/Action/NoType.php
@@ -4,16 +4,17 @@
 
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Action\ActionBase;
+use Drupal\Core\Action\Attribute\Action;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Provides an operation with no type specified.
- *
- * @Action(
- *   id = "action_test_no_type",
- *   label = @Translation("An operation with no type specified")
- * )
  */
+#[Action(
+  id: 'action_test_no_type',
+  label: new TranslatableMarkup('An operation with no type specified'),
+)]
 class NoType extends ActionBase {
 
   /**
diff --git a/core/modules/system/tests/modules/action_test/src/Plugin/Action/SaveEntity.php b/core/modules/system/tests/modules/action_test/src/Plugin/Action/SaveEntity.php
index b20f424ce9bca3646d91423c7bdeaae56c0433e5..2bc7e83c4078402770bd976a7e44f1ff75931c62 100644
--- a/core/modules/system/tests/modules/action_test/src/Plugin/Action/SaveEntity.php
+++ b/core/modules/system/tests/modules/action_test/src/Plugin/Action/SaveEntity.php
@@ -3,17 +3,18 @@
 namespace Drupal\action_test\Plugin\Action;
 
 use Drupal\Core\Action\ActionBase;
+use Drupal\Core\Action\Attribute\Action;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Provides an operation to save user entities.
- *
- * @Action(
- *   id = "action_test_save_entity",
- *   label = @Translation("Saves entities"),
- *   type = "user"
- * )
  */
+#[Action(
+  id: 'action_test_save_entity',
+  label: new TranslatableMarkup('Saves entities'),
+  type: 'user'
+)]
 class SaveEntity extends ActionBase {
 
   /**
diff --git a/core/modules/system/tests/modules/ajax_forms_test/src/Plugin/Block/AjaxFormBlock.php b/core/modules/system/tests/modules/ajax_forms_test/src/Plugin/Block/AjaxFormBlock.php
index 65e1372d999f0fc9d47131a1841a6519cd81fdab..8e0d1c112b3958715fd9b01fcf78a1dd2950bd5b 100644
--- a/core/modules/system/tests/modules/ajax_forms_test/src/Plugin/Block/AjaxFormBlock.php
+++ b/core/modules/system/tests/modules/ajax_forms_test/src/Plugin/Block/AjaxFormBlock.php
@@ -2,23 +2,24 @@
 
 namespace Drupal\ajax_forms_test\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Form\FormBuilderInterface;
 use Drupal\Core\Form\FormInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Messenger\MessengerInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Provides an AJAX form block.
- *
- * @Block(
- *   id = "ajax_forms_test_block",
- *   admin_label = @Translation("AJAX test form"),
- *   category = @Translation("Forms")
- * )
  */
+#[Block(
+  id: "ajax_forms_test_block",
+  admin_label: new TranslatableMarkup("AJAX test form"),
+  category: new TranslatableMarkup("Forms")
+)]
 class AjaxFormBlock extends BlockBase implements FormInterface, ContainerFactoryPluginInterface {
 
   /**
diff --git a/core/modules/system/tests/modules/form_test/src/Plugin/Block/RedirectFormBlock.php b/core/modules/system/tests/modules/form_test/src/Plugin/Block/RedirectFormBlock.php
index 027be19fe3a0e596c1c1f30efabf8a791903de24..e8ee69fe5419ad13d3bccff674718ee3a7b0c93b 100644
--- a/core/modules/system/tests/modules/form_test/src/Plugin/Block/RedirectFormBlock.php
+++ b/core/modules/system/tests/modules/form_test/src/Plugin/Block/RedirectFormBlock.php
@@ -2,22 +2,23 @@
 
 namespace Drupal\form_test\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Form\FormBuilderInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Provides a block containing a simple redirect form.
  *
  * @see \Drupal\form_test\Form\RedirectBlockForm
- *
- * @Block(
- *   id = "redirect_form_block",
- *   admin_label = @Translation("Redirecting form"),
- *   category = @Translation("Forms")
- * )
  */
+#[Block(
+  id: "redirect_form_block",
+  admin_label: new TranslatableMarkup("Redirecting form"),
+  category: new TranslatableMarkup("Forms"),
+)]
 class RedirectFormBlock extends BlockBase implements ContainerFactoryPluginInterface {
 
   /**
diff --git a/core/modules/system/tests/modules/plugin_test/src/Plugin/Attribute/PluginExample.php b/core/modules/system/tests/modules/plugin_test/src/Plugin/Attribute/PluginExample.php
new file mode 100644
index 0000000000000000000000000000000000000000..af916bb90dba9cd301ecc333351a4652c9f47134
--- /dev/null
+++ b/core/modules/system/tests/modules/plugin_test/src/Plugin/Attribute/PluginExample.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\plugin_test\Plugin\Attribute;
+
+use Drupal\Component\Plugin\Attribute\Plugin;
+
+/**
+ * Defines a custom PluginExample attribute.
+ */
+#[\Attribute(\Attribute::TARGET_CLASS)]
+class PluginExample extends Plugin {
+
+  /**
+   * Constructs a PluginExample attribute.
+   *
+   * @param string $id
+   *   The plugin ID.
+   * @param string $custom
+   *   Some other sample plugin metadata.
+   */
+  public function __construct(
+    public readonly string $id,
+    public readonly ?string $custom = NULL
+  ) {}
+
+}
diff --git a/core/modules/system/tests/modules/plugin_test/src/Plugin/plugin_test/custom_annotation/Example3.php b/core/modules/system/tests/modules/plugin_test/src/Plugin/plugin_test/custom_annotation/Example3.php
new file mode 100644
index 0000000000000000000000000000000000000000..2394b85c2de76ae00b44ce21bea8e194859cefd8
--- /dev/null
+++ b/core/modules/system/tests/modules/plugin_test/src/Plugin/plugin_test/custom_annotation/Example3.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Drupal\plugin_test\Plugin\plugin_test\custom_annotation;
+
+use Drupal\plugin_test\Plugin\Attribute\PluginExample;
+
+/**
+ * Provides a test plugin with a custom attribute.
+ */
+#[PluginExample(
+  id: "example_3",
+  custom: "George"
+)]
+class Example3 {}
diff --git a/core/modules/system/tests/modules/render_attached_test/src/Plugin/Block/AttachedRenderingBlock.php b/core/modules/system/tests/modules/render_attached_test/src/Plugin/Block/AttachedRenderingBlock.php
index 721d10cafe6a4345f0f1eef2bd18ec729da89af4..c660a4ff7783d3b2447514b4255f980fba484078 100644
--- a/core/modules/system/tests/modules/render_attached_test/src/Plugin/Block/AttachedRenderingBlock.php
+++ b/core/modules/system/tests/modules/render_attached_test/src/Plugin/Block/AttachedRenderingBlock.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\render_attached_test\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\render_attached_test\Controller\RenderAttachedTestController;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Cache\Cache;
@@ -10,13 +12,12 @@
 /**
  * A block we can use to test caching of #attached headers.
  *
- * @Block(
- *   id = "attached_rendering_block",
- *   admin_label = @Translation("AttachedRenderingBlock")
- * )
- *
  * @see \Drupal\system\Tests\Render\HtmlResponseAttachmentsTest
  */
+#[Block(
+  id: "attached_rendering_block",
+  admin_label: new TranslatableMarkup("AttachedRenderingBlock")
+)]
 class AttachedRenderingBlock extends BlockBase {
 
   /**
diff --git a/core/modules/user/src/Plugin/Action/AddRoleUser.php b/core/modules/user/src/Plugin/Action/AddRoleUser.php
index 6980ff9b44ce69805b90449e79f3ff237f25475c..f8be14d9225836fd95d44d01daf51ec37e528918 100644
--- a/core/modules/user/src/Plugin/Action/AddRoleUser.php
+++ b/core/modules/user/src/Plugin/Action/AddRoleUser.php
@@ -2,15 +2,17 @@
 
 namespace Drupal\user\Plugin\Action;
 
+use Drupal\Core\Action\Attribute\Action;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+
 /**
  * Adds a role to a user.
- *
- * @Action(
- *   id = "user_add_role_action",
- *   label = @Translation("Add a role to the selected users"),
- *   type = "user"
- * )
  */
+#[Action(
+  id: 'user_add_role_action',
+  label: new TranslatableMarkup('Add a role to the selected users'),
+  type: 'user'
+)]
 class AddRoleUser extends ChangeUserRoleBase {
 
   /**
diff --git a/core/modules/user/src/Plugin/Action/BlockUser.php b/core/modules/user/src/Plugin/Action/BlockUser.php
index 6a875f78d95a79b0214a8a220e229c6ee04f10c7..560c97e33bd91e1d8fb178861ac0a10e1fd6d4ec 100644
--- a/core/modules/user/src/Plugin/Action/BlockUser.php
+++ b/core/modules/user/src/Plugin/Action/BlockUser.php
@@ -3,17 +3,18 @@
 namespace Drupal\user\Plugin\Action;
 
 use Drupal\Core\Action\ActionBase;
+use Drupal\Core\Action\Attribute\Action;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Blocks a user.
- *
- * @Action(
- *   id = "user_block_user_action",
- *   label = @Translation("Block the selected users"),
- *   type = "user"
- * )
  */
+#[Action(
+  id: 'user_block_user_action',
+  label: new TranslatableMarkup('Block the selected users'),
+  type: 'user'
+)]
 class BlockUser extends ActionBase {
 
   /**
diff --git a/core/modules/user/src/Plugin/Action/CancelUser.php b/core/modules/user/src/Plugin/Action/CancelUser.php
index 0fa74c021bcf1e3a70b5cc0828b914336f3a09ce..68ad56ad9c6fcc7794716e9e505df7d3a2ab7f09 100644
--- a/core/modules/user/src/Plugin/Action/CancelUser.php
+++ b/core/modules/user/src/Plugin/Action/CancelUser.php
@@ -3,21 +3,22 @@
 namespace Drupal\user\Plugin\Action;
 
 use Drupal\Core\Action\ActionBase;
+use Drupal\Core\Action\Attribute\Action;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\TempStore\PrivateTempStoreFactory;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Cancels a user account.
- *
- * @Action(
- *   id = "user_cancel_user_action",
- *   label = @Translation("Cancel the selected user accounts"),
- *   type = "user",
- *   confirm_form_route_name = "user.multiple_cancel_confirm"
- * )
  */
+#[Action(
+  id: 'user_cancel_user_action',
+  label: new TranslatableMarkup('Cancel the selected user accounts'),
+  type: 'user',
+  confirm_form_route_name: 'user.multiple_cancel_confirm'
+)]
 class CancelUser extends ActionBase implements ContainerFactoryPluginInterface {
 
   /**
diff --git a/core/modules/user/src/Plugin/Action/RemoveRoleUser.php b/core/modules/user/src/Plugin/Action/RemoveRoleUser.php
index 66cf6d0f3637b4db2a3b291a510a65224f2c8028..1c6e1186702c6b086c9e8707acb27d0b0270caef 100644
--- a/core/modules/user/src/Plugin/Action/RemoveRoleUser.php
+++ b/core/modules/user/src/Plugin/Action/RemoveRoleUser.php
@@ -2,15 +2,17 @@
 
 namespace Drupal\user\Plugin\Action;
 
+use Drupal\Core\Action\Attribute\Action;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+
 /**
  * Removes a role from a user.
- *
- * @Action(
- *   id = "user_remove_role_action",
- *   label = @Translation("Remove a role from the selected users"),
- *   type = "user"
- * )
  */
+#[Action(
+  id: 'user_remove_role_action',
+  label: new TranslatableMarkup('Remove a role from the selected users'),
+  type: 'user'
+)]
 class RemoveRoleUser extends ChangeUserRoleBase {
 
   /**
diff --git a/core/modules/user/src/Plugin/Action/UnblockUser.php b/core/modules/user/src/Plugin/Action/UnblockUser.php
index 68c59b40e161084254e89f769a3ed6641bfe4f6e..6df7686731bc5c12b6848ad0e502aab6cb887091 100644
--- a/core/modules/user/src/Plugin/Action/UnblockUser.php
+++ b/core/modules/user/src/Plugin/Action/UnblockUser.php
@@ -3,17 +3,18 @@
 namespace Drupal\user\Plugin\Action;
 
 use Drupal\Core\Action\ActionBase;
+use Drupal\Core\Action\Attribute\Action;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Unblocks a user.
- *
- * @Action(
- *   id = "user_unblock_user_action",
- *   label = @Translation("Unblock the selected users"),
- *   type = "user"
- * )
  */
+#[Action(
+  id: 'user_unblock_user_action',
+  label: new TranslatableMarkup('Unblock the selected users'),
+  type: 'user'
+)]
 class UnblockUser extends ActionBase {
 
   /**
diff --git a/core/modules/user/src/Plugin/Block/UserLoginBlock.php b/core/modules/user/src/Plugin/Block/UserLoginBlock.php
index e35295a59069268248f7917e8aa9b343121cb56b..431d9edd4aab36aee14d9bba7d81eced2728051e 100644
--- a/core/modules/user/src/Plugin/Block/UserLoginBlock.php
+++ b/core/modules/user/src/Plugin/Block/UserLoginBlock.php
@@ -4,10 +4,12 @@
 
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Security\TrustedCallbackInterface;
 use Drupal\Core\Routing\RedirectDestinationTrait;
 use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\Url;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Block\BlockBase;
@@ -16,13 +18,12 @@
 
 /**
  * Provides a 'User login' block.
- *
- * @Block(
- *   id = "user_login_block",
- *   admin_label = @Translation("User login"),
- *   category = @Translation("Forms")
- * )
  */
+#[Block(
+  id: "user_login_block",
+  admin_label: new TranslatableMarkup("User login"),
+  category: new TranslatableMarkup("Forms")
+)]
 class UserLoginBlock extends BlockBase implements ContainerFactoryPluginInterface, TrustedCallbackInterface {
 
   use RedirectDestinationTrait;
diff --git a/core/modules/views/src/Plugin/Block/ViewsBlock.php b/core/modules/views/src/Plugin/Block/ViewsBlock.php
index 34738cddd4d45cba0fda6e1bb2d11fceb284869c..8c4bd7542cf9a673d271872978443e9931f9cb5f 100644
--- a/core/modules/views/src/Plugin/Block/ViewsBlock.php
+++ b/core/modules/views/src/Plugin/Block/ViewsBlock.php
@@ -3,19 +3,21 @@
 namespace Drupal\views\Plugin\Block;
 
 use Drupal\Component\Utility\Xss;
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\views\Element\View;
+use Drupal\views\Plugin\Derivative\ViewsBlock as ViewsBlockDeriver;
 use Drupal\Core\Entity\EntityInterface;
 
 /**
  * Provides a generic Views block.
- *
- * @Block(
- *   id = "views_block",
- *   admin_label = @Translation("Views Block"),
- *   deriver = "Drupal\views\Plugin\Derivative\ViewsBlock"
- * )
  */
+#[Block(
+  id: "views_block",
+  admin_label: new TranslatableMarkup("Views Block"),
+  deriver: ViewsBlockDeriver::class
+)]
 class ViewsBlock extends ViewsBlockBase {
 
   /**
diff --git a/core/modules/views/src/Plugin/Block/ViewsExposedFilterBlock.php b/core/modules/views/src/Plugin/Block/ViewsExposedFilterBlock.php
index f43fda6d498ab6eb3b9892fb6d1ce9d9d34ae0a0..135c4313abad1b7b81554cd3b7592e961f7830c8 100644
--- a/core/modules/views/src/Plugin/Block/ViewsExposedFilterBlock.php
+++ b/core/modules/views/src/Plugin/Block/ViewsExposedFilterBlock.php
@@ -2,18 +2,20 @@
 
 namespace Drupal\views\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Cache\Cache;
 use Drupal\Component\Utility\Xss;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\views\Plugin\Derivative\ViewsExposedFilterBlock as ViewsExposedFilterBlockDeriver;
 
 /**
  * Provides a 'Views Exposed Filter' block.
- *
- * @Block(
- *   id = "views_exposed_filter_block",
- *   admin_label = @Translation("Views Exposed Filter Block"),
- *   deriver = "Drupal\views\Plugin\Derivative\ViewsExposedFilterBlock"
- * )
  */
+#[Block(
+  id: "views_exposed_filter_block",
+  admin_label: new TranslatableMarkup("Views Exposed Filter Block"),
+  deriver: ViewsExposedFilterBlockDeriver::class
+)]
 class ViewsExposedFilterBlock extends ViewsBlockBase {
 
   /**
diff --git a/core/modules/views/tests/modules/user_batch_action_test/src/Plugin/Action/BatchUserAction.php b/core/modules/views/tests/modules/user_batch_action_test/src/Plugin/Action/BatchUserAction.php
index 8c054dead34ef25472d10866722595f144dd298f..9e58ca8abeb501ae5b78b7e0a848dd90b20dd1a8 100644
--- a/core/modules/views/tests/modules/user_batch_action_test/src/Plugin/Action/BatchUserAction.php
+++ b/core/modules/views/tests/modules/user_batch_action_test/src/Plugin/Action/BatchUserAction.php
@@ -3,18 +3,19 @@
 namespace Drupal\user_batch_action_test\Plugin\Action;
 
 use Drupal\Core\Action\ActionBase;
+use Drupal\Core\Action\Attribute\Action;
 use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Provides action that sets batch precessing.
- *
- * @Action(
- *   id = "user_batch_action_test_action",
- *   label = @Translation("Process user in batch"),
- *   type = "user",
- * )
  */
+#[Action(
+  id: 'user_batch_action_test_action',
+  label: new TranslatableMarkup('Process user in batch'),
+  type: 'user'
+)]
 class BatchUserAction extends ActionBase {
 
   /**
diff --git a/core/modules/workspaces/src/Plugin/Block/WorkspaceSwitcherBlock.php b/core/modules/workspaces/src/Plugin/Block/WorkspaceSwitcherBlock.php
index 2f5a2fe743a3957153809df9f9845af75e107ec5..0333524db6aa98c6187e4971a654f7394d1c3900 100644
--- a/core/modules/workspaces/src/Plugin/Block/WorkspaceSwitcherBlock.php
+++ b/core/modules/workspaces/src/Plugin/Block/WorkspaceSwitcherBlock.php
@@ -2,22 +2,23 @@
 
 namespace Drupal\workspaces\Plugin\Block;
 
+use Drupal\Core\Block\Attribute\Block;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Form\FormBuilderInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\workspaces\Form\WorkspaceSwitcherForm;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Provides a 'Workspace switcher' block.
- *
- * @Block(
- *   id = "workspace_switcher",
- *   admin_label = @Translation("Workspace switcher"),
- *   category = @Translation("Workspace"),
- * )
  */
+#[Block(
+  id: "workspace_switcher",
+  admin_label: new TranslatableMarkup("Workspace switcher"),
+  category: new TranslatableMarkup("Workspace")
+)]
 class WorkspaceSwitcherBlock extends BlockBase implements ContainerFactoryPluginInterface {
 
   /**
diff --git a/core/tests/Drupal/KernelTests/Core/Plugin/DefaultPluginManagerTest.php b/core/tests/Drupal/KernelTests/Core/Plugin/DefaultPluginManagerTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..a4566a1a3dc8f3e0e098201b9699d84e17877fcd
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Plugin/DefaultPluginManagerTest.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Plugin;
+
+use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\plugin_test\Plugin\Annotation\PluginExample as AnnotationPluginExample;
+use Drupal\plugin_test\Plugin\Attribute\PluginExample as AttributePluginExample;
+
+/**
+ * Tests the default plugin manager.
+ *
+ * @group Plugin
+ */
+class DefaultPluginManagerTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['plugin_test'];
+
+  /**
+   * Tests annotations and attributes on the default plugin manager.
+   */
+  public function testDefaultPluginManager() {
+    $subdir = 'Plugin/plugin_test/custom_annotation';
+    $base_directory = $this->root . '/core/modules/system/tests/modules/plugin_test/src';
+    $namespaces = new \ArrayObject(['Drupal\plugin_test' => $base_directory]);
+    $module_handler = $this->container->get('module_handler');
+
+    // Annotation only.
+    $manager = new DefaultPluginManager($subdir, $namespaces, $module_handler, NULL, AnnotationPluginExample::class);
+    $definitions = $manager->getDefinitions();
+    $this->assertArrayHasKey('example_1', $definitions);
+    $this->assertArrayHasKey('example_2', $definitions);
+    $this->assertArrayNotHasKey('example_3', $definitions);
+
+    // Annotations and attributes together.
+    $manager = new DefaultPluginManager($subdir, $namespaces, $module_handler, NULL, AttributePluginExample::class, AnnotationPluginExample::class);
+    $definitions = $manager->getDefinitions();
+    $this->assertArrayHasKey('example_1', $definitions);
+    $this->assertArrayHasKey('example_2', $definitions);
+    $this->assertArrayHasKey('example_3', $definitions);
+
+    // Attributes only.
+    $manager = new DefaultPluginManager($subdir, $namespaces, $module_handler, NULL, AttributePluginExample::class);
+    $definitions = $manager->getDefinitions();
+    $this->assertArrayNotHasKey('example_1', $definitions);
+    $this->assertArrayNotHasKey('example_2', $definitions);
+    $this->assertArrayHasKey('example_3', $definitions);
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeBaseTest.php b/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeBaseTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..1f9a5657a66ae79a4efaad01c059a245cb3aa5a8
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeBaseTest.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Drupal\Tests\Component\Plugin\Attribute;
+
+use Drupal\Component\Plugin\Attribute\AttributeBase;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Component\Plugin\Attribute\AttributeBase
+ * @group Attribute
+ */
+class AttributeBaseTest extends TestCase {
+
+  /**
+   * @covers ::getProvider
+   * @covers ::setProvider
+   */
+  public function testSetProvider() {
+    $plugin = new AttributeBaseStub(id: '1');
+    $plugin->setProvider('example');
+    $this->assertEquals('example', $plugin->getProvider());
+  }
+
+  /**
+   * @covers ::getId
+   */
+  public function testGetId() {
+    $plugin = new AttributeBaseStub(id: 'example');
+    $this->assertEquals('example', $plugin->getId());
+  }
+
+  /**
+   * @covers ::getClass
+   * @covers ::setClass
+   */
+  public function testSetClass() {
+    $plugin = new AttributeBaseStub(id: '1');
+    $plugin->setClass('example');
+    $this->assertEquals('example', $plugin->getClass());
+  }
+
+}
+/**
+ * {@inheritdoc}
+ */
+class AttributeBaseStub extends AttributeBase {
+
+}
diff --git a/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryCachedTest.php b/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryCachedTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..d68a724b566f4ec516678dfc61c13426e8332e39
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryCachedTest.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Drupal\Tests\Component\Plugin\Attribute;
+
+use Drupal\Component\Plugin\Discovery\AttributeClassDiscovery;
+use Drupal\Component\FileCache\FileCacheFactory;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Component\Plugin\Discovery\AttributeClassDiscovery
+ * @group Attribute
+ * @runTestsInSeparateProcesses
+ */
+class AttributeClassDiscoveryCachedTest extends TestCase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    // Ensure FileCacheFactory::DISABLE_CACHE is *not* set, since we're testing
+    // integration with the file cache.
+    FileCacheFactory::setConfiguration([]);
+    // Ensure that FileCacheFactory has a prefix.
+    FileCacheFactory::setPrefix('prefix');
+
+    // Normally the attribute classes would be autoloaded.
+    include_once __DIR__ . '/Fixtures/CustomPlugin.php';
+    include_once __DIR__ . '/Fixtures/Plugins/PluginNamespace/AttributeDiscoveryTest1.php';
+  }
+
+  /**
+   * Tests that getDefinitions() retrieves the file cache correctly.
+   *
+   * @covers ::getDefinitions
+   */
+  public function testGetDefinitions() {
+    // Path to the classes which we'll discover and parse annotation.
+    $discovery_path = __DIR__ . '/Fixtures/Plugins';
+    // File path that should be discovered within that directory.
+    $file_path = $discovery_path . '/PluginNamespace/AttributeDiscoveryTest1.php';
+
+    $discovery = new AttributeClassDiscovery(['com\example' => [$discovery_path]]);
+    $this->assertEquals([
+      'discovery_test_1' => [
+        'id' => 'discovery_test_1',
+        'class' => 'com\example\PluginNamespace\AttributeDiscoveryTest1',
+      ],
+    ], $discovery->getDefinitions());
+
+    // Gain access to the file cache so we can change it.
+    $ref_file_cache = new \ReflectionProperty($discovery, 'fileCache');
+    $ref_file_cache->setAccessible(TRUE);
+    /** @var \Drupal\Component\FileCache\FileCacheInterface $file_cache */
+    $file_cache = $ref_file_cache->getValue($discovery);
+    // The file cache is keyed by the file path, and we'll add some known
+    // content to test against.
+    $file_cache->set($file_path, [
+      'id' => 'wrong_id',
+      'content' => serialize(['an' => 'array']),
+    ]);
+
+    // Now perform the same query and check for the cached results.
+    $this->assertEquals([
+      'wrong_id' => [
+        'an' => 'array',
+      ],
+    ], $discovery->getDefinitions());
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryTest.php b/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..965254c719e3410af2651791b581667324e65020
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryTest.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Drupal\Tests\Component\Plugin\Attribute;
+
+use Drupal\Component\Plugin\Discovery\AttributeClassDiscovery;
+use Drupal\Component\FileCache\FileCacheFactory;
+use PHPUnit\Framework\TestCase;
+use com\example\PluginNamespace\CustomPlugin;
+use com\example\PluginNamespace\CustomPlugin2;
+
+/**
+ * @coversDefaultClass \Drupal\Component\Plugin\Discovery\AttributeClassDiscovery
+ * @group Attribute
+ * @runTestsInSeparateProcesses
+ */
+class AttributeClassDiscoveryTest extends TestCase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    // Ensure the file cache is disabled.
+    FileCacheFactory::setConfiguration([FileCacheFactory::DISABLE_CACHE => TRUE]);
+    // Ensure that FileCacheFactory has a prefix.
+    FileCacheFactory::setPrefix('prefix');
+
+    // Normally the attribute classes would be autoloaded.
+    include_once __DIR__ . '/Fixtures/CustomPlugin.php';
+    include_once __DIR__ . '/Fixtures/Plugins/PluginNamespace/AttributeDiscoveryTest1.php';
+  }
+
+  /**
+   * @covers ::__construct
+   * @covers ::getPluginNamespaces
+   */
+  public function testGetPluginNamespaces() {
+    // Path to the classes which we'll discover and parse annotation.
+    $discovery = new AttributeClassDiscovery(['com/example' => [__DIR__]]);
+
+    $reflection = new \ReflectionMethod($discovery, 'getPluginNamespaces');
+    $reflection->setAccessible(TRUE);
+
+    $result = $reflection->invoke($discovery);
+    $this->assertEquals(['com/example' => [__DIR__]], $result);
+  }
+
+  /**
+   * @covers ::getDefinitions
+   * @covers ::prepareAttributeDefinition
+   */
+  public function testGetDefinitions() {
+    $discovery = new AttributeClassDiscovery(['com\example' => [__DIR__ . '/Fixtures/Plugins']]);
+    $this->assertEquals([
+      'discovery_test_1' => [
+        'id' => 'discovery_test_1',
+        'class' => 'com\example\PluginNamespace\AttributeDiscoveryTest1',
+      ],
+    ], $discovery->getDefinitions());
+
+    $custom_annotation_discovery = new AttributeClassDiscovery(['com\example' => [__DIR__ . '/Fixtures/Plugins']], CustomPlugin::class);
+    $this->assertEquals([
+      'discovery_test_1' => [
+        'id' => 'discovery_test_1',
+        'class' => 'com\example\PluginNamespace\AttributeDiscoveryTest1',
+        'title' => 'Discovery test plugin',
+      ],
+    ], $custom_annotation_discovery->getDefinitions());
+
+    $empty_discovery = new AttributeClassDiscovery(['com\example' => [__DIR__ . '/Fixtures/Plugins']], CustomPlugin2::class);
+    $this->assertEquals([], $empty_discovery->getDefinitions());
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/Plugin/Attribute/Fixtures/CustomPlugin.php b/core/tests/Drupal/Tests/Component/Plugin/Attribute/Fixtures/CustomPlugin.php
new file mode 100644
index 0000000000000000000000000000000000000000..9100fb800d9944934121c8693459a39b86fff50d
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Plugin/Attribute/Fixtures/CustomPlugin.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace com\example\PluginNamespace;
+
+use Drupal\Component\Plugin\Attribute\Plugin;
+
+/**
+ * Custom plugin attribute.
+ */
+#[\Attribute(\Attribute::TARGET_CLASS)]
+class CustomPlugin extends Plugin {
+
+  /**
+   * Constructs a CustomPlugin attribute object.
+   *
+   * @param string $id
+   *   The attribute class ID.
+   * @param string $title
+   *   The title.
+   */
+  public function __construct(
+    public readonly string $id,
+    public readonly string $title
+  ) {}
+
+}
+
+/**
+ * Custom plugin attribute.
+ */
+#[\Attribute(\Attribute::TARGET_CLASS)]
+class CustomPlugin2 extends Plugin {}
diff --git a/core/tests/Drupal/Tests/Component/Plugin/Attribute/Fixtures/Plugins/PluginNamespace/AttributeDiscoveryTest1.php b/core/tests/Drupal/Tests/Component/Plugin/Attribute/Fixtures/Plugins/PluginNamespace/AttributeDiscoveryTest1.php
new file mode 100644
index 0000000000000000000000000000000000000000..d550e99303e94e0ed768797ad6a2efcc77c93f84
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Plugin/Attribute/Fixtures/Plugins/PluginNamespace/AttributeDiscoveryTest1.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace com\example\PluginNamespace;
+
+use Drupal\Component\Plugin\Attribute\Plugin;
+
+/**
+ * Provides a custom test plugin.
+ */
+#[Plugin(
+  id: "discovery_test_1",
+)]
+#[CustomPlugin(
+  id: "discovery_test_1",
+  title: "Discovery test plugin"
+)]
+class AttributeDiscoveryTest1 {}
diff --git a/core/tests/Drupal/Tests/Component/Plugin/Attribute/PluginIdTest.php b/core/tests/Drupal/Tests/Component/Plugin/Attribute/PluginIdTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..8c8391228330c4e1515e44b4888d17fdc246d0fc
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Plugin/Attribute/PluginIdTest.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Drupal\Tests\Component\Plugin\Attribute;
+
+use Drupal\Component\Plugin\Attribute\PluginID;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Component\Plugin\Attribute\PluginId
+ * @group Attribute
+ */
+class PluginIdTest extends TestCase {
+
+  /**
+   * @covers ::get
+   */
+  public function testGet() {
+    // Assert plugin starts with only an ID.
+    $plugin = new PluginID(id: 'test');
+    // Plugin's always have a class set by discovery.
+    $plugin->setClass('bar');
+    $this->assertEquals([
+      'id' => 'test',
+      'class' => 'bar',
+      'provider' => NULL,
+    ], $plugin->get());
+
+    // Set values and ensure we can retrieve them.
+    $plugin->setClass('bar2');
+    $plugin->setProvider('baz');
+    $this->assertEquals([
+      'id' => 'test',
+      'class' => 'bar2',
+      'provider' => 'baz',
+    ], $plugin->get());
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/Plugin/Attribute/PluginTest.php b/core/tests/Drupal/Tests/Component/Plugin/Attribute/PluginTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..7fdfaa1eb90327ed6660f19e1e0ddb93cceff76e
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Plugin/Attribute/PluginTest.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Drupal\Tests\Component\Plugin\Attribute;
+
+use Drupal\Component\Plugin\Attribute\Plugin;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Component\Annotation\Plugin
+ * @group Attribute
+ */
+class PluginTest extends TestCase {
+
+  /**
+   * @covers ::__construct
+   * @covers ::get
+   */
+  public function testGet() {
+    $plugin = new PluginStub(id: 'example', deriver: 'test');
+    $plugin->setClass('foo');
+    $this->assertEquals([
+      'id' => 'example',
+      'class' => 'foo',
+      'deriver' => 'test',
+    ], $plugin->get());
+  }
+
+  /**
+   * @covers ::setProvider
+   * @covers ::getProvider
+   */
+  public function testSetProvider() {
+    $plugin = new Plugin(id: 'example');
+    $plugin->setProvider('example');
+    $this->assertEquals('example', $plugin->getProvider());
+  }
+
+  /**
+   * @covers ::getId
+   */
+  public function testGetId() {
+    $plugin = new Plugin(id: 'example');
+    $this->assertEquals('example', $plugin->getId());
+  }
+
+  /**
+   * @covers ::setClass
+   * @covers ::getClass
+   */
+  public function testSetClass() {
+    $plugin = new Plugin(id: 'test');
+    $plugin->setClass('example');
+    $this->assertEquals('example', $plugin->getClass());
+  }
+
+}
+
+/**
+ * {@inheritdoc}
+ */
+class PluginStub extends Plugin {
+
+}
diff --git a/core/tests/Drupal/Tests/Component/Plugin/Discovery/AttributeBridgeDecoratorTest.php b/core/tests/Drupal/Tests/Component/Plugin/Discovery/AttributeBridgeDecoratorTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..f6b37647a498d1901041139e7bd0a60a0a45e154
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Plugin/Discovery/AttributeBridgeDecoratorTest.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace Drupal\Tests\Component\Plugin\Discovery;
+
+use Drupal\Component\Plugin\Attribute\Plugin;
+use Drupal\Component\Plugin\Definition\PluginDefinition;
+use Drupal\Component\Plugin\Discovery\AttributeBridgeDecorator;
+use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Component\Annotation\Plugin\Discovery\AnnotationBridgeDecorator
+ * @group Plugin
+ */
+class AttributeBridgeDecoratorTest extends TestCase {
+
+  /**
+   * @covers ::getDefinitions
+   */
+  public function testGetDefinitions() {
+    // Normally the attribute classes would be autoloaded.
+    include_once __DIR__ . '/../Attribute/Fixtures/CustomPlugin.php';
+    include_once __DIR__ . '/../Attribute/Fixtures/Plugins/PluginNamespace/AttributeDiscoveryTest1.php';
+
+    $definitions = [];
+    $definitions['object'] = new ObjectDefinition(['id' => 'foo']);
+    $definitions['array'] = [
+      'id' => 'bar',
+      'class' => 'com\example\PluginNamespace\AttributeDiscoveryTest1',
+    ];
+    $discovery = $this->createMock(DiscoveryInterface::class);
+    $discovery->expects($this->any())
+      ->method('getDefinitions')
+      ->willReturn($definitions);
+
+    $decorator = new AttributeBridgeDecorator($discovery, TestAttribute::class);
+
+    $expected = [
+      'object' => new ObjectDefinition(['id' => 'foo']),
+      'array' => (new ObjectDefinition(['id' => 'bar']))->setClass('com\example\PluginNamespace\AttributeDiscoveryTest1'),
+    ];
+    $this->assertEquals($expected, $decorator->getDefinitions());
+  }
+
+  /**
+   * Tests that the decorator of other methods works.
+   *
+   * @covers ::__call
+   */
+  public function testOtherMethod(): void {
+    // Normally the attribute classes would be autoloaded.
+    include_once __DIR__ . '/../Attribute/Fixtures/CustomPlugin.php';
+    include_once __DIR__ . '/../Attribute/Fixtures/Plugins/PluginNamespace/AttributeDiscoveryTest1.php';
+
+    $discovery = $this->createMock(ExtendedDiscoveryInterface::class);
+    $discovery->expects($this->exactly(2))
+      ->method('otherMethod')
+      ->willReturnCallback(fn($id) => $id === 'foo');
+
+    $decorator = new AttributeBridgeDecorator($discovery, TestAttribute::class);
+
+    $this->assertTrue($decorator->otherMethod('foo'));
+    $this->assertFalse($decorator->otherMethod('bar'));
+  }
+
+}
+
+interface ExtendedDiscoveryInterface extends DiscoveryInterface {
+
+  public function otherMethod(string $id): bool;
+
+}
+
+/**
+ * {@inheritdoc}
+ */
+class TestAttribute extends Plugin {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function get(): object {
+    return new ObjectDefinition(parent::get());
+  }
+
+}
+
+/**
+ * {@inheritdoc}
+ */
+class ObjectDefinition extends PluginDefinition {
+
+  /**
+   * ObjectDefinition constructor.
+   *
+   * @param array $definition
+   *   An array of definition values.
+   */
+  public function __construct(array $definition) {
+    foreach ($definition as $property => $value) {
+      $this->{$property} = $value;
+    }
+  }
+
+}