diff --git a/modules/og_prepopulate/og_prepopulate.info.yml b/modules/og_prepopulate/og_prepopulate.info.yml
index 6a14670..d21cb1c 100644
--- a/modules/og_prepopulate/og_prepopulate.info.yml
+++ b/modules/og_prepopulate/og_prepopulate.info.yml
@@ -1,7 +1,7 @@
 type: module
 name: Organic Groups Prepopulate
 description: Allows prepopulation of og reference fields.
-core_version_requirement: ^8 || ^9 || ^10 || ^11
+core_version_requirement: ^10.1 || ^11 || ^12
 dependencies:
   - prepopulate:prepopulate
   - og:og
diff --git a/modules/og_prepopulate/og_prepopulate.module b/modules/og_prepopulate/og_prepopulate.module
index 16e4b08..4f784e4 100644
--- a/modules/og_prepopulate/og_prepopulate.module
+++ b/modules/og_prepopulate/og_prepopulate.module
@@ -5,26 +5,16 @@
  * The module file for og_prepopulate.
  */
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\og_prepopulate\Hook\OgPrepopulateHooks;
 use Drupal\Core\Form\FormStateInterface;
 
 /**
  * Implements hook_field_widget_WIDGET_TYPE_form_alter() for og_complex.
  */
+#[LegacyHook]
 function og_prepopulate_field_widget_og_complex_form_alter(&$element, FormStateInterface $form_state, $context) {
-  // If this is a subsequent step of a multi-step form, the prepopulate values
-  // have done their work, and the user may have modified them: bail.
-  if ($form_state->isRebuilding()) {
-    return;
-  }
-  /** @var \Drupal\Core\Field\FieldItemList $field */
-  $field = $context['items'];
-  if (\Drupal::request()->query->has($field->getName())) {
-    $element['#after_build'][] = 'og_prepopulate_after_build';
-  }
-  // If a value is already assigned, then hide the og audience field.
-  elseif (!$field->isEmpty()) {
-    $element['#access'] = FALSE;
-  }
+  \Drupal::service(OgPrepopulateHooks::class)->fieldWidgetOgComplexFormAlter($element, $form_state, $context);
 }
 
 /**
diff --git a/modules/og_prepopulate/og_prepopulate.services.yml b/modules/og_prepopulate/og_prepopulate.services.yml
index 5040259..4ed9006 100644
--- a/modules/og_prepopulate/og_prepopulate.services.yml
+++ b/modules/og_prepopulate/og_prepopulate.services.yml
@@ -2,3 +2,7 @@ services:
   og_prepopulate.populator:
     class: Drupal\og_prepopulate\Populate
     arguments: ['@request_stack', '@entity_type.manager', '@module_handler', '@current_user']
+
+  Drupal\og_prepopulate\Hook\OgPrepopulateHooks:
+    class: Drupal\og_prepopulate\Hook\OgPrepopulateHooks
+    autowire: true
diff --git a/modules/og_prepopulate/src/Hook/OgPrepopulateHooks.php b/modules/og_prepopulate/src/Hook/OgPrepopulateHooks.php
new file mode 100644
index 0000000..ee467d1
--- /dev/null
+++ b/modules/og_prepopulate/src/Hook/OgPrepopulateHooks.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\og_prepopulate\Hook;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for og_prepopulate.
+ */
+class OgPrepopulateHooks {
+
+  /**
+   * Implements hook_field_widget_WIDGET_TYPE_form_alter() for og_complex.
+   */
+  #[Hook('field_widget_og_complex_form_alter')]
+  public static function fieldWidgetOgComplexFormAlter(&$element, FormStateInterface $form_state, $context) {
+    // If this is a subsequent step of a multi-step form, the prepopulate values
+    // have done their work, and the user may have modified them: bail.
+    if ($form_state->isRebuilding()) {
+      return;
+    }
+    /** @var \Drupal\Core\Field\FieldItemList $field */
+    $field = $context['items'];
+    if (\Drupal::request()->query->has($field->getName())) {
+      $element['#after_build'][] = 'og_prepopulate_after_build';
+    }
+    elseif (!$field->isEmpty()) {
+      $element['#access'] = FALSE;
+    }
+  }
+
+}
diff --git a/prepopulate.info.yml b/prepopulate.info.yml
index 3e0cd7f..4e07ceb 100644
--- a/prepopulate.info.yml
+++ b/prepopulate.info.yml
@@ -1,7 +1,7 @@
 type: module
 name: Prepopulate
 description: Allows form elements to be prepopulated from the URL.
