Adding AJAX Callbacks: The #ajax Property Explainedfor Drupal 8 , 9 , 10 , and 11

Last updated :  

You've built forms that submit, validate, and store data — but every one of them so far has needed a full page reload to do anything. Think of a restaurant order pad versus a live kitchen display: the order pad only updates once, when the waiter walks it over; the kitchen display updates the instant a new ticket prints, with nobody having to physically carry paper anywhere. Drupal's #ajax property turns any form element into that live display — a change on the page triggers a server round-trip and a targeted update, with no full reload and, crucially, no hand-written JavaScript required.

What you'll learn in this lesson

  • The two keys every #ajax array needs — callback and wrapper — and what each one actually does
  • Why the AJAX callback method itself does no real work
  • How a wrapper element's HTML id is what gets replaced on the page
  • How a dependent-select pattern (one field's choice determining another's options) falls naturally out of this mechanism

The two pieces of an #ajax array

$form['temperature'] = [
  '#title' => $this->t('Temperature'),
  '#type' => 'select',
  '#options' => $this->getColorTemperatures(),
  '#empty_option' => $this->t('- Select a color temperature -'),
  '#ajax' => [
    'callback' => '::updateColor',
    'wrapper' => 'color-wrapper',
  ],
];

Add an #ajax array to almost any form element and Drupal attaches JavaScript behind the scenes that intercepts its change or click event, sends an asynchronous request, and waits for a response — you never write that JavaScript yourself. Two keys matter:

  • callback — the PHP method Drupal invokes server-side once the request arrives. The :: prefix means "a method on this form class."
  • wrapper — the HTML id of an element already present on the page, whose inner content gets replaced with whatever the callback returns.

Building the wrapper element

$form['color_wrapper'] = [
  '#type' => 'container',
  '#attributes' => ['id' => 'color-wrapper'],
];

$temperature = $form_state->getValue('temperature');
if (!empty($temperature)) {
  $form['color_wrapper']['color'] = [
    '#type' => 'select',
    '#title' => $this->t('Color'),
    '#options' => $this->getColorsByTemperature($temperature),
  ];
}

This is the detail that trips people up the first time they meet this pattern: the color dropdown isn't specially wired for AJAX at all. It's an ordinary conditional — buildForm() checks whether a temperature has been selected yet, and only adds the color select if one has. The moment someone picks a temperature, Drupal rebuilds the whole form server-side with that value already present in $form_state, and this same conditional now evaluates true.

The callback does no real work. All the actual logic — deciding which colors belong to which temperature — already happened in buildForm() by the time the callback runs. Its only job is to hand back the already-correct slice of the rebuilt $form array so Drupal can render it and swap it into the wrapper:
public function updateColor(array $form, FormStateInterface $form_state) {
  return $form['color_wrapper'];
}
That's the entire method — one line. Every AJAX callback you write will tend to look this thin once you internalize this pattern.

Quick check: why doesn't updateColor() need a parameter telling it which temperature was picked? (Answer: because buildForm() already read $form_state->getValue('temperature') and built the correct color options before the callback ever ran — the callback just returns what's already sitting in $form.)

Key takeaways

  • The #ajax property turns any element's change or click event into an asynchronous request — Drupal generates all the JavaScript; you only supply a callback and a wrapper.
  • Wrap the target region in an element with a stable id, either a container with #attributes => ['id' => '...'] or #prefix/#suffix on any element.
  • AJAX callbacks should never contain real logic — they only return the slice of the already-rebuilt $form array that should replace the wrapper's contents.
  • A dependent-select pattern (one field's value determining another field's options) falls out naturally from an ordinary conditional in buildForm() — no special AJAX-only code path needed.

Coming up next

You've seen the shape of an AJAX callback, but glossed over what "return the wrapper" actually means under the hood. The next lesson opens that up properly — the exact signature an AJAX callback function needs, and what Drupal does with whatever you hand it back.