diff --git a/features.module b/features.module
index 07e2740..da26d68 100644
--- a/features.module
+++ b/features.module
@@ -4,54 +4,31 @@
  * @file
  * Main hooks for Features module.
  */
-
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\features\Hook\FeaturesHooks;
 use Drupal\Core\Routing\RouteMatchInterface;
 
 /**
  * Implements hook_help().
  */
+#[LegacyHook]
 function features_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.features':
-      $output = '';
-      $output .= '<h3>' . t('About') . '</h3>';
-      $output .= '<p>' . t('The Features module provides a user interface for exporting bundles of configuration into modules. For more information, see the online documentation for <a href=":url">Features module</a>', [
-        ':url' => 'http://drupal.org/node/2404427',
-      ]) . '</p>';
-      return $output;
-  }
+  return \Drupal::service(FeaturesHooks::class)->help($route_name, $route_match);
 }
 
 /**
  * Implements hook_file_download().
  */
-function features_file_download($uri) {
-  $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
-  $scheme = $stream_wrapper_manager->getScheme($uri);
-  $target = $stream_wrapper_manager->getTarget($uri);
-
-  if ($scheme == 'temporary' && $target) {
-    $request = \Drupal::request();
-    $route = $request->attributes->get('_route');
-    // Check if we were called by Features download route.
-    // No additional access checking needed here: route requires
-    // "export configuration" permission, token is validated by the controller.
-    // @see \Drupal\features\Controller\FeaturesController::downloadExport()
-    if ($route == 'features.export_download') {
-      return [
-        'Content-disposition' => 'attachment; filename="' . $target . '"',
-      ];
-    }
-  }
+#[LegacyHook]
+function features_file_download($uri)
+{
+    return \Drupal::service(FeaturesHooks::class)->fileDownload($uri);
 }
 
 /**
  * Implements hook_modules_installed().
  */
+#[LegacyHook]
 function features_modules_installed($modules) {
-  if (!in_array('features', $modules)) {
-    /** @var \Drupal\features\FeaturesAssignerInterface $assigner */
-    $assigner = \Drupal::service('features_assigner');
-    $assigner->purgeConfiguration();
-  }
+  \Drupal::service(FeaturesHooks::class)->modulesInstalled($modules);
 }
diff --git a/features.services.yml b/features.services.yml
index 2b9e1e2..6582ac7 100644
--- a/features.services.yml
+++ b/features.services.yml
@@ -49,3 +49,7 @@ services:
   logger.channel.features:
     parent: logger.channel_base
     arguments: ['features']
+
+  Drupal\features\Hook\FeaturesHooks:
+    class: Drupal\features\Hook\FeaturesHooks
+    autowire: true
diff --git a/modules/features_ui/features_ui.module b/modules/features_ui/features_ui.module
index cd2c165..a830467 100644
--- a/modules/features_ui/features_ui.module
+++ b/modules/features_ui/features_ui.module
@@ -4,44 +4,22 @@
  * @file
  * Allows site administrators to modify configuration.
  */
-
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\features_ui\Hook\FeaturesUiHooks;
 use Drupal\Core\Routing\RouteMatchInterface;
 
 /**
  * Implements hook_help().
  */
+#[LegacyHook]
 function features_ui_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'features.assignment':
-      $output = '';
-      $output .= '<p>' . t('Bundles are used to collect together groups of features. A bundle provides a shared <a href=":namespace">namespace</a> for all features included in it, which prevents conflicts and helps distinguish your features from those produced for other purposes. Common uses of bundles include:', [':namespace' => 'http://en.wikipedia.org/wiki/Namespace']);
-      $output .= '<ul>';
-      $output .= '<li>' . t('Custom features for use on a particular site.') . '</li>';
-      $output .= '<li>' . t('The features of a given <a href=":distributions">distribution</a>.', [':distributions' => 'https://www.drupal.org/documentation/build/distributions']) . '</li>';
-      $output .= '</ul></p>';
-      $output .= '<p>' . t("Use the form below to manage bundles. Each bundle comes with a set of assignment methods. By configuring and ordering the assignment methods, you can set the defaults for what does and doesn't get packaged into features for your bundle. Use the <em>Bundle</em> select to choose which bundle to edit, or chose <em>--New--</em> to create a new bundle. The <em>Default</em> bundle does not include a namespace and cannot be deleted.") . '</p>';
-      return $output;
-
-    case 'features.export':
-      $output = '';
-      $output .= '<p>' . t('Export packages of configuration into modules.') . '</p>';
-      return $output;
-  }
+  return \Drupal::service(FeaturesUiHooks::class)->help($route_name, $route_match);
 }
 
 /**
  * Implements hook_theme().
  */