-core_version_requirement: ^8 || ^9 || ^10 || ^11
+core_version_requirement: ^10.1 || ^11 || ^12
 test_dependencies:
   - og:og
   - inline_entity_form:inline_entity_form
diff --git a/prepopulate.module b/prepopulate.module
index af89df2..ed08f51 100644
--- a/prepopulate.module
+++ b/prepopulate.module
@@ -5,20 +5,16 @@
  * Fill form elements with data from GET or POST values.
  */
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\prepopulate\Hook\PrepopulateHooks;
 use Drupal\Core\Form\FormStateInterface;
 
 /**
  * Implements hook_form_alter().
  */
+#[LegacyHook]
 function prepopulate_form_alter(&$form, FormStateInterface $form_state, $form_id) {
-  // If this is a subsequent step of a multi-step form, the prepopulate values
-  // have done their work, and the user may have modified them: bail.
-  if ($form_state->isRebuilding()) {
-    return;
-  }
-  if (\Drupal::request()->query->has('edit')) {
-    $form['#after_build'][] = 'prepopulate_after_build';
-  }
+  \Drupal::service(PrepopulateHooks::class)->formAlter($form, $form_state, $form_id);
 }
 
 /**
diff --git a/prepopulate.services.yml b/prepopulate.services.yml
index d061482..f9fc052 100644
--- a/prepopulate.services.yml
+++ b/prepopulate.services.yml
@@ -3,3 +3,7 @@ services:
     class: Drupal\prepopulate\Populate
     arguments: ['@request_stack', '@entity_type.manager', '@module_handler']
     required_cache_contexts: ['languages:language_interface', 'theme', 'user.permissions', 'url.query_args']
+
+  Drupal\prepopulate\Hook\PrepopulateHooks:
+    class: Drupal\prepopulate\Hook\PrepopulateHooks
+    autowire: true
diff --git a/src/Hook/PrepopulateHooks.php b/src/Hook/PrepopulateHooks.php
new file mode 100644
index 0000000..4caf361
--- /dev/null
+++ b/src/Hook/PrepopulateHooks.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\prepopulate\Hook;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for prepopulate.
+ */
+class PrepopulateHooks {
+
+  /**
+   * Implements hook_form_alter().
+   */
+  #[Hook('form_alter')]
+  public static function formAlter(&$form, FormStateInterface $form_state, $form_id) {
+    // If this is a subsequent step of a multi-step form, the prepopulate values
+    // have done their work, and the user may have modified them: bail.
+    if ($form_state->isRebuilding()) {
+      return;
+    }
+    if (\Drupal::request()->query->has('edit')) {
+      $form['#after_build'][] = 'prepopulate_after_build';
+    }
+  }
+
+}
diff --git a/tests/modules/prepopulate_test/prepopulate_test.info.yml b/tests/modules/prepopulate_test/prepopulate_test.info.yml
index 7ad299a..edaf182 100644
--- a/tests/modules/prepopulate_test/prepopulate_test.info.yml
+++ b/tests/modules/prepopulate_test/prepopulate_test.info.yml
@@ -2,7 +2,7 @@ name: 'Prepopulate test module'
 type: module
 description: 'Provides test form and config for pre-populating.'
 package: Testing
-core_version_requirement: ^8 || ^9 || ^10 || ^11
+core_version_requirement: ^10.1 || ^11 || ^12
 dependencies:
   - drupal:datetime
   - drupal:datetime_range
