AJAX to HTMX Migration
When to Use
Use this when converting existing AJAX implementations to HTMX, or running both systems in parallel during gradual migration.
Decision
| AJAX Pattern | Migrate to HTMX? | Reason |
|---|---|---|
| Simple content replacement | Yes | HTMX simpler, less code |
| Form field dependencies | Yes | Better DX, automatic form_build_id handling |
| Load more / infinite scroll | Yes | Native HTMX patterns |
| Modal content loading | Yes | Simpler target swapping |
| Complex command sequences (css, invoke, settings) | No | AJAX commands needed |
| Heavy JavaScript processing | No | HTMX is server-driven |
| Contrib module integration | No | Maintain compatibility |
Pattern
AJAX button → HTMX button:
Before (AJAX):
$form['load_button'] = [
'#type' => 'button',
'#value' => 'Load Content',
'#ajax' => ['callback' => '::ajaxCallback', 'wrapper' => 'content-wrapper'],
];
public function ajaxCallback(array &$form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$response->addCommand(new ReplaceCommand('#content-wrapper', $content));
return $response;
}
After (HTMX):
$form['load_button'] = ['#type' => 'button', '#value' => 'Load Content'];
(new Htmx())
->get(Url::fromRoute('my.route'))
->target('#content-wrapper')
->swap('innerHTML')
->onlyMainContent()
->applyTo($form['load_button']);
// Controller returns render array directly — no callback needed
Multiple AJAX commands → OOB swaps:
Before (AJAX):
$response->addCommand(new ReplaceCommand('#region-1', $content1));
$response->addCommand(new ReplaceCommand('#region-2', $content2));
$response->addCommand(new InvokeCommand('.alert', 'show'));
After (HTMX):
// Primary target handles region-1 automatically
(new Htmx())->swapOob('outerHTML:#region-2')->applyTo($form['region2'], '#wrapper_attributes');
(new Htmx())->triggerHeader(['showAlert' => []])->applyTo($form);
// JS: htmx.on('showAlert', () => document.querySelector('.alert').show());
Migration checklist:
- [ ] Identify AJAX callbacks — simple content returns migrate well
- [ ] Check for command complexity — multiple commands may stay AJAX
- [ ] Replace
#ajaxarrays withHtmxclass attributes - [ ] Update JavaScript behaviors — listen for
htmx:drupal:loadinstead of AJAX events - [ ] Test progressive enhancement — form should work without JavaScript
- [ ] Update tests — change AJAX test expectations to HTMX
- [ ] Document decisions — why some stayed AJAX, why some moved
Common Mistakes
- Wrong: Trying to use AJAX commands with HTMX → Right: Return HTML render arrays, not JSON
- Wrong: Not updating behaviors for HTMX events → Right: Use
htmx:drupal:loadinstead of AJAX events - Wrong: Migrating everything blindly → Right: Some use cases genuinely need AJAX
- Wrong: Not removing old AJAX callbacks → Right: Dead code accumulates
See Also
- Troubleshooting
- Core File Reference
- HTMX vs AJAX Decision
- Reference:
/core/modules/system/tests/modules/test_htmx/src/Form/HtmxTestAjaxForm.php