+#[LegacyHook]
 function features_ui_theme() {
-  return [
-    'features_assignment_configure_form' => [
-      'render element' => 'form',
-      'file' => 'features_ui.admin.inc',
-    ],
-    'features_items' => [
-      'variables' => [
-        'items' => [],
-      ],
-    ],
-  ];
+  return \Drupal::service(FeaturesUiHooks::class)->theme();
 }
diff --git a/modules/features_ui/features_ui.services.yml b/modules/features_ui/features_ui.services.yml
new file mode 100644
index 0000000..a13bbe0
--- /dev/null
+++ b/modules/features_ui/features_ui.services.yml
@@ -0,0 +1,5 @@
+
+services:
+  Drupal\features_ui\Hook\FeaturesUiHooks:
+    class: Drupal\features_ui\Hook\FeaturesUiHooks
+    autowire: true
diff --git a/modules/features_ui/src/Form/FeaturesEditForm.php b/modules/features_ui/src/Form/FeaturesEditForm.php
index 941cb22..560412b 100644
--- a/modules/features_ui/src/Form/FeaturesEditForm.php
+++ b/modules/features_ui/src/Form/FeaturesEditForm.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\features_ui\Form;
 
+use Drupal\Component\Utility\DeprecationHelper;
 use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\SortArray;
 use Drupal\Component\Utility\Xss;
@@ -433,7 +434,18 @@ class FeaturesEditForm extends FormBase {
     ];
 
     // Filter field used in JavaScript, so JavaScript will unhide it.