diff --git a/tests/modules/prepopulate_test_unsafe/prepopulate_test_unsafe.info.yml b/tests/modules/prepopulate_test_unsafe/prepopulate_test_unsafe.info.yml
index b3fed2d..265a6be 100644
--- a/tests/modules/prepopulate_test_unsafe/prepopulate_test_unsafe.info.yml
+++ b/tests/modules/prepopulate_test_unsafe/prepopulate_test_unsafe.info.yml
@@ -2,6 +2,6 @@ name: 'Prepopulate test unsafe module'
 type: module
 description: 'Provides alter for testing unsafe methods of pre-populating.'
 package: Testing
-core_version_requirement: ^8 || ^9 || ^10 || ^11
+core_version_requirement: ^10.1 || ^11 || ^12
 dependencies:
   - prepopulate:prepopulate
diff --git a/tests/modules/prepopulate_test_unsafe/prepopulate_test_unsafe.module b/tests/modules/prepopulate_test_unsafe/prepopulate_test_unsafe.module
index 7144e56..934be30 100644
--- a/tests/modules/prepopulate_test_unsafe/prepopulate_test_unsafe.module
+++ b/tests/modules/prepopulate_test_unsafe/prepopulate_test_unsafe.module
@@ -1,5 +1,12 @@
 <?php
 
+/**
+ * @file
+ */
+
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\prepopulate_test_unsafe\Hook\PrepopulateTestUnsafeHooks;
+
 /**
  * @file
  * Hooks for the prepopulate_test_unsafe module.
@@ -8,9 +15,7 @@
 /**
  * Implements hook_prepopulate_whitelist_alter().
  */
+#[LegacyHook]
 function prepopulate_test_unsafe_prepopulate_whitelist_alter(array &$whitelisted_types) {
-  $whitelisted_types[] = 'checkbox';
-  $whitelisted_types[] = 'checkboxes';
-  $whitelisted_types[] = 'radio';
-  $whitelisted_types[] = 'radios';
+  \Drupal::service(PrepopulateTestUnsafeHooks::class)->prepopulateWhitelistAlter($whitelisted_types);
 }
diff --git a/tests/modules/prepopulate_test_unsafe/prepopulate_test_unsafe.services.yml b/tests/modules/prepopulate_test_unsafe/prepopulate_test_unsafe.services.yml
new file mode 100644
index 0000000..53f4e05
--- /dev/null
+++ b/tests/modules/prepopulate_test_unsafe/prepopulate_test_unsafe.services.yml
@@ -0,0 +1,5 @@
+
+services:
+  Drupal\prepopulate_test_unsafe\Hook\PrepopulateTestUnsafeHooks:
+    class: Drupal\prepopulate_test_unsafe\Hook\PrepopulateTestUnsafeHooks
+    autowire: true
diff --git a/tests/modules/prepopulate_test_unsafe/src/Hook/PrepopulateTestUnsafeHooks.php b/tests/modules/prepopulate_test_unsafe/src/Hook/PrepopulateTestUnsafeHooks.php
new file mode 100644
index 0000000..72b6d6d
--- /dev/null
+++ b/tests/modules/prepopulate_test_unsafe/src/Hook/PrepopulateTestUnsafeHooks.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\prepopulate_test_unsafe\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for prepopulate_test_unsafe.
+ */
+class PrepopulateTestUnsafeHooks {
+  /**
+   * @file
+   * Hooks for the prepopulate_test_unsafe module.
+   */
+
+  /**
+   * Implements hook_prepopulate_whitelist_alter().
+   */
+  #[Hook('prepopulate_whitelist_alter')]
+  public static function prepopulateWhitelistAlter(array &$whitelisted_types) {
+    $whitelisted_types[] = 'checkbox';
+    $whitelisted_types[] = 'checkboxes';
+    $whitelisted_types[] = 'radio';
+    $whitelisted_types[] = 'radios';
+  }
+
+}
