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
More from camoa/claude-skills
html-generator
Use when generating branded HTML pages and components from a design system. Creates standalone HTML components and composes them into full pages with embedded CSS, responsive design, and brand integration.
43memory-manager
Use after completing any phase activity - updates project_state.md, project registry, ensures files are in correct locations, maintains lean memory
39diagram-generator
Use when visualizing architecture - generates Mermaid diagrams for data flow, service relationships, or entity structures. Trigger: 'draw diagram', 'visualize', 'show relationships', 'mermaid chart'.
31code-quality-audit
Use when checking code quality, running security audits, testing coverage, finding SOLID/DRY violations, or setting up quality tools. Use when user says "audit this code", "check security", "run PHPStan", "code quality", "find violations", "SOLID check", "DRY check", "test coverage", "lint this", "security review", "is this production ready", "check for vulnerabilities", "code review", "grade this code". Supports Drupal (PHPStan, PHPMD, Psalm, Semgrep, Trivy, Gitleaks via DDEV) and Next.js (ESLint, Jest, Semgrep, Trivy, Gitleaks). Use proactively before deployment or after significant code changes.
20requirements-gatherer
Use when gathering project requirements - asks structured questions about project type, scope, integrations, and constraints to populate project_state.md
17generating-infographics
Use when creating infographics, data visualizations, process diagrams, timelines, or comparisons - generates branded infographics using @antv/infographic with 114 templates across 7 categories. Triggers on "create infographic", "make infographic", "visualize data", "timeline", "process diagram".
17