AJAX to Submit Buttons: submit vs. button Typesfor Drupal 8 , 9 , 10 , and 11

Last updated :  

Every AJAX trigger you've built so far has responded to a select element's change event. But #ajax isn't limited to selects — it works on submit buttons too, and you've actually already seen it: the "Add one more" and "Remove one" buttons from the earlier add-more lesson were both submit buttons with an #ajax property attached. This lesson looks at that combination directly, and at a genuinely different use case: an AJAX-only submit that never triggers a real form submission at all.

What you'll learn in this lesson

  • Why a button can carry both #submit and #ajax at once, and what order they run in
  • The difference between a submit button and a plain button element for AJAX purposes
  • How to build a button that only ever does AJAX, and never a real page submission
  • Why the button's own #type choice matters more than it looks like it should

Revisiting the button you've already used

$form['names_fieldset']['actions']['add_name'] = [
  '#type' => 'submit',
  '#value' => $this->t('Add one more'),
  '#submit' => ['::addOne'],
  '#ajax' => [
    'callback' => '::addMoreCallback',
    'wrapper' => 'names-fieldset-wrapper',
  ],
];

This is real code from the earlier add-more lesson, and it already answers the "how" question: a #type => 'submit' button can carry #submit and #ajax simultaneously, with the #submit handler running first and the #ajax callback running second, once the form has been rebuilt with whatever the submit handler changed.

#type: 'button' — a submit that never really submits

$form['preview'] = [
  '#type' => 'button',
  '#value' => $this->t('Preview'),
  '#ajax' => [
    'callback' => '::previewCallback',
    'wrapper' => 'preview-wrapper',
  ],
];

This is the genuinely different case: #type => 'button' instead of #type => 'submit'. A plain button element never triggers Drupal's normal form-submission pipeline at all — no validateForm(), no submitForm(), ever, no matter what. Combined with #ajax, it becomes a control whose sole purpose is triggering an AJAX callback: a "Preview" button that renders a preview without ever actually submitting anything, or a "Check availability" button that just queries something and reports back.

This is the detail worth remembering: #type => 'submit' + #ajax gives you both a real submission path (with #submit handlers) and an AJAX response, running in that order. #type => 'button' + #ajax gives you AJAX only — there is no submission path at all, by design, regardless of what you set #submit to. Choosing the wrong one is a common source of "why did my form submit when I only wanted a preview" bugs.

Quick check: you want a "Check username availability" button that queries the database and shows a result, but should never actually submit the surrounding registration form. Which #type do you reach for? (Answer: #type => 'button', not 'submit' — a plain button with #ajax never enters the form-submission pipeline at all, which is exactly the guarantee you need here.)

Key takeaways

  • A #type => 'submit' button can carry both #submit and #ajax — the submit handler runs first, then the AJAX callback runs against the rebuilt form.
  • #type => 'button' never enters the normal form-submission pipeline, with or without #ajax attached — it's purely a trigger for the AJAX callback.
  • Reach for 'button' whenever you want an action that should never, under any circumstance, submit the surrounding form.
  • Reach for 'submit' + #ajax whenever you genuinely want both a real submission and an AJAX-driven update, like the add-more pattern.

Coming up next

You've now covered AJAX from every angle — callbacks, commands, dynamic fields, error handling, and submit buttons. The next lesson moves to a related but distinct tool for reactive forms: the #states property, which lets you show, hide, and enable fields conditionally with zero PHP round-trips at all.