Using Drupal's AJAX command systemfor Drupal 11 , 10 , 9 , and 8

Last updated :  

An AjaxResponse is never HTML by itself — it's a list of instructions, and each instruction is a command. Think of it like a stage manager's cue sheet: "replace this prop, append that line, fade this light" — the browser reads the cue sheet and carries out each cue in order. This lesson tours the command system properly.

What you'll learn in this lesson

  • What an AjaxResponse actually contains
  • The real AppendCommand pattern from the Examples module
  • The most common commands you'll reach for, and when each fits
  • Why a callback can return multiple commands in one response

A real command, from the Examples module

use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\AppendCommand;

public function ajaxLinkCallback($nojs = 'nojs') {
  if ($nojs == 'ajax') {
    $output = $this->t("This is some content delivered via AJAX");
    $response = new AjaxResponse();
    $response->addCommand(new AppendCommand('#ajax-example-destination-div', $output));
    return $response;
  }
  // ...
}

AppendCommand('#ajax-example-destination-div', $output) tells the browser: "find the element matching this CSS selector, and append this HTML inside it, after any existing content." addCommand() can be called as many times as you like on one AjaxResponse — each call queues one more instruction, and the browser executes all of them, in the order added, from a single request.

The command family you'll actually use

  • AppendCommand($selector, $content) — adds content inside an element, after whatever's already there. Good for "load more" style additions.
  • ReplaceCommand($selector, $content) — swaps an element's entire outer HTML for new markup. The most common command for "re-render this whole region with fresh state."
  • HtmlCommand($selector, $content) — replaces only the inner HTML of an element, keeping the element itself (and any attributes on it) intact.
  • InvokeCommand($selector, $method, $args) — calls an arbitrary jQuery method on matched elements, for anything the built-in commands don't cover directly (toggling a class, calling a plugin's method).
  • RemoveCommand($selector) — deletes matched elements from the page entirely.
  • MessageCommand($message) — displays a Drupal status/warning/error message the same way $this->messenger()->addMessage() would on a full page load, but without a page reload.

Combining commands in one response

$response = new AjaxResponse();
$response->addCommand(new ReplaceCommand('#results-wrapper', $new_results_markup));
$response->addCommand(new MessageCommand($this->t('Results updated.')));
return $response;

This is exactly why the command pattern exists rather than an AJAX callback just returning one blob of HTML: a single interaction often needs to update more than one unrelated part of the page — swap the main content and show a confirmation message and, say, update a counter elsewhere — and each of those is naturally its own command targeting its own selector.

Commands target CSS selectors, not Drupal render-array IDs: the string you pass as the first argument is a literal CSS selector ('#my-id', '.my-class'), matched against the live DOM exactly the way document.querySelector() would. It's easy to assume some special Drupal-only targeting is happening — there isn't; get the selector wrong and the command silently does nothing, with no error anywhere.

Quick check: you need to update a summary count elsewhere on the page and replace a results table, from one AJAX callback triggered by a single button. Do you need two separate AJAX requests? If you said no — one AjaxResponse with two addCommand() calls (one ReplaceCommand for the table, one for the count) handles both updates from a single round trip.

Key takeaways

  • An AjaxResponse is a container for one or more commands, executed by the browser in the order they were added.
  • ReplaceCommand and HtmlCommand cover most "update this region" needs; the difference is whether the target element itself is replaced or just its contents.
  • MessageCommand gives you Drupal's normal status-message styling without a page reload.
  • Command selectors are plain CSS selectors matched against the live DOM — a typo fails silently, with no visible error.

Coming up next

Commands assume everything went well — but AJAX requests fail too (network errors, server exceptions, validation failures). Next: handling those failure cases gracefully instead of leaving the user staring at a request that silently went nowhere.