Using #element_validate for individual elementsfor Drupal 8 , 9 , 10 , and 11

Last updated :  

Not every validation rule belongs in one giant validateForm() method checking every field at once. If a single element's rule is genuinely local to that element — nothing else on the form affects it — Drupal lets you attach the validation directly to the element itself with #element_validate, keeping the logic right next to the field it governs.

What you'll learn in this lesson

  • How #element_validate attaches a validation callback to one specific element
  • The exact signature Drupal calls your callback with
  • When to reach for #element_validate instead of the form-wide validateForm()
  • How multiple element-level validators and the form-level validator interact

Attaching a validator to one element

$form['age'] = [
  '#type' => 'number',
  '#title' => $this->t('Age'),
  '#element_validate' => ['::validateAge'],
];
public function validateAge(array &$element, FormStateInterface $form_state, array &$complete_form) {
  $value = $element['#value'];
  if ($value !== '' && (int) $value < 18) {
    $form_state->setError($element, $this->t('You must be at least 18 years old.'));
  }
}

#element_validate takes an array of callables, run in order, each receiving exactly three arguments: &$element (the specific render array for this one element, not the whole form), FormStateInterface $form_state, and &$complete_form (the entire form array, in case you need context from elsewhere). Reading the submitted value inside an element validator comes from $element['#value'], not $form_state->getValue() — at this point in the request lifecycle, the individual element's own value has already been extracted onto it directly.

Reporting the error against the right element

$form_state->setError($element, $this->t('You must be at least 18 years old.'));

Passing $element (not a field name string) to setError() ties the error message directly to that specific element's position on the rendered form — Drupal highlights the exact field and places the message right next to it, rather than relying on you knowing its machine name.

Element-level vs. form-level validation

Both run during the same validation phase, and both run every time, regardless of whether an earlier validator already set an error. Use #element_validate when a rule only needs that one element's own value — format checks, range checks, single-field business rules. Reach for the form-wide validateForm() method instead when a rule needs to compare two or more fields against each other (like "end date must be after start date") — no single element's own validator can see a sibling element's value cleanly.

Order matters, sort of: Drupal core's own element types often already have their own built-in #element_validate callbacks (for example, email elements validate their own format). Adding your own callback appends to that list rather than replacing it — both run. Don't duplicate a check core already performs for a given #type.

See it for yourself

Add an #element_validate callback to a number field that rejects values under 18, submit the form with a value of 15, and confirm the error message appears directly beneath that field — not as a generic form-wide message at the top.

Quick check: your element validator signature is function validateAge(array &$element, FormStateInterface $form_state, array &$complete_form) — why is $complete_form passed in at all, if the validator is only supposed to care about one element? (Answer: sometimes a single element's validity genuinely depends on another field elsewhere on the form — $complete_form gives you an escape hatch to inspect the whole form array when that happens, without forcing every element-level validator to become a form-level one.)

Key takeaways

  • #element_validate attaches one or more validation callbacks directly to a single form element.
  • The callback signature is fixed: (&$element, FormStateInterface $form_state, &$complete_form) — read the submitted value from $element['#value'].
  • Pass the $element array (not a string field name) to $form_state->setError() so the message anchors to the right field.
  • Use element-level validation for single-field rules; use form-level validateForm() for rules comparing multiple fields.
  • Core's built-in element types often already carry their own #element_validate callbacks — yours adds to that list, it doesn't replace it.

Coming up next

Next up: the single most common validation rule of all — making sure a field was filled in at all. You'll see how #required handles that automatically, and exactly when Drupal checks it relative to your own custom validators.