Modifying existing forms with hook_form_alterfor Drupal 8 , 9 , 10 , and 11

Last updated :  

Imagine a colleague finishing a report and placing it in a shared tray marked "final draft — last chance to edit before it's printed." Anyone can walk up and cross something out, add a note in the margin, or change a heading — directly on that same physical page — before it goes to the printer, without ever asking the original author for permission. That's exactly what hook_form_alter() gives every module on your site: a chance to reach into any form, right before it renders, and change it.

What you'll learn in this lesson

  • What makes hook_form_alter() different from a "gathering" hook like hook_help()
  • Why &$form is passed by reference, and what that actually buys you
  • How to target one specific form instead of every form on the site

A real, working example

This is the exact pattern from Drupal's own Examples project — a hook implementation that rewrites the description text on the core login form's username field:

/**
 * Implements hook_form_alter().
 */
function hooks_example_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  // This is an example of what is known as an alter hook. The $form
  // parameter in this case represents an already complete Form API array
  // and our hook implementation is being given the opportunity to make
  // changes to the existing data structure before it's used.
  //
  // If this is the user login form, change the description text of the
  // username field.
  if ($form_id === 'user_login_form') {
    $form['name']['#description'] = t('This text has been altered by hooks_example_form_alter().');
  }
}

Run this in any enabled module and every login form on the site — core's, a theme's custom one, anything — shows the altered description, without a single line of the login form's own code ever changing.

Why &$form is passed by reference

You're not handed a copy of the form to inspect — you're handed the actual, live data structure, with permission to change it directly, right before Drupal renders it. That's what the & before $form means. Change $form['name']['#description'] inside your function, and the calling code's $form variable reflects that change immediately — no return value needed, which is unusual compared to most PHP functions you write.

$form_id: the guard clause you almost always need

hook_form_alter() fires for every single form rendered anywhere on the site — the login form, every node edit form, every module's configuration form, all of them. The if ($form_id === 'user_login_form') check above is not optional decoration; without it, your alter logic runs against every form and will eventually collide with something you never meant to touch.

A cleaner alternative: Drupal also lets you implement hook_form_FORM_ID_alter() — for example hooks_example_form_user_login_form_alter() — which Drupal only invokes for that one specific form ID. It's the exact same mechanism as hook_form_alter(), just with the target baked into the function name instead of an if-check in the body. Prefer it whenever you only care about one form.

Quick check: what would happen if you deployed the code above but accidentally left off the if ($form_id === 'user_login_form') check? (Answer: the description-rewriting line would try to run against every form's name element sitewide — on forms that don't have a name field at all it would create one, and on forms where name means something else entirely, you'd silently corrupt unrelated functionality.)

Key takeaways

  • hook_form_alter(&$form, FormStateInterface $form_state, $form_id) fires for every form on the site — always guard with an if check on $form_id, or use hook_form_FORM_ID_alter() instead.
  • $form is passed by reference — modify it directly, don't return a new value.
  • Alter hooks are the general pattern for "receive an already-assembled data structure and get a chance to change it before use" — the same idea applies well beyond forms.

Coming up next

Forms aren't the only fully-assembled data structure you can intercept before Drupal uses it. Next: Implementing hook_entity_type_alter, where the same alter-hook pattern reaches into how entity types themselves are defined.