-    $element['features_filter_wrapper'] = [
+    $element['features_filter_wrapper'] = DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.2.0', fn() => [
+      '#type' => 'fieldset',
+      '#title' => $this->t('Filters'),
+      '#title_display' => 'invisible',
+      '#tree' => FALSE,
+      '#prefix' => '<div id="features-filter" class="features-filter js-features-filter visually-hidden">',
+      '#suffix' => '</div>',
+      '#weight' => -10,
+      '#attributes' => [
+        'class' => ['features-filter__fieldset', 'container-inline'],
+      ],
+    ], fn() => [
       '#type' => 'fieldgroup',
       '#title' => $this->t('Filters'),
       '#title_display' => 'invisible',
@@ -444,7 +456,7 @@ class FeaturesEditForm extends FormBase {
       '#attributes' => [
         'class' => ['features-filter__fieldset', 'container-inline'],
       ],
-    ];
+    ]);
     $element['features_filter_wrapper']['features_filter'] = [
       '#type' => 'textfield',
       '#title' => $this->t('Search'),
diff --git a/modules/features_ui/src/Hook/FeaturesUiHooks.php b/modules/features_ui/src/Hook/FeaturesUiHooks.php
new file mode 100644
index 0000000..4857c26
--- /dev/null
+++ b/modules/features_ui/src/Hook/FeaturesUiHooks.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Drupal\features_ui\Hook;
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+/**
+ * Hook implementations for features_ui.
+ */
+class FeaturesUiHooks
+{
+    use StringTranslationTrait;
+    /**
+     * Implements hook_help().
+     */
+    #[Hook('help')]
+    public function help($route_name, \Drupal\Core\Routing\RouteMatchInterface $route_match)
+    {
+        switch ($route_name) {
+            case 'features.assignment':
+                $output = '';
+                $output .= '<p>' . $this->t('Bundles are used to collect together groups of features. A bundle provides a shared <a href=":namespace">namespace</a> for all features included in it, which prevents conflicts and helps distinguish your features from those produced for other purposes. Common uses of bundles include:', [
+                    ':namespace' => 'http://en.wikipedia.org/wiki/Namespace',
+                ]);
+                $output .= '<ul>';
+                $output .= '<li>' . $this->t('Custom features for use on a particular site.') . '</li>';
+                $output .= '<li>' . $this->t('The features of a given <a href=":distributions">distribution</a>.', [
+                    ':distributions' => 'https://www.drupal.org/documentation/build/distributions',
+                ]) . '</li>';
+                $output .= '</ul></p>';
+                $output .= '<p>' . $this->t("Use the form below to manage bundles. Each bundle comes with a set of assignment methods. By configuring and ordering the assignment methods, you can set the defaults for what does and doesn't get packaged into features for your bundle. Use the <em>Bundle</em> select to choose which bundle to edit, or chose <em>--New--</em> to create a new bundle. The <em>Default</em> bundle does not include a namespace and cannot be deleted.") . '</p>';
+                return $output;
+            case 'features.export':
+                $output = '';
+                $output .= '<p>' . $this->t('Export packages of configuration into modules.') . '</p>';
+                return $output;
+        }
+    }
+    /**
+     * Implements hook_theme().
+     */
+    #[Hook('theme')]
+    public static function theme()
+    {
+        return [
+            'features_assignment_configure_form' => [
+                'render element' => 'form',
+                'file' => 'features_ui.admin.inc',
+            ],
+            'features_items' => [
+                'variables' => [
+                    'items' => [
+                    ],
+                ],
+            ],
+        ];
+    }
+}
diff --git a/src/Hook/FeaturesHooks.php b/src/Hook/FeaturesHooks.php
new file mode 100644
index 0000000..d8a3eed
--- /dev/null
+++ b/src/Hook/FeaturesHooks.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Drupal\features\Hook;
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+/**
+ * Hook implementations for features.
+ */
+class FeaturesHooks
+{
+    use StringTranslationTrait;
+    /**
+     * Implements hook_help().
+     */
+    #[Hook('help')]
+    public function help($route_name, \Drupal\Core\Routing\RouteMatchInterface $route_match)
+    {
+        switch ($route_name) {
+            case 'help.page.features':
+                $output = '';
+                $output .= '<h3>' . $this->t('About') . '</h3>';
+                $output .= '<p>' . $this->t('The Features module provides a user interface for exporting bundles of configuration into modules. For more information, see the online documentation for <a href=":url">Features module</a>', [
+                    ':url' => 'http://drupal.org/node/2404427',
+                ]) . '</p>';
+                return $output;
+        }
+    }
+    /**
+     * Implements hook_file_download().
+     */
+    #[Hook('file_download')]
+    public static function fileDownload($uri)
+    {
+        $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
+        $scheme = $stream_wrapper_manager->getScheme($uri);
+        $target = $stream_wrapper_manager->getTarget($uri);
+        if ($scheme == 'temporary' && $target) {
+            $request = \Drupal::request();
+            $route = $request->attributes->get('_route');
+            // Check if we were called by Features download route.
+            // No additional access checking needed here: route requires
+            // "export configuration" permission, token is validated by the controller.
+            // @see \Drupal\features\Controller\FeaturesController::downloadExport()
+            if ($route == 'features.export_download') {
+                return [
+                    'Content-disposition' => 'attachment; filename="' . $target . '"',
+                ];
+            }
+        }
+    }
+    /**
+     * Implements hook_modules_installed().
+     */
+    #[Hook('modules_installed')]
+    public static function modulesInstalled($modules)
+    {
+        if (!in_array('features', $modules)) {
+            /** @var \Drupal\features\FeaturesAssignerInterface $assigner */
+            $assigner = \Drupal::service('features_assigner');
+            $assigner->purgeConfiguration();
+        }
+    }
+}
