diff --git a/modules/selectify_webform/selectify_webform.module b/modules/selectify_webform/selectify_webform.module
index fba26aa..3c4612c 100644
--- a/modules/selectify_webform/selectify_webform.module
+++ b/modules/selectify_webform/selectify_webform.module
@@ -9,6 +9,8 @@
  * webform_likert, and webform_mapping.
  */
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\selectify_webform\Hook\SelectifyWebformHooks;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element;
 
@@ -97,89 +99,9 @@ function _selectify_webform_get_config(): array|false {
  * Intercepts each Webform element render array and applies Selectify styling
  * to select elements and composite elements containing selects.
  */
+#[LegacyHook]
 function selectify_webform_webform_element_alter(array &$element, FormStateInterface $form_state, array $context): void {
-  $config = _selectify_webform_get_config();
-  if ($config === FALSE) {
-    return;
-  }
-
-  $webform = $context['webform'] ?? NULL;
-  $element_type = $element['#type'] ?? '';
-
-  // Determine the effective widget for this Webform:
-  // - If apply_site_wide is enabled, always use the global widget.
-  // - Otherwise, look for a specific widget for this Webform ID.
-  //   If none is configured, do not style this element.
-  $widget = NULL;
-  $webform_id = $webform?->id() ?? '';
-  if (!empty($config['apply_site_wide'])) {
-    $widget = $config['global_widget'] ?? NULL;
-  }
-  elseif ($webform_id && !empty($config['widget_map'][$webform_id])) {
-    $widget = $config['widget_map'][$webform_id];
-  }
-
-  // Allow other modules to alter the widget selection.
-  if (!empty($widget) && $widget !== 'none') {
-    $context = [
-      'form_id' => $webform_id,
-      'element' => &$element,
-      'integration_type' => 'webform',
-      'webform' => $webform,
-    ];
-    \Drupal::moduleHandler()->alter('selectify_widget', $widget, $context);
-  }
-
-  if (empty($widget) || $widget === 'none') {
-    return;
-  }
-
-  /** @var \Drupal\selectify\Service\SelectifyHelper $helper */
-  $helper = \Drupal::service('selectify.helper');
-
-  // Direct select element.
-  if ($element_type === 'select') {
-    // Skip selects with AJAX callbacks.
-    if (!empty($element['#ajax'])) {
-      return;
-    }
-    // Skip field widget selects.
-    if (isset($element['#key_column'])) {
-      return;
-    }
-    // Skip date/time picker selects (year, month, day, hour, minute).
-    // These are part of a cohesive date picker UI and should remain grouped.
-    if (_selectify_webform_is_date_select($element)) {
-      return;
-    }
-    // Skip selects per shared rules (third-party libs, hidden/disabled, context).
-    if ($helper->shouldSkipSelectElement($element)) {
-      return;
-    }
-
-    // Allow other modules to alter the element before Selectify processes it.
-    $element_context = [
-      'widget' => $widget,
-      'webform_id' => $webform_id,
-      'integration_type' => 'webform',
-      'webform' => $webform,
-    ];
-    \Drupal::moduleHandler()->alter('selectify_element', $element, $element_context);
-
-    // Check if the element was marked to skip Selectify processing.
-    if (!empty($element['#selectify_skip'])) {
-      return;
-    }
-
-    _selectify_webform_style_select($element, $helper, $widget);
-    return;
-  }
-
-  // Composite element types — walk children to find nested selects.
-  $composites = _selectify_webform_composite_types();
-  if (in_array($element_type, $composites, TRUE)) {
-    _selectify_webform_walk_composites($element, $helper, $widget);
-  }
+  \Drupal::service(SelectifyWebformHooks::class)->webformElementAlter($element, $form_state, $context);
 }
 
 /**
@@ -189,40 +111,9 @@ function selectify_webform_webform_element_alter(array &$element, FormStateInter
  * couldn't reach (e.g. dynamically added elements). Skips elements already
  * styled.
  */
+#[LegacyHook]
 function selectify_webform_form_alter(array &$form, FormStateInterface $form_state, string $form_id): void {
-  if (!str_starts_with($form_id, 'webform_submission_')) {
-    return;
-  }
-
-  $config = _selectify_webform_get_config();
-  if ($config === FALSE) {
-    return;
-  }
-
-  $webform_id = '';
-  if (isset($form['#webform']) && is_object($form['#webform']) && method_exists($form['#webform'], 'id')) {
-    $webform_id = $form['#webform']->id() ?? '';
-  }
-  if ($webform_id === '' && preg_match('/^webform_submission_(.+)_(add|edit|api)_form$/', $form_id, $m)) {
-    $webform_id = $m[1];
-  }
-
-  $widget = NULL;
-  if (!empty($config['apply_site_wide'])) {
-    $widget = $config['global_widget'] ?? NULL;
-  }
-  elseif ($webform_id && !empty($config['widget_map'][$webform_id])) {
-    $widget = $config['widget_map'][$webform_id];
-  }
-
-  if (empty($widget) || $widget === 'none') {
-    return;
-  }
-
-  /** @var \Drupal\selectify\Service\SelectifyHelper $helper */
-  $helper = \Drupal::service('selectify.helper');
-
-  _selectify_webform_walk_composites($form, $helper, $widget);
+  \Drupal::service(SelectifyWebformHooks::class)->formAlter($form, $form_state, $form_id);
 }
 
 /**
diff --git a/modules/selectify_webform/selectify_webform.services.yml b/modules/selectify_webform/selectify_webform.services.yml
new file mode 100644
index 0000000..6571ed2
--- /dev/null
+++ b/modules/selectify_webform/selectify_webform.services.yml
@@ -0,0 +1,5 @@
+
+services:
+  Drupal\selectify_webform\Hook\SelectifyWebformHooks:
+    class: Drupal\selectify_webform\Hook\SelectifyWebformHooks
+    autowire: true
diff --git a/modules/selectify_webform/src/Hook/SelectifyWebformHooks.php b/modules/selectify_webform/src/Hook/SelectifyWebformHooks.php
new file mode 100644
index 0000000..67abdfa
--- /dev/null
+++ b/modules/selectify_webform/src/Hook/SelectifyWebformHooks.php
@@ -0,0 +1,133 @@
+<?php
+
+namespace Drupal\selectify_webform\Hook;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for selectify_webform.
+ */
+class SelectifyWebformHooks {
+
+  /**
+   * Implements hook_webform_element_alter().
+   *
+   * Intercepts each Webform element render array and applies Selectify styling
+   * to select elements and composite elements containing selects.
+   */
+  #[Hook('webform_element_alter')]
+  public static function webformElementAlter(array &$element, FormStateInterface $form_state, array $context): void {
+    $config = _selectify_webform_get_config();
+    if ($config === FALSE) {
+      return;
+    }
+    $webform = $context['webform'] ?? NULL;
+    $element_type = $element['#type'] ?? '';
+    // Determine the effective widget for this Webform:
+    // - If apply_site_wide is enabled, always use the global widget.
+    // - Otherwise, look for a specific widget for this Webform ID.
+    //   If none is configured, do not style this element.
+    $widget = NULL;
+    $webform_id = $webform?->id() ?? '';
+    if (!empty($config['apply_site_wide'])) {
+      $widget = $config['global_widget'] ?? NULL;
+    }
+    elseif ($webform_id && !empty($config['widget_map'][$webform_id])) {
+      $widget = $config['widget_map'][$webform_id];
+    }
+    // Allow other modules to alter the widget selection.
+    if (!empty($widget) && $widget !== 'none') {
+      $context = [
+        'form_id' => $webform_id,
+        'element' => &$element,
+        'integration_type' => 'webform',
+        'webform' => $webform,
+      ];
+      \Drupal::moduleHandler()->alter('selectify_widget', $widget, $context);
+    }
+    if (empty($widget) || $widget === 'none') {
+      return;
+    }
+    /** @var \Drupal\selectify\Service\SelectifyHelper $helper */
+    $helper = \Drupal::service('selectify.helper');
+    // Direct select element.
+    if ($element_type === 'select') {
+      // Skip selects with AJAX callbacks.
+      if (!empty($element['#ajax'])) {
+        return;
+      }
+      // Skip field widget selects.
+      if (isset($element['#key_column'])) {
+        return;
+      }
+      // Skip date/time picker selects (year, month, day, hour, minute).
+      // These are part of a cohesive date picker UI and should remain grouped.
+      if (_selectify_webform_is_date_select($element)) {
+        return;
+      }
+      // Skip selects per shared rules (third-party libs, hidden/disabled, context).
+      if ($helper->shouldSkipSelectElement($element)) {
+        return;
+      }
+      // Allow other modules to alter the element before Selectify processes it.
+      $element_context = [
+        'widget' => $widget,
+        'webform_id' => $webform_id,
+        'integration_type' => 'webform',
+        'webform' => $webform,
+      ];
+      \Drupal::moduleHandler()->alter('selectify_element', $element, $element_context);
+      // Check if the element was marked to skip Selectify processing.
+      if (!empty($element['#selectify_skip'])) {
+        return;
+      }
+      _selectify_webform_style_select($element, $helper, $widget);
+      return;
+    }
+    // Composite element types — walk children to find nested selects.
+    $composites = _selectify_webform_composite_types();
+    if (in_array($element_type, $composites, TRUE)) {
+      _selectify_webform_walk_composites($element, $helper, $widget);
+    }
+  }
+
+  /**
+   * Implements hook_form_alter().
+   *
+   * Safety net: catches any webform selects that hook_webform_element_alter
+   * couldn't reach (e.g. dynamically added elements). Skips elements already
+   * styled.
+   */
+  #[Hook('form_alter')]
+  public static function formAlter(array &$form, FormStateInterface $form_state, string $form_id): void {
+    if (!str_starts_with($form_id, 'webform_submission_')) {
+      return;
+    }
+    $config = _selectify_webform_get_config();
+    if ($config === FALSE) {
+      return;
+    }
+    $webform_id = '';
+    if (isset($form['#webform']) && is_object($form['#webform']) && method_exists($form['#webform'], 'id')) {
+      $webform_id = $form['#webform']->id() ?? '';
+    }
+    if ($webform_id === '' && preg_match('/^webform_submission_(.+)_(add|edit|api)_form$/', $form_id, $m)) {
+      $webform_id = $m[1];
+    }
+    $widget = NULL;
+    if (!empty($config['apply_site_wide'])) {
+      $widget = $config['global_widget'] ?? NULL;
+    }
+    elseif ($webform_id && !empty($config['widget_map'][$webform_id])) {
+      $widget = $config['widget_map'][$webform_id];
+    }
+    if (empty($widget) || $widget === 'none') {
+      return;
+    }
+    /** @var \Drupal\selectify\Service\SelectifyHelper $helper */
+    $helper = \Drupal::service('selectify.helper');
+    _selectify_webform_walk_composites($form, $helper, $widget);
+  }
+
+}
diff --git a/modules/selectify_webform/tests/src/Kernel/SelectifyWebformElementAlterTest.php b/modules/selectify_webform/tests/src/Kernel/SelectifyWebformElementAlterTest.php
index 25f653d..5abf2a6 100644
--- a/modules/selectify_webform/tests/src/Kernel/SelectifyWebformElementAlterTest.php
+++ b/modules/selectify_webform/tests/src/Kernel/SelectifyWebformElementAlterTest.php
@@ -4,6 +4,8 @@ declare(strict_types=1);
 
 namespace Drupal\Tests\selectify_webform\Kernel;
 
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
 use Drupal\Core\Form\FormState;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\KernelTests\KernelTestBase;
@@ -20,6 +22,8 @@ use PHPUnit\Framework\Exception as PHPUnitException;
  * @group selectify_webform
  * @requires module webform
  */
+#[Group('selectify_webform')]
+#[RunTestsInSeparateProcesses]
 class SelectifyWebformElementAlterTest extends KernelTestBase {
 
   use StringTranslationTrait;
@@ -41,6 +45,7 @@ class SelectifyWebformElementAlterTest extends KernelTestBase {
    * {@inheritdoc}
    */
   protected function setUp(): void {
+    parent::setUp();
     try {
       parent::setUp();
     }
diff --git a/selectify.install b/selectify.install
index 51ed92a..c15b798 100644
--- a/selectify.install
+++ b/selectify.install
@@ -1,5 +1,11 @@
 <?php
 
+/**
+ * @file
+ */
+
+use Drupal\Component\Utility\DeprecationHelper;
+
 /**
  * @file
  * Install, update, and uninstall functions for the Selectify module.
@@ -43,7 +49,7 @@ function selectify_update_10001(): void {
     $changed = TRUE;
   }
   if ($changed) {
-    $config->save(TRUE);
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => $config->save(), fn() => $config->save(TRUE));
   }
 }
 
@@ -54,7 +60,7 @@ function selectify_update_10002(): void {
   $config = \Drupal::configFactory()->getEditable('selectify.settings');
   if ($config->get('form_api_excluded_forms') !== NULL) {
     $config->clear('form_api_excluded_forms');
-    $config->save(TRUE);
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => $config->save(), fn() => $config->save(TRUE));
   }
 }
 
@@ -65,7 +71,7 @@ function selectify_update_10003(): void {
   $config = \Drupal::configFactory()->getEditable('selectify.settings');
   if ($config->get('css_selector_widget_map') === NULL) {
     $config->set('css_selector_widget_map', []);
-    $config->save(TRUE);
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => $config->save(), fn() => $config->save(TRUE));
   }
 }
 
@@ -78,6 +84,6 @@ function selectify_update_10004(): void {
   $config = \Drupal::configFactory()->getEditable('selectify.settings');
   if ($config->get('form_api_widget') !== NULL) {
     $config->clear('form_api_widget');
-    $config->save(TRUE);
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => $config->save(), fn() => $config->save(TRUE));
   }
 }
diff --git a/selectify.module b/selectify.module
index 0b6a497..b275154 100644
--- a/selectify.module
+++ b/selectify.module
@@ -10,6 +10,8 @@
  * Developer:    Alaa Haddad https://www.alaahaddad.com.
  */
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\selectify\Hook\SelectifyHooks;
 use Drupal\Component\Utility\Html;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element;
@@ -19,52 +21,17 @@ use Drupal\Core\Template\Attribute;
 /**
  * Implements hook_preprocess_html().
  */
+#[LegacyHook]
 function selectify_preprocess_html(&$variables) {
-  /** @var \Drupal\selectify\Service\SelectifyHelper $helper */
-  $helper = \Drupal::service('selectify.helper');
-
-  // Check if current page is in the disabled pages list.
-  if ($helper->isPageDisabled()) {
-    return;
-  }
-
-  $config = \Drupal::config('selectify.settings');
-  $radioEnabled = $config->get('enable_radio');
-  $checkboxEnabled = $config->get('enable_checkbox');
-
-  if (!$radioEnabled && !$checkboxEnabled) {
-    return;
-  }
-
-  /** @var \Drupal\selectify\Service\SelectifyStyleService $styleService */
-  $styleService = \Drupal::service('selectify.style_service');
-  $bodyClasses = $styleService->getBodyClasses();
-
-  if (!empty($bodyClasses)) {
-    $classes = explode(' ', $bodyClasses);
-    foreach ($classes as $class) {
-      $variables['attributes']['class'][] = $class;
-    }
-  }
-
-  // Add selectify-color-disabled class when the active color mode is none.
-  $colorMode = $helper->isAdminRoute()
-    ? ($config->get('admin_accent_color_mode') ?? 'light')
-    : ($config->get('accent_color_mode') ?? 'light');
-
-  if ($colorMode === 'none') {
-    $variables['attributes']['class'][] = 'selectify-color-disabled';
-  }
+  \Drupal::service(SelectifyHooks::class)->preprocessHtml($variables);
 }
 
 /**
  * Implements hook_help().
  */
+#[LegacyHook]
 function selectify_help($route_name, RouteMatchInterface $route_match): ?string {
-  if ($route_name === 'help.page.selectify') {
-    return _selectify_render_readme();
-  }
-  return NULL;
+  return \Drupal::service(SelectifyHooks::class)->help($route_name, $route_match);
 }
 
 /**
@@ -94,65 +61,17 @@ function _selectify_render_readme(): string {
 /**
  * Implements hook_theme_suggestions_select_alter().
  */
+#[LegacyHook]
 function selectify_theme_suggestions_select_alter(array &$suggestions, array $variables) {
-  if (empty($variables['element']['#attributes']['class'])) {
-    return;
-  }
-
-  $classes = (array) $variables['element']['#attributes']['class'];
-
-  foreach ($suggestions as $key => $suggestion) {
-    if (strpos($suggestion, 'form_element') !== FALSE) {
-      unset($suggestions[$key]);
-    }
-  }
-
-  $class_to_suggestion = [
-    'selectify-apply-dropdown' => 'select__selectify_dropdown',
-    'selectify-apply-checkbox' => 'select__selectify_dropdown_checkbox',
-    'selectify-apply-searchable' => 'select__selectify_dropdown_searchable',
-    'selectify-apply-tags' => 'select__selectify_dropdown_tags',
-    'selectify-apply-dual' => 'select__selectify_dual',
-  ];
-
-  foreach ($class_to_suggestion as $class => $suggestion) {
-    if (in_array($class, $classes)) {
-      $suggestions[] = $suggestion;
-    }
-  }
+  \Drupal::service(SelectifyHooks::class)->themeSuggestionsSelectAlter($suggestions, $variables);
 }
 
 /**
  * Implements hook_theme().
  */
+#[LegacyHook]
 function selectify_theme($existing, $type, $theme, $path) {
-  return [
-    'select__selectify_dropdown' => [
-      'base hook' => 'select',
-      'template' => 'select--selectify-dropdown',
-      'path' => $path . '/templates',
-    ],
-    'select__selectify_dropdown_checkbox' => [
-      'base hook' => 'select',
-      'template' => 'select--selectify-dropdown-checkbox',
-      'path' => $path . '/templates',
-    ],
-    'select__selectify_dropdown_searchable' => [
-      'base hook' => 'select',
-      'template' => 'select--selectify-dropdown-searchable',
-      'path' => $path . '/templates',
-    ],
-    'select__selectify_dropdown_tags' => [
-      'base hook' => 'select',
-      'template' => 'select--selectify-dropdown-tags',
-      'path' => $path . '/templates',
-    ],
-    'select__selectify_dual' => [
-      'base hook' => 'select',
-      'template' => 'select--selectify-dual',
-      'path' => $path . '/templates',
-    ],
-  ];
+  return \Drupal::service(SelectifyHooks::class)->theme($existing, $type, $theme, $path);
 }
 
 /**
@@ -325,296 +244,49 @@ function _selectify_preprocess_widget(array &$variables) {
 /**
  * Implements hook_preprocess_form_element() for Selectify.
  */
+#[LegacyHook]
 function selectify_preprocess_form_element(array &$variables): void {
-  /** @var \Drupal\selectify\Service\SelectifyHelper $helper */
-  $helper = \Drupal::service('selectify.helper');
-  $config = $helper->getRadioCheckboxConfig();
-
-  if ($config === FALSE) {
-    return;
-  }
-
-  if (!isset($variables['element']['#type'])) {
-    return;
-  }
-
-  $type = $variables['element']['#type'];
-
-  if ($type === 'radio' && ($config['enable_radio'] === TRUE)) {
-    $variables['attributes']['class'][] = 'selectify-form-item selectify-radio-wrapper';
-  }
-  elseif ($type === 'checkbox' && ($config['enable_checkbox'] === TRUE)) {
-    $variables['attributes']['class'][] = 'selectify-form-item selectify-checkbox-wrapper';
-  }
+  \Drupal::service(SelectifyHooks::class)->preprocessFormElement($variables);
 }
 
 /**
  * Implements hook_preprocess_input() for Selectify.
  */
+#[LegacyHook]
 function selectify_preprocess_input(array &$variables): void {
-  /** @var \Drupal\selectify\Service\SelectifyHelper $helper */
-  $helper = \Drupal::service('selectify.helper');
-  $config = $helper->getRadioCheckboxConfig();
-
-  if ($config === FALSE) {
-    return;
-  }
-
-  if (!isset($variables['attributes']['type'])) {
-    return;
-  }
-
-  $type = $variables['attributes']['type'];
-
-  if ($type === 'radio' && ($config['enable_radio'] === TRUE)) {
-    $variables['attributes']['class'][] = 'selectify-radio';
-  }
-  elseif ($type === 'checkbox' && ($config['enable_checkbox'] === TRUE)) {
-    $variables['attributes']['class'][] = 'selectify-checkbox';
-  }
+  \Drupal::service(SelectifyHooks::class)->preprocessInput($variables);
 }
 
 /**
  * Implements hook_page_attachments() for Selectify.
  */
+#[LegacyHook]
 function selectify_page_attachments(array &$attachments) {
-  /** @var \Drupal\selectify\Service\SelectifyHelper $helper */
-  $helper = \Drupal::service('selectify.helper');
-  $config = $helper->getRadioCheckboxConfig();
-
-  if ($config === FALSE) {
-    return;
-  }
-
-  if (($config['enable_checkbox'] === TRUE) || ($config['enable_radio'] === TRUE)) {
-    $attachments['#attached']['library'][] = 'selectify/selectify-base';
-    $attachments['#attached']['library'][] = 'selectify/selectify-radio-checkbox';
-
-    $selectifyConfig = $helper->getConfig();
-    $isAdmin = $helper->isAdminRoute();
-    $selected_accent = $isAdmin ? $selectifyConfig->get('admin_accent_color') : $config['accent_color'];
-    $accent_color_mode = $isAdmin
-      ? ($selectifyConfig->get('admin_accent_color_mode') ?: 'light')
-      : ($selectifyConfig->get('accent_color_mode') ?: 'light');
-
-    if (!empty($selected_accent) && $selected_accent !== 'none' && $accent_color_mode !== 'none') {
-      $attachments['#attached']['library'][] = "selectify/selectify-color-{$selected_accent}-{$accent_color_mode}";
-    }
-  }
+  \Drupal::service(SelectifyHooks::class)->pageAttachments($attachments);
 }
 
 /**
  * Implements hook_page_attachments_alter().
  */
+#[LegacyHook]
 function selectify_page_attachments_alter(array &$attachments): void {
-  /** @var \Drupal\selectify\Service\SelectifyHelper $helper */
-  $helper = \Drupal::service('selectify.helper');
-
-  // Check if current page is in the disabled pages list.
-  if ($helper->isPageDisabled()) {
-    if (isset($attachments['#attached']['library'])) {
-      foreach ($attachments['#attached']['library'] as $key => $library) {
-        if (is_string($library) && strpos($library, 'selectify/') === 0) {
-          unset($attachments['#attached']['library'][$key]);
-        }
-      }
-    }
-    if (isset($attachments['#attached']['drupalSettings']['selectify'])) {
-      unset($attachments['#attached']['drupalSettings']['selectify']);
-    }
-    return;
-  }
-
-  // Load configuration.
-  $selectifyConfig = $helper->getConfig();
-
-  // Attach radio-checkbox library and inject CSS variables if enabled.
-  $radioEnabled = (bool) $selectifyConfig->get('enable_radio');
-  $checkboxEnabled = (bool) $selectifyConfig->get('enable_checkbox');
-
-  if ($radioEnabled || $checkboxEnabled) {
-    $attachments['#attached']['library'][] = 'selectify/selectify-radio-checkbox';
-
-    /** @var \Drupal\selectify\Service\SelectifyStyleService $styleService */
-    $styleService = \Drupal::service('selectify.style_service');
-    $inlineCSS = $styleService->getConfiguredInlineCss();
-
-    if (!empty($inlineCSS)) {
-      $attachments['#attached']['html_head'][] = [
-        [
-          '#tag' => 'style',
-          '#value' => $inlineCSS,
-          '#attributes' => ['id' => 'selectify-inline-variables'],
-        ],
-        'selectify_inline_variables',
-      ];
-    }
-
-    $isAdmin = $helper->isAdminRoute();
-    $accentColor = $isAdmin
-      ? $selectifyConfig->get('admin_accent_color')
-      : $selectifyConfig->get('accent_color');
-    $accentColorMode = $isAdmin
-      ? ($selectifyConfig->get('admin_accent_color_mode') ?? 'light')
-      : ($selectifyConfig->get('accent_color_mode') ?? 'light');
-
-    if (!empty($accentColor) && $accentColor !== 'none' && $accentColorMode !== 'none') {
-      $attachments['#attached']['library'][] = "selectify/selectify-color-{$accentColor}-{$accentColorMode}";
-    }
-  }
-
-  // Gin theme integration.
-  if ($helper->isBaseTheme('gin')) {
-    $attachments['#attached']['library'][] = 'selectify/selectify-gin-base';
-
-    $adminColorMode = $selectifyConfig->get('admin_accent_color_mode') ?? 'light';
-    if ($adminColorMode === 'dark') {
-      $attachments['#attached']['library'][] = 'selectify/selectify-gin-dark';
-    }
-    elseif ($adminColorMode === 'light') {
-      $attachments['#attached']['library'][] = 'selectify/selectify-gin-light';
-    }
-  }
-
-  // Claro theme integration.
-  if ($helper->isBaseTheme('claro')) {
-    $attachments['#attached']['library'][] = 'selectify/selectify-claro';
-  }
-
-  // Olivero theme integration.
-  if ($helper->isBaseTheme('olivero')) {
-    $attachments['#attached']['library'][] = 'selectify/selectify-olivero';
-  }
+  \Drupal::service(SelectifyHooks::class)->pageAttachmentsAlter($attachments);
 }
 
 /**
  * Implements hook_preprocess_container().
  */
+#[LegacyHook]
 function selectify_preprocess_container(array &$variables): void {
-  if (empty($variables['attributes'])) {
-    return;
-  }
-
-  $attr = $variables['attributes'];
-  $classes = [];
-
-  // Normalize classes to a clean string[] in all cases.
-  if ($attr instanceof Attribute) {
-    $classes = $attr->offsetGet('class') ?? [];
-  }
-  elseif (is_array($attr) && array_key_exists('class', $attr)) {
-    $classes = $attr['class'];
-  }
-  elseif (isset($attr['class'])) {
-    $classes = $attr['class'];
-  }
-
-  if (is_string($classes)) {
-    $classes = preg_split('/\s+/', trim($classes), -1, PREG_SPLIT_NO_EMPTY) ?: [];
-  }
-  elseif (!is_array($classes)) {
-    $classes = [];
-  }
-
-  if (!$classes) {
-    return;
-  }
-
-  foreach ($classes as $class) {
-    if (is_string($class) && str_starts_with($class, 'field--widget-selectify-')) {
-      $max = $variables['element']['widget'][0]['#attributes']['data-max-selections'] ?? NULL;
-      $new_class = ($max === '1') ? 'selectify-single' : 'selectify-multi';
-
-      if ($attr instanceof Attribute) {
-        $variables['attributes']->addClass($new_class);
-      }
-      else {
-        if (!isset($variables['attributes']['class']) || !is_array($variables['attributes']['class'])) {
-          $variables['attributes']['class'] = (array) ($variables['attributes']['class'] ?? []);
-        }
-        $variables['attributes']['class'][] = $new_class;
-      }
-      break;
-    }
-  }
+  \Drupal::service(SelectifyHooks::class)->preprocessContainer($variables);
 }
 
 /**
  * Implements hook_preprocess_views_exposed_form() for Selectify.
  */
+#[LegacyHook]
 function selectify_preprocess_views_exposed_form(&$variables) {
-  $form = &$variables['form'];
-
-  /** @var \Drupal\selectify\Service\SelectifyHelper $helper */
-  $helper = \Drupal::service('selectify.helper');
-
-  if ($helper->isPageDisabled()) {
-    return;
-  }
-
-  if ($helper->isDisabledOnAdminRoute()) {
-    return;
-  }
-
-  // Add the 'selectify-views' class to all select elements.
-  foreach ($form as &$element) {
-    if (is_array($element) && isset($element['#type']) && $element['#type'] === 'select') {
-      $element['#attributes'] = $element['#attributes'] ?? [];
-      $element['#attributes']['class'] = $element['#attributes']['class'] ?? [];
-      $element['#attributes']['class'][] = 'selectify-views';
-    }
-  }
-  unset($element);
-
-  // Retrieve the Selectify configuration.
-  $config = $helper->getViewsConfig();
-  $apply_site_wide = $config['apply_site_wide'];
-  $global_widget = $config['global_widget'];
-  $widget_map = $config['widget_map'];
-  $clean_form_id = $helper->normalizeFormId($form);
-
-  // Determine widget to apply.
-  $widget_to_apply = NULL;
-  if ($apply_site_wide) {
-    $widget_to_apply = $global_widget;
-  }
-  else {
-    foreach ($widget_map as $widget_data) {
-      $stored_id = $widget_data['exposed_form_id'] ?? '';
-      $normalized_id = str_replace('_', '-', $stored_id);
-
-      if (!empty($normalized_id) && $normalized_id === $clean_form_id) {
-        $widget_to_apply = $widget_data['widget'];
-        break;
-      }
-    }
-  }
-
-  // Allow other modules to alter the widget selection.
-  if (!empty($widget_to_apply)) {
-    $view = $variables['view'] ?? NULL;
-    $context = [
-      'form_id' => $clean_form_id,
-      'form' => &$form,
-      'integration_type' => 'views',
-      'view_name' => ($view && is_object($view) && method_exists($view, 'id')) ? $view->id() : NULL,
-      'display_id' => ($view && is_object($view) && isset($view->current_display)) ? $view->current_display : NULL,
-    ];
-    \Drupal::moduleHandler()->alter('selectify_widget', $widget_to_apply, $context);
-  }
-
-  // Apply global or per-view widget.
-  if (!empty($widget_to_apply)) {
-    $helper->applyWidgetToForm($form, $widget_to_apply);
-  }
-
-  // Add single/multi class to each select wrapper.
-  foreach ($form as &$element) {
-    if (is_array($element) && isset($element['#type']) && $element['#type'] === 'select') {
-      $is_multiple = isset($element['#multiple']) && $element['#multiple'] === TRUE;
-      $element['#wrapper_attributes']['class'][] = $is_multiple ? 'selectify-multi' : 'selectify-single';
-    }
-  }
+  \Drupal::service(SelectifyHooks::class)->preprocessViewsExposedForm($variables);
 }
 
 /**
@@ -624,100 +296,9 @@ function selectify_preprocess_views_exposed_form(&$variables) {
  * Skips Views exposed forms (handled by hook_preprocess_views_exposed_form)
  * and elements already styled by field widgets.
  */
+#[LegacyHook]
 function selectify_form_alter(array &$form, FormStateInterface $form_state, string $form_id): void {
-  /** @var \Drupal\selectify\Service\SelectifyHelper $helper */
-  $helper = \Drupal::service('selectify.helper');
-
-  // Bail early if the feature is disabled or on a disabled admin route.
-  $config = $helper->getFormApiConfig();
-  if ($config === FALSE) {
-    return;
-  }
-
-  // Bail if page is in the disabled pages list.
-  if ($helper->isPageDisabled()) {
-    return;
-  }
-
-  // Skip Views exposed forms — handled by hook_preprocess_views_exposed_form().
-  if (str_starts_with($form_id, 'views_exposed_form')) {
-    return;
-  }
-
-  // Skip Webform submission forms if the selectify_webform submodule handles
-  // them.
-  if (str_starts_with($form_id, 'webform_submission_') &&
-      \Drupal::moduleHandler()->moduleExists('selectify_webform')) {
-    return;
-  }
-
-  // Check built-in blocklist (always-excluded admin forms: Field UI,
-  // Views UI, Layout Builder, etc.) that break with custom selects.
-  if ($helper->isFormExcluded($form_id)) {
-    return;
-  }
-
-  // Allow other modules to prevent Selectify from applying to this form.
-  $applicable = TRUE;
-  $context = [
-    'form_id' => $form_id,
-    'form' => &$form,
-    'integration_type' => 'form_api',
-  ];
-  \Drupal::moduleHandler()->alter('selectify_is_applicable', $applicable, $context);
-  if (!$applicable) {
-    return;
-  }
-
-  // Also skip any form element that has an #ajax callback on a select,
-  // since custom select replacement can break AJAX triggers.
-  // (Individual selects with #ajax are skipped in the recursive walker.)
-  // Record this form in the discovered list if it contains any eligible
-  // (non-AJAX, non-field-widget) selects. This powers the Views-like
-  // configuration UI for Form API.
-  if (_selectify_form_api_has_eligible_select($form)) {
-    _selectify_form_api_record_discovered_form($form_id);
-  }
-
-  // Compute accent color library once for this request.
-  $selectifyConfig = $helper->getConfig();
-  $isAdmin = $helper->isAdminRoute();
-  $accent = $isAdmin
-    ? ($selectifyConfig->get('admin_accent_color') ?: 'neutral')
-    : ($selectifyConfig->get('accent_color') ?: 'neutral');
-  $accent_mode = $isAdmin
-    ? ($selectifyConfig->get('admin_accent_color_mode') ?: 'light')
-    : ($selectifyConfig->get('accent_color_mode') ?: 'light');
-  $accent_library = NULL;
-  if (!empty($accent) && $accent !== 'none' && $accent_mode !== 'none') {
-    $accent_library = "selectify/selectify-color-{$accent}-{$accent_mode}";
-  }
-
-  // CSS selector rules: apply per-element targeting first. Matched selects
-  // receive their rule's widget and are marked with selectify-apply-* so
-  // the per-form widget step (below) will not override them.
-  $css_rules = $config['css_selector_rules'] ?? [];
-  if (!empty($css_rules)) {
-    _selectify_apply_css_selector_rules($form, $helper, $css_rules, $accent_library);
-  }
-
-  // Form API does not have a global "apply to all forms" mode. Only forms
-  // explicitly configured in the per-form widget map will be styled.
-  $widget = $config['widget_map'][$form_id] ?? 'none';
-
-  // Allow other modules to alter the widget selection.
-  $context = [
-    'form_id' => $form_id,
-    'form' => &$form,
-    'integration_type' => 'form_api',
-  ];
-  \Drupal::moduleHandler()->alter('selectify_widget', $widget, $context);
-
-  if (empty($widget) || $widget === 'none') {
-    return;
-  }
-
-  _selectify_apply_to_form_selects($form, $helper, $widget, $accent_library);
+  \Drupal::service(SelectifyHooks::class)->formAlter($form, $form_state, $form_id);
 }
 
 /**
diff --git a/selectify.services.yml b/selectify.services.yml
index 912b881..a74a34c 100644
--- a/selectify.services.yml
+++ b/selectify.services.yml
@@ -23,3 +23,7 @@ services:
       - '@current_route_match'
 
   Drupal\selectify\Service\SelectifyHelperInterface: '@selectify.helper'
+
+  Drupal\selectify\Hook\SelectifyHooks:
+    class: Drupal\selectify\Hook\SelectifyHooks
+    autowire: true
diff --git a/src/Hook/SelectifyHooks.php b/src/Hook/SelectifyHooks.php
new file mode 100644
index 0000000..a1c0906
--- /dev/null
+++ b/src/Hook/SelectifyHooks.php
@@ -0,0 +1,465 @@
+<?php
+
+namespace Drupal\selectify\Hook;
+
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Template\Attribute;
+use Drupal\Core\Hook\Attribute\Hook;
+/**
+ * Hook implementations for selectify.
+ */
+class SelectifyHooks
+{
+    /**
+     * Implements hook_preprocess_html().
+     */
+    #[Hook('preprocess_html')]
+    public static function preprocessHtml(&$variables)
+    {
+        /** @var \Drupal\selectify\Service\SelectifyHelper $helper */
+        $helper = \Drupal::service('selectify.helper');
+        // Check if current page is in the disabled pages list.
+        if ($helper->isPageDisabled()) {
+            return;
+        }
+        $config = \Drupal::config('selectify.settings');
+        $radioEnabled = $config->get('enable_radio');
+        $checkboxEnabled = $config->get('enable_checkbox');
+        if (!$radioEnabled && !$checkboxEnabled) {
+            return;
+        }
+        /** @var \Drupal\selectify\Service\SelectifyStyleService $styleService */
+        $styleService = \Drupal::service('selectify.style_service');
+        $bodyClasses = $styleService->getBodyClasses();
+        if (!empty($bodyClasses)) {
+            $classes = explode(' ', $bodyClasses);
+            foreach ($classes as $class) {
+                $variables['attributes']['class'][] = $class;
+            }
+        }
+        // Add selectify-color-disabled class when the active color mode is none.
+        $colorMode = $helper->isAdminRoute() ? $config->get('admin_accent_color_mode') ?? 'light' : $config->get('accent_color_mode') ?? 'light';
+        if ($colorMode === 'none') {
+            $variables['attributes']['class'][] = 'selectify-color-disabled';
+        }
+    }
+    /**
+     * Implements hook_help().
+     */
+    #[Hook('help')]
+    public static function help($route_name, \Drupal\Core\Routing\RouteMatchInterface $route_match): ?string
+    {
+        if ($route_name === 'help.page.selectify') {
+            return _selectify_render_readme();
+        }
+        return NULL;
+    }
+    /**
+     * Implements hook_theme_suggestions_select_alter().
+     */
+    #[Hook('theme_suggestions_select_alter')]
+    public static function themeSuggestionsSelectAlter(array &$suggestions, array $variables)
+    {
+        if (empty($variables['element']['#attributes']['class'])) {
+            return;
+        }
+        $classes = (array) $variables['element']['#attributes']['class'];
+        foreach ($suggestions as $key => $suggestion) {
+            if (strpos($suggestion, 'form_element') !== FALSE) {
+                unset($suggestions[$key]);
+            }
+        }
+        $class_to_suggestion = [
+            'selectify-apply-dropdown' => 'select__selectify_dropdown',
+            'selectify-apply-checkbox' => 'select__selectify_dropdown_checkbox',
+            'selectify-apply-searchable' => 'select__selectify_dropdown_searchable',
+            'selectify-apply-tags' => 'select__selectify_dropdown_tags',
+            'selectify-apply-dual' => 'select__selectify_dual',
+        ];
+        foreach ($class_to_suggestion as $class => $suggestion) {
+            if (in_array($class, $classes)) {
+                $suggestions[] = $suggestion;
+            }
+        }
+    }
+    /**
+     * Implements hook_theme().
+     */
+    #[Hook('theme')]
+    public static function theme($existing, $type, $theme, $path)
+    {
+        return [
+            'select__selectify_dropdown' => [
+                'base hook' => 'select',
+                'template' => 'select--selectify-dropdown',
+                'path' => $path . '/templates',
+            ],
+            'select__selectify_dropdown_checkbox' => [
+                'base hook' => 'select',
+                'template' => 'select--selectify-dropdown-checkbox',
+                'path' => $path . '/templates',
+            ],
+            'select__selectify_dropdown_searchable' => [
+                'base hook' => 'select',
+                'template' => 'select--selectify-dropdown-searchable',
+                'path' => $path . '/templates',
+            ],
+            'select__selectify_dropdown_tags' => [
+                'base hook' => 'select',
+                'template' => 'select--selectify-dropdown-tags',
+                'path' => $path . '/templates',
+            ],
+            'select__selectify_dual' => [
+                'base hook' => 'select',
+                'template' => 'select--selectify-dual',
+                'path' => $path . '/templates',
+            ],
+        ];
+    }
+    /**
+     * Implements hook_preprocess_form_element() for Selectify.
+     */
+    #[Hook('preprocess_form_element')]
+    public static function preprocessFormElement(array &$variables): void
+    {
+        /** @var \Drupal\selectify\Service\SelectifyHelper $helper */
+        $helper = \Drupal::service('selectify.helper');
+        $config = $helper->getRadioCheckboxConfig();
+        if ($config === FALSE) {
+            return;
+        }
+        if (!isset($variables['element']['#type'])) {
+            return;
+        }
+        $type = $variables['element']['#type'];
+        if ($type === 'radio' && $config['enable_radio'] === TRUE) {
+            $variables['attributes']['class'][] = 'selectify-form-item selectify-radio-wrapper';
+        } elseif ($type === 'checkbox' && $config['enable_checkbox'] === TRUE) {
+            $variables['attributes']['class'][] = 'selectify-form-item selectify-checkbox-wrapper';
+        }
+    }
+    /**
+     * Implements hook_preprocess_input() for Selectify.
+     */
+    #[Hook('preprocess_input')]
+    public static function preprocessInput(array &$variables): void
+    {
+        /** @var \Drupal\selectify\Service\SelectifyHelper $helper */
+        $helper = \Drupal::service('selectify.helper');
+        $config = $helper->getRadioCheckboxConfig();
+        if ($config === FALSE) {
+            return;
+        }
+        if (!isset($variables['attributes']['type'])) {
+            return;
+        }
+        $type = $variables['attributes']['type'];
+        if ($type === 'radio' && $config['enable_radio'] === TRUE) {
+            $variables['attributes']['class'][] = 'selectify-radio';
+        } elseif ($type === 'checkbox' && $config['enable_checkbox'] === TRUE) {
+            $variables['attributes']['class'][] = 'selectify-checkbox';
+        }
+    }
+    /**
+     * Implements hook_page_attachments() for Selectify.
+     */
+    #[Hook('page_attachments')]
+    public static function pageAttachments(array &$attachments)
+    {
+        /** @var \Drupal\selectify\Service\SelectifyHelper $helper */
+        $helper = \Drupal::service('selectify.helper');
+        $config = $helper->getRadioCheckboxConfig();
+        if ($config === FALSE) {
+            return;
+        }
+        if ($config['enable_checkbox'] === TRUE || $config['enable_radio'] === TRUE) {
+            $attachments['#attached']['library'][] = 'selectify/selectify-base';
+            $attachments['#attached']['library'][] = 'selectify/selectify-radio-checkbox';
+            $selectifyConfig = $helper->getConfig();
+            $isAdmin = $helper->isAdminRoute();
+            $selected_accent = $isAdmin ? $selectifyConfig->get('admin_accent_color') : $config['accent_color'];
+            $accent_color_mode = $isAdmin ? $selectifyConfig->get('admin_accent_color_mode') ?: 'light' : ($selectifyConfig->get('accent_color_mode') ?: 'light');
+            if (!empty($selected_accent) && $selected_accent !== 'none' && $accent_color_mode !== 'none') {
+                $attachments['#attached']['library'][] = "selectify/selectify-color-{$selected_accent}-{$accent_color_mode}";
+            }
+        }
+    }
+    /**
+     * Implements hook_page_attachments_alter().
+     */
+    #[Hook('page_attachments_alter')]
+    public static function pageAttachmentsAlter(array &$attachments): void
+    {
+        /** @var \Drupal\selectify\Service\SelectifyHelper $helper */
+        $helper = \Drupal::service('selectify.helper');
+        // Check if current page is in the disabled pages list.
+        if ($helper->isPageDisabled()) {
+            if (isset($attachments['#attached']['library'])) {
+                foreach ($attachments['#attached']['library'] as $key => $library) {
+                    if (is_string($library) && strpos($library, 'selectify/') === 0) {
+                        unset($attachments['#attached']['library'][$key]);
+                    }
+                }
+            }
+            if (isset($attachments['#attached']['drupalSettings']['selectify'])) {
+                unset($attachments['#attached']['drupalSettings']['selectify']);
+            }
+            return;
+        }
+        // Load configuration.
+        $selectifyConfig = $helper->getConfig();
+        // Attach radio-checkbox library and inject CSS variables if enabled.
+        $radioEnabled = (bool) $selectifyConfig->get('enable_radio');
+        $checkboxEnabled = (bool) $selectifyConfig->get('enable_checkbox');
+        if ($radioEnabled || $checkboxEnabled) {
+            $attachments['#attached']['library'][] = 'selectify/selectify-radio-checkbox';
+            /** @var \Drupal\selectify\Service\SelectifyStyleService $styleService */
+            $styleService = \Drupal::service('selectify.style_service');
+            $inlineCSS = $styleService->getConfiguredInlineCss();
+            if (!empty($inlineCSS)) {
+                $attachments['#attached']['html_head'][] = [
+                    [
+                        '#tag' => 'style',
+                        '#value' => $inlineCSS,
+                        '#attributes' => [
+                            'id' => 'selectify-inline-variables',
+                        ],
+                    ],
+                    'selectify_inline_variables',
+                ];
+            }
+            $isAdmin = $helper->isAdminRoute();
+            $accentColor = $isAdmin ? $selectifyConfig->get('admin_accent_color') : $selectifyConfig->get('accent_color');
+            $accentColorMode = $isAdmin ? $selectifyConfig->get('admin_accent_color_mode') ?? 'light' : $selectifyConfig->get('accent_color_mode') ?? 'light';
+            if (!empty($accentColor) && $accentColor !== 'none' && $accentColorMode !== 'none') {
+                $attachments['#attached']['library'][] = "selectify/selectify-color-{$accentColor}-{$accentColorMode}";
+            }
+        }
+        // Gin theme integration.
+        if ($helper->isBaseTheme('gin')) {
+            $attachments['#attached']['library'][] = 'selectify/selectify-gin-base';
+            $adminColorMode = $selectifyConfig->get('admin_accent_color_mode') ?? 'light';
+            if ($adminColorMode === 'dark') {
+                $attachments['#attached']['library'][] = 'selectify/selectify-gin-dark';
+            } elseif ($adminColorMode === 'light') {
+                $attachments['#attached']['library'][] = 'selectify/selectify-gin-light';
+            }
+        }
+        // Claro theme integration.
+        if ($helper->isBaseTheme('claro')) {
+            $attachments['#attached']['library'][] = 'selectify/selectify-claro';
+        }
+        // Olivero theme integration.
+        if ($helper->isBaseTheme('olivero')) {
+            $attachments['#attached']['library'][] = 'selectify/selectify-olivero';
+        }
+    }
+    /**
+     * Implements hook_preprocess_container().
+     */
+    #[Hook('preprocess_container')]
+    public static function preprocessContainer(array &$variables): void
+    {
+        if (empty($variables['attributes'])) {
+            return;
+        }
+        $attr = $variables['attributes'];
+        $classes = [
+        ];
+        // Normalize classes to a clean string[] in all cases.
+        if ($attr instanceof \Drupal\Core\Template\Attribute) {
+            $classes = $attr->offsetGet('class') ?? [
+            ];
+        } elseif (is_array($attr) && array_key_exists('class', $attr)) {
+            $classes = $attr['class'];
+        } elseif (isset($attr['class'])) {
+            $classes = $attr['class'];
+        }
+        if (is_string($classes)) {
+            $classes = preg_split('/\s+/', trim($classes), -1, PREG_SPLIT_NO_EMPTY) ?: [
+            ];
+        } elseif (!is_array($classes)) {
+            $classes = [
+            ];
+        }
+        if (!$classes) {
+            return;
+        }
+        foreach ($classes as $class) {
+            if (is_string($class) && str_starts_with($class, 'field--widget-selectify-')) {
+                $max = $variables['element']['widget'][0]['#attributes']['data-max-selections'] ?? NULL;
+                $new_class = $max === '1' ? 'selectify-single' : 'selectify-multi';
+                if ($attr instanceof \Drupal\Core\Template\Attribute) {
+                    $variables['attributes']->addClass($new_class);
+                } else {
+                    if (!isset($variables['attributes']['class']) || !is_array($variables['attributes']['class'])) {
+                        $variables['attributes']['class'] = (array) ($variables['attributes']['class'] ?? [
+                        ]);
+                    }
+                    $variables['attributes']['class'][] = $new_class;
+                }
+                break;
+            }
+        }
+    }
+    /**
+     * Implements hook_preprocess_views_exposed_form() for Selectify.
+     */
+    #[Hook('preprocess_views_exposed_form')]
+    public static function preprocessViewsExposedForm(&$variables)
+    {
+        $form =& $variables['form'];
+        /** @var \Drupal\selectify\Service\SelectifyHelper $helper */
+        $helper = \Drupal::service('selectify.helper');
+        if ($helper->isPageDisabled()) {
+            return;
+        }
+        if ($helper->isDisabledOnAdminRoute()) {
+            return;
+        }
+        // Add the 'selectify-views' class to all select elements.
+        foreach ($form as &$element) {
+            if (is_array($element) && isset($element['#type']) && $element['#type'] === 'select') {
+                $element['#attributes'] = $element['#attributes'] ?? [
+                ];
+                $element['#attributes']['class'] = $element['#attributes']['class'] ?? [
+                ];
+                $element['#attributes']['class'][] = 'selectify-views';
+            }
+        }
+        unset($element);
+        // Retrieve the Selectify configuration.
+        $config = $helper->getViewsConfig();
+        $apply_site_wide = $config['apply_site_wide'];
+        $global_widget = $config['global_widget'];
+        $widget_map = $config['widget_map'];
+        $clean_form_id = $helper->normalizeFormId($form);
+        // Determine widget to apply.
+        $widget_to_apply = NULL;
+        if ($apply_site_wide) {
+            $widget_to_apply = $global_widget;
+        } else {
+            foreach ($widget_map as $widget_data) {
+                $stored_id = $widget_data['exposed_form_id'] ?? '';
+                $normalized_id = str_replace('_', '-', $stored_id);
+                if (!empty($normalized_id) && $normalized_id === $clean_form_id) {
+                    $widget_to_apply = $widget_data['widget'];
+                    break;
+                }
+            }
+        }
+        // Allow other modules to alter the widget selection.
+        if (!empty($widget_to_apply)) {
+            $view = $variables['view'] ?? NULL;
+            $context = [
+                'form_id' => $clean_form_id,
+                'form' => &$form,
+                'integration_type' => 'views',
+                'view_name' => $view && is_object($view) && method_exists($view, 'id') ? $view->id() : NULL,
+                'display_id' => $view && is_object($view) && isset($view->current_display) ? $view->current_display : NULL,
+            ];
+            \Drupal::moduleHandler()->alter('selectify_widget', $widget_to_apply, $context);
+        }
+        // Apply global or per-view widget.
+        if (!empty($widget_to_apply)) {
+            $helper->applyWidgetToForm($form, $widget_to_apply);
+        }
+        // Add single/multi class to each select wrapper.
+        foreach ($form as &$element) {
+            if (is_array($element) && isset($element['#type']) && $element['#type'] === 'select') {
+                $is_multiple = isset($element['#multiple']) && $element['#multiple'] === TRUE;
+                $element['#wrapper_attributes']['class'][] = $is_multiple ? 'selectify-multi' : 'selectify-single';
+            }
+        }
+    }
+    /**
+     * Implements hook_form_alter().
+     *
+     * Applies Selectify widgets to plain Form API <select> elements.
+     * Skips Views exposed forms (handled by hook_preprocess_views_exposed_form)
+     * and elements already styled by field widgets.
+     */
+    #[Hook('form_alter')]
+    public static function formAlter(array &$form, \Drupal\Core\Form\FormStateInterface $form_state, string $form_id): void
+    {
+        /** @var \Drupal\selectify\Service\SelectifyHelper $helper */
+        $helper = \Drupal::service('selectify.helper');
+        // Bail early if the feature is disabled or on a disabled admin route.
+        $config = $helper->getFormApiConfig();
+        if ($config === FALSE) {
+            return;
+        }
+        // Bail if page is in the disabled pages list.
+        if ($helper->isPageDisabled()) {
+            return;
+        }
+        // Skip Views exposed forms — handled by hook_preprocess_views_exposed_form().
+        if (str_starts_with($form_id, 'views_exposed_form')) {
+            return;
+        }
+        // Skip Webform submission forms if the selectify_webform submodule handles
+        // them.
+        if (str_starts_with($form_id, 'webform_submission_') && \Drupal::moduleHandler()->moduleExists('selectify_webform')) {
+            return;
+        }
+        // Check built-in blocklist (always-excluded admin forms: Field UI,
+        // Views UI, Layout Builder, etc.) that break with custom selects.
+        if ($helper->isFormExcluded($form_id)) {
+            return;
+        }
+        // Allow other modules to prevent Selectify from applying to this form.
+        $applicable = TRUE;
+        $context = [
+            'form_id' => $form_id,
+            'form' => &$form,
+            'integration_type' => 'form_api',
+        ];
+        \Drupal::moduleHandler()->alter('selectify_is_applicable', $applicable, $context);
+        if (!$applicable) {
+            return;
+        }
+        // Also skip any form element that has an #ajax callback on a select,
+        // since custom select replacement can break AJAX triggers.
+        // (Individual selects with #ajax are skipped in the recursive walker.)
+        // Record this form in the discovered list if it contains any eligible
+        // (non-AJAX, non-field-widget) selects. This powers the Views-like
+        // configuration UI for Form API.
+        if (_selectify_form_api_has_eligible_select($form)) {
+            _selectify_form_api_record_discovered_form($form_id);
+        }
+        // Compute accent color library once for this request.
+        $selectifyConfig = $helper->getConfig();
+        $isAdmin = $helper->isAdminRoute();
+        $accent = $isAdmin ? $selectifyConfig->get('admin_accent_color') ?: 'neutral' : ($selectifyConfig->get('accent_color') ?: 'neutral');
+        $accent_mode = $isAdmin ? $selectifyConfig->get('admin_accent_color_mode') ?: 'light' : ($selectifyConfig->get('accent_color_mode') ?: 'light');
+        $accent_library = NULL;
+        if (!empty($accent) && $accent !== 'none' && $accent_mode !== 'none') {
+            $accent_library = "selectify/selectify-color-{$accent}-{$accent_mode}";
+        }
+        // CSS selector rules: apply per-element targeting first. Matched selects
+        // receive their rule's widget and are marked with selectify-apply-* so
+        // the per-form widget step (below) will not override them.
+        $css_rules = $config['css_selector_rules'] ?? [
+        ];
+        if (!empty($css_rules)) {
+            _selectify_apply_css_selector_rules($form, $helper, $css_rules, $accent_library);
+        }
+        // Form API does not have a global "apply to all forms" mode. Only forms
+        // explicitly configured in the per-form widget map will be styled.
+        $widget = $config['widget_map'][$form_id] ?? 'none';
+        // Allow other modules to alter the widget selection.
+        $context = [
+            'form_id' => $form_id,
+            'form' => &$form,
+            'integration_type' => 'form_api',
+        ];
+        \Drupal::moduleHandler()->alter('selectify_widget', $widget, $context);
+        if (empty($widget) || $widget === 'none') {
+            return;
+        }
+        _selectify_apply_to_form_selects($form, $helper, $widget, $accent_library);
+    }
+}
diff --git a/src/TwigExtension/SelectifyTwigExtension.php b/src/TwigExtension/SelectifyTwigExtension.php
index c197da6..df86ed1 100644
--- a/src/TwigExtension/SelectifyTwigExtension.php
+++ b/src/TwigExtension/SelectifyTwigExtension.php
@@ -17,7 +17,7 @@ class SelectifyTwigExtension extends AbstractExtension {
    */
   public function getFilters(): array {
     return [
-      new TwigFilter('selectify_clean_id', [$this, 'cleanId']),
+      new TwigFilter('selectify_clean_id', $this->cleanId(...)),
     ];
   }
 
diff --git a/tests/modules/selectify_test/selectify_test.module b/tests/modules/selectify_test/selectify_test.module
index 9d1147f..cb9662d 100644
--- a/tests/modules/selectify_test/selectify_test.module
+++ b/tests/modules/selectify_test/selectify_test.module
@@ -1,5 +1,12 @@
 <?php
 
+/**
+ * @file
+ */
+
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\selectify_test\Hook\SelectifyTestHooks;
+
 /**
  * @file
  * Test module implementing Selectify alter hooks for Kernel tests.
@@ -8,35 +15,23 @@
 /**
  * Implements hook_selectify_widget_alter().
  */
+#[LegacyHook]
 function selectify_test_selectify_widget_alter(string &$widget, array $context): void {
-  if ($context['integration_type'] === 'form_api' && $context['form_id'] === 'selectify_test_form') {
-    if ($widget === 'selectify_dropdown') {
-      $widget = 'selectify_searchable';
-    }
-  }
+  \Drupal::service(SelectifyTestHooks::class)->selectifyWidgetAlter($widget, $context);
 }
 
 /**
  * Implements hook_selectify_element_alter().
  */
+#[LegacyHook]
 function selectify_test_selectify_element_alter(array &$element, array $context): void {
-  $element['#attributes'] = $element['#attributes'] ?? [];
-  $element['#attributes']['data-selectify-test'] = 'altered';
-
-  if (isset($element['#type']) && $element['#type'] === 'select') {
-    $options = $element['#options'] ?? [];
-    if (count($options) <= 2) {
-      $element['#selectify_skip'] = TRUE;
-    }
-  }
+  \Drupal::service(SelectifyTestHooks::class)->selectifyElementAlter($element, $context);
 }
 
 /**
  * Implements hook_selectify_is_applicable_alter().
  */
+#[LegacyHook]
 function selectify_test_selectify_is_applicable_alter(bool &$applicable, array $context): void {
-  $request = \Drupal::request();
-  if ($request && $request->query && $request->query->get('disable_selectify') === '1') {
-    $applicable = FALSE;
-  }
+  \Drupal::service(SelectifyTestHooks::class)->selectifyIsApplicableAlter($applicable, $context);
 }
diff --git a/tests/modules/selectify_test/selectify_test.services.yml b/tests/modules/selectify_test/selectify_test.services.yml
new file mode 100644
index 0000000..e09158c
--- /dev/null
+++ b/tests/modules/selectify_test/selectify_test.services.yml
@@ -0,0 +1,5 @@
+
+services:
+  Drupal\selectify_test\Hook\SelectifyTestHooks:
+    class: Drupal\selectify_test\Hook\SelectifyTestHooks
+    autowire: true
diff --git a/tests/modules/selectify_test/src/Hook/SelectifyTestHooks.php b/tests/modules/selectify_test/src/Hook/SelectifyTestHooks.php
new file mode 100644
index 0000000..0f1e540
--- /dev/null
+++ b/tests/modules/selectify_test/src/Hook/SelectifyTestHooks.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\selectify_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for selectify_test.
+ */
+class SelectifyTestHooks {
+  /**
+   * @file
+   * Test module implementing Selectify alter hooks for Kernel tests.
+   */
+
+  /**
+   * Implements hook_selectify_widget_alter().
+   */
+  #[Hook('selectify_widget_alter')]
+  public static function selectifyWidgetAlter(string &$widget, array $context): void {
+    if ($context['integration_type'] === 'form_api' && $context['form_id'] === 'selectify_test_form') {
+      if ($widget === 'selectify_dropdown') {
+        $widget = 'selectify_searchable';
+      }
+    }
+  }
+
+  /**
+   * Implements hook_selectify_element_alter().
+   */
+  #[Hook('selectify_element_alter')]
+  public static function selectifyElementAlter(array &$element, array $context): void {
+    $element['#attributes'] = $element['#attributes'] ?? [];
+    $element['#attributes']['data-selectify-test'] = 'altered';
+    if (isset($element['#type']) && $element['#type'] === 'select') {
+      $options = $element['#options'] ?? [];
+      if (count($options) <= 2) {
+        $element['#selectify_skip'] = TRUE;
+      }
+    }
+  }
+
+  /**
+   * Implements hook_selectify_is_applicable_alter().
+   */
+  #[Hook('selectify_is_applicable_alter')]
+  public static function selectifyIsApplicableAlter(bool &$applicable, array $context): void {
+    $request = \Drupal::request();
+    if ($request && $request->query && $request->query->get('disable_selectify') === '1') {
+      $applicable = FALSE;
+    }
+  }
+
+}
diff --git a/tests/src/Functional/SelectifySettingsFormTest.php b/tests/src/Functional/SelectifySettingsFormTest.php
index 4528c9e..c64e899 100644
--- a/tests/src/Functional/SelectifySettingsFormTest.php
+++ b/tests/src/Functional/SelectifySettingsFormTest.php
@@ -4,6 +4,8 @@ declare(strict_types=1);
 
 namespace Drupal\Tests\selectify\Functional;
 
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
 use Drupal\Tests\BrowserTestBase;
 
 /**
@@ -11,6 +13,8 @@ use Drupal\Tests\BrowserTestBase;
  *
  * @group selectify
  */
+#[Group('selectify')]
+#[RunTestsInSeparateProcesses]
 class SelectifySettingsFormTest extends BrowserTestBase {
 
   /**
diff --git a/tests/src/Kernel/SelectifyFormAlterTest.php b/tests/src/Kernel/SelectifyFormAlterTest.php
index bed2982..62dafab 100644
--- a/tests/src/Kernel/SelectifyFormAlterTest.php
+++ b/tests/src/Kernel/SelectifyFormAlterTest.php
@@ -4,6 +4,8 @@ declare(strict_types=1);
 
 namespace Drupal\Tests\selectify\Kernel;
 
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
 use Drupal\Core\Form\FormState;
 use Drupal\KernelTests\KernelTestBase;
 
@@ -12,6 +14,8 @@ use Drupal\KernelTests\KernelTestBase;
  *
  * @group selectify
  */
+#[Group('selectify')]
+#[RunTestsInSeparateProcesses]
 class SelectifyFormAlterTest extends KernelTestBase {
 
   /**
diff --git a/tests/src/Kernel/SelectifyHelperFormApiTest.php b/tests/src/Kernel/SelectifyHelperFormApiTest.php
index 814284f..109ee92 100644
--- a/tests/src/Kernel/SelectifyHelperFormApiTest.php
+++ b/tests/src/Kernel/SelectifyHelperFormApiTest.php
@@ -4,6 +4,8 @@ declare(strict_types=1);
 
 namespace Drupal\Tests\selectify\Kernel;
 
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
 use Drupal\KernelTests\KernelTestBase;
 
 /**
@@ -11,6 +13,8 @@ use Drupal\KernelTests\KernelTestBase;
  *
  * @group selectify
  */
+#[Group('selectify')]
+#[RunTestsInSeparateProcesses]
 class SelectifyHelperFormApiTest extends KernelTestBase {
 
   /**
diff --git a/tests/src/Kernel/SelectifyViewsNoConflictTest.php b/tests/src/Kernel/SelectifyViewsNoConflictTest.php
index 10c0740..07e9d74 100644
--- a/tests/src/Kernel/SelectifyViewsNoConflictTest.php
+++ b/tests/src/Kernel/SelectifyViewsNoConflictTest.php
@@ -4,6 +4,8 @@ declare(strict_types=1);
 
 namespace Drupal\Tests\selectify\Kernel;
 
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
 use Drupal\Core\Form\FormState;
 use Drupal\KernelTests\KernelTestBase;
 
@@ -17,6 +19,8 @@ use Drupal\KernelTests\KernelTestBase;
  *
  * @group selectify
  */
+#[Group('selectify')]
+#[RunTestsInSeparateProcesses]
 class SelectifyViewsNoConflictTest extends KernelTestBase {
 
   /**
