htmx-development
HTMX Development
Drupal 11.3+ HTMX implementation and AJAX migration guidance.
When to Use
- Implementing dynamic content updates in Drupal
- Building forms with dependent fields
- Migrating existing AJAX to HTMX
- Adding infinite scroll, load more, real-time validation
- NOT for: Traditional AJAX maintenance (use ajax-reference.md)
Decision: HTMX vs AJAX
| Choose HTMX | Choose AJAX |
|---|---|
| New features | Existing AJAX code |
| Declarative HTML preferred | Complex command sequences |
| Returns HTML fragments | Dialog commands needed |
| Progressive enhancement needed | Contrib expects AJAX |
Hybrid OK: Both systems coexist. Migrate incrementally.
Quick Start
1. Basic HTMX Element
use Drupal\Core\Htmx\Htmx;
use Drupal\Core\Url;
$build['button'] = [
'#type' => 'button',
'#value' => t('Load'),
];
(new Htmx())
->get(Url::fromRoute('my.content'))
->onlyMainContent()
->target('#result')
->swap('innerHTML')
->applyTo($build['button']);
2. Controller Returns Render Array
public function content() {
return ['#markup' => '<p>Content loaded</p>'];
}
3. Route (Optional HTMX-Only)
my.content:
path: '/my/content'
options:
_htmx_route: TRUE # Always minimal response
Core Patterns
Pattern Selection
| Use Case | Pattern | Key Methods |
|---|---|---|
| Dependent dropdown | Form partial update | select(), target(), swap('outerHTML') |
| Load more | Append content | swap('beforeend'), trigger('click') |
| Infinite scroll | Auto-load | swap('beforeend'), trigger('revealed') |
| Real-time validation | Blur check | trigger('focusout'), field update |
| Multi-step wizard | URL-based steps | pushUrl(), route parameters |
| Multiple updates | OOB swap | swapOob('outerHTML:#selector') |
Dependent Dropdown
public function buildForm(array $form, FormStateInterface $form_state) {
$form['category'] = ['#type' => 'select', '#options' => $this->getCategories()];
(new Htmx())
->post(Url::fromRoute('<current>'))
->onlyMainContent()
->select('#edit-subcategory--wrapper')
->target('#edit-subcategory--wrapper')
->swap('outerHTML')
->applyTo($form['category']);
$form['subcategory'] = ['#type' => 'select', '#options' => []];
// Handle trigger
if ($this->getHtmxTriggerName() === 'category') {
$form['subcategory']['#options'] = $this->getSubcategories(
$form_state->getValue('category')
);
}
return $form;
}
Reference: core/modules/config/src/Form/ConfigSingleExportForm.php
Multiple Element Updates
// Primary element updates via target
// Secondary element updates via OOB
(new Htmx())
->swapOob('outerHTML:[data-secondary]')
->applyTo($form['secondary'], '#wrapper_attributes');
URL History
(new Htmx())
->pushUrlHeader(Url::fromRoute('my.route', $params))
->applyTo($form);
Htmx Class Reference
Request Methods
get(Url)/post(Url)/put(Url)/patch(Url)/delete(Url)
Control Methods
target(selector)- Where to swapselect(selector)- What to extract from responseswap(strategy)- How to swap (outerHTML, innerHTML, beforeend, etc.)swapOob(selector)- Out-of-band updatestrigger(event)- When to triggervals(array)- Additional valuesonlyMainContent()- Minimal response
Response Headers
pushUrlHeader(Url)- Update browser URLredirectHeader(Url)- Full redirecttriggerHeader(event)- Fire client eventreswapHeader(strategy)- Change swapretargetHeader(selector)- Change target
See: references/quick-reference.md for complete tables
Detecting HTMX Requests
In forms (trait included automatically):
if ($this->isHtmxRequest()) {
$trigger = $this->getHtmxTriggerName();
}
In controllers (add trait):
use Drupal\Core\Htmx\HtmxRequestInfoTrait;
class MyController extends ControllerBase {
use HtmxRequestInfoTrait;
protected function getRequest() { return \Drupal::request(); }
}
Migration from AJAX
Quick Conversion
| AJAX | HTMX |
|---|---|
'#ajax' => ['callback' => '::cb'] |
(new Htmx())->post()->applyTo() |
'wrapper' => 'id' |
->target('#id') |
return $form['element'] |
Logic in buildForm() |
new AjaxResponse() |
Return render array |
ReplaceCommand |
->swap('outerHTML') |
HtmlCommand |
->swap('innerHTML') |
AppendCommand |
->swap('beforeend') |
MessageCommand |
Auto-included |
Migration Steps
- Identify
#ajaxproperties - Replace with
Htmxclass - Move callback logic to
buildForm() - Use
getHtmxTriggerName()for conditional logic - Replace
AjaxResponsewith render arrays - Test progressive enhancement
See: references/migration-patterns.md for detailed examples
Validation Checklist
When reviewing HTMX implementations:
-
Htmxclass used (not raw attributes) -
onlyMainContent()for minimal response - Proper swap strategy selected
- OOB used for multiple updates
- Trigger element detection works
- Works without JavaScript (progressive)
- Accessibility:
aria-livefor dynamic regions - URL updates for bookmarkable states
Common Issues
| Problem | Solution |
|---|---|
| Content not swapping | Check target() selector exists |
| Wrong content extracted | Check select() selector |
| JS not running | Verify htmx:drupal:load fires |
| Form not submitting | Check post() and URL |
| Multiple swaps fail | Add swapOob('true') to elements |
| History broken | Use pushUrlHeader() |
References
Bundled (HTMX-Specific)
references/quick-reference.md- Command equivalents, method tablesreferences/htmx-implementation.md- Full Htmx class API, detection, JSreferences/migration-patterns.md- 7 patterns with before/after codereferences/ajax-reference.md- AJAX commands for understanding existing code
Online Dev-Guides
For deeper Drupal context beyond bundled references, use the dev-guides-navigator plugin:
Invoke /dev-guides-navigator with keywords like "Drupal forms", "routing", "JS development", or "render API". The navigator handles caching and disambiguation — never fetch dev-guides URLs directly.
Relevant topics: drupal/forms (FAPI with HTMX), drupal/routing (HTMX routes), drupal/js-development (behaviors + HTMX events), drupal/render-api (render arrays for HTMX responses).
Key Files in Drupal Core
core/lib/Drupal/Core/Htmx/Htmx.php- Main APIcore/lib/Drupal/Core/Htmx/HtmxRequestInfoTrait.php- Request detectioncore/lib/Drupal/Core/Render/MainContent/HtmxRenderer.php- Response renderercore/modules/config/src/Form/ConfigSingleExportForm.php- Production examplecore/modules/system/tests/modules/test_htmx/- Test examples