Skip to content

Theme Hooks (OOP)

When to Use

You are writing hook_preprocess_HOOK(), hook_theme_suggestions_HOOK_alter(), hook_form_alter() or hook_page_attachments_alter() in a theme, and your minimum core is 11.3.0 or later. Themes support #[Hook] classes since 11.3.0 (change record 3551652); the .theme file is deprecated in 11.5.0 (unreleased at verification, change record 3581222).

Decision

If your theme supports... Put hooks in... Why
11.3.0 or later only src/Hook/*.php classes, no .theme file Claro ships no .theme file in 11.4.5. Change record 3581222: "Drupal supports themes without a .theme file on all versions greater than Drupal 11.3.0"
11.2 or earlier as well .theme file, procedural, with a #[LegacyHook] shim only if you also ship the class The theme collector landed in 11.3.0; older cores never see src/Hook/ in a theme
Anything, after 11.5.0 ships Classes, and #[ExtensionFileIsConverted] on the first function of .theme only if it must remain for cores below 11.3.0 Change record 3581222: the attribute only silences the deprecation; .theme files are no longer auto-loaded in Drupal 13. The attribute is not present in 11.4.5

Rules that differ from modules, from core/lib/Drupal/Core/Hook/ThemeHookCollectorPass.php and change record 3551652:

Rule Detail
Namespace Drupal\<theme>\Hook, files under src/Hook/, .php only
module: parameter Forbidden. LogicException("The 'module' parameter on the #[Hook] attribute is not allowed in themes. Found in $class.")
order: parameter Forbidden. LogicException("The 'order' parameter on the #[Hook] attribute is not allowed in themes. Found in $class.")
#[ReorderHook], #[RemoveHook] Forbidden, each with its own LogicException
Execution order Base theme hooks run before the active theme. Cannot be changed
Inactive themes Their hooks do not execute
Autowiring Same as modules: the class is registered as an autowired service named after itself
Services Themes cannot define services. Helper classes that are not hook classes must use \Drupal::service() or \Drupal::classResolver() (change record 3581222)
Procedural scan .theme, .inc, and theme-settings.php are still scanned. templates/ is skipped

Pattern

Preprocess with injected services, the shape of core/themes/olivero/src/Hook/OliveroPagePreprocessHooks.php:

namespace Drupal\my_theme\Hook;

use Drupal\Core\Extension\ThemeSettingsProvider;
use Drupal\Core\Hook\Attribute\Hook;

class MyThemePreprocessHooks {

  public function __construct(
    protected ThemeSettingsProvider $themeSettingsProvider,
  ) {}

  #[Hook('preprocess_html')]
  public function preprocessHtml(array &$variables): void {
    if ($this->themeSettingsProvider->getSetting('mobile_menu_all_widths') === 1) {
      $variables['attributes']['class'][] = 'is-always-mobile-nav';
    }
  }

}

Suggestion alter, from core/themes/claro/src/Hook/ClaroHooks.php:

#[Hook('theme_suggestions_form_element_alter')]
public function themeSuggestionsFormElementAlter(array &$suggestions, array $variables): void {
  if (!empty($variables['element']['#type'])) {
    $suggestions[] = 'form_element__' . $variables['element']['#type'];
  }
}

The stale core docblock

core/core.api.php and core/lib/Drupal/Core/Hook/Attribute/Hook.php both still say "Hooks implemented by themes must remain procedural." That sentence predates 11.3.0. ThemeHookCollectorPass and the src/Hook/ classes in Claro and Olivero are the working behavior; the docblock is stale.

Common Mistakes

  • Adding order: to a theme hook to run after the base theme → LogicException at container build. Base-before-active is fixed.
  • Expecting a sub-theme's src/Hook/ class to run when the parent is active → hooks in inactive themes do not execute.
  • Leaving helper functions in .theme after converting hooks → the file is deprecated in 11.5.0 and not auto-loaded in 13. Move helpers to a method on the hook class or a utility class.
  • Declaring services in a my_theme.services.yml → themes cannot define services. Only the hook classes themselves get autowired.

See Also

  • Procedural-Only Hooks | Procedural Scan Performance
  • Preprocess mechanics and variables: https://camoa.github.io/dev-guides/drupal/twig/preprocess-functions/, https://camoa.github.io/dev-guides/drupal/twig/adding-variables-preprocess/
  • Theme hook registration and suggestions: https://camoa.github.io/dev-guides/drupal/twig/theme-hooks-registration/, https://camoa.github.io/dev-guides/drupal/twig/template-suggestions/, https://camoa.github.io/dev-guides/drupal/render-api/theme-hooks-suggestions/
  • Change records: https://www.drupal.org/node/3551652, https://www.drupal.org/node/3581222
  • Reference: core/lib/Drupal/Core/Hook/ThemeHookCollectorPass.php, core/themes/claro/src/Hook/, core/themes/olivero/src/Hook/