Injecting new fields via form alter hooksfor Drupal 8 , 9 , 10 , and 11

Last updated :  

Every alter hook so far has changed a property on a field that already existed. This lesson does something bigger: injecting a brand-new element into $form that the original form's own code never defined at all — exactly the trick behind lesson 57's earlier example, which added a whole phone_number field to the user registration form from outside that form's own class.

What you'll learn in this lesson

  • How to add a completely new render array element inside an alter hook
  • Why the key you choose for the new element matters
  • How to control where the new element appears using #weight

The pattern

/**
 * Implements hook_form_FORM_ID_alter() for user_register_form.
 */
function mymodule_form_user_register_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state) {
  $form['phone_number'] = [
    '#type' => 'textfield',
    '#title' => t('Phone Number'),
    '#required' => TRUE,
    '#description' => t('Enter your phone number.'),
    '#weight' => 10,
  ];
}

How it works

A new array key is a new element

Render arrays don't distinguish between "elements the form's own code defined" and "elements you added afterward" — they're both just keys in the same PHP array. Assigning $form['phone_number'] = [...] to a key that didn't exist before creates it, exactly the same as if the original buildForm() method had written that line itself.

Pick a key that won't collide

Namespace your added keys. If another module (or a future version of the base form itself) also adds a key named phone_number, one of you silently overwrites the other. A common real-world convention is to prefix injected keys with your module's machine name, e.g. $form['mymodule_phone_number'], to make a collision effectively impossible.

#weight controls where it lands

'#weight' => 10,

Without an explicit #weight, a newly added element typically renders after every element that already had a weight assigned — but that's an implementation detail you shouldn't rely on. Set #weight explicitly to control exactly where your injected field appears relative to the form's existing fields: lower numbers sort earlier, higher numbers later.

See it for yourself

Add this hook implementation to a custom module, visit the form it targets, and confirm the new field renders in the expected position, submits correctly, and (since it's marked #required) blocks submission if left empty.

Quick check: after adding the field this way, where does its submitted value show up in $form_state? (Answer: exactly where you'd expect any other top-level element's value to be — $form_state->getValue('phone_number') — because as far as the Form API is concerned, there's no difference between a field the form defined itself and one your alter hook added.)

Key takeaways

  • Assigning to a new key on $form inside an alter hook creates a brand-new form element, indistinguishable from one the original form defined.
  • Namespace injected keys with your module's machine name to avoid silent collisions with other modules or future core changes.
  • Set #weight explicitly on injected elements rather than relying on default ordering behavior.
  • An injected field's submitted value is read from $form_state exactly like any other element's — no special handling required.

Coming up next

Adding a field is only half the job if that field needs its own validation logic. Next, we look at attaching custom validation to the elements you add — or even to elements that already existed — entirely from an alter hook.