# htmx-development
> Use when developing HTMX features in Drupal 11.3+ or migrating AJAX to HTMX. Covers Htmx class usage, form patterns, migration strategies, and validation. Triggers on "htmx", "ajax to htmx", "dynamic form", "dependent dropdown".
- Author: Carlos Ospina
- Repository: camoa/claude-skills
- Version: 20260114114512
- Stars: 0
- Forks: 0
- Last Updated: 2026-02-06
- Source: https://github.com/camoa/claude-skills
- Web: https://mule.run/skillshub/@@camoa/claude-skills~htmx-development:20260114114512
---
---
name: htmx-development
description: Use when developing HTMX features in Drupal 11.3+ or migrating AJAX to HTMX. Covers Htmx class usage, form patterns, migration strategies, and validation. Triggers on "htmx", "ajax to htmx", "dynamic form", "dependent dropdown".
---
# 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
```php
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
```php
public function content() {
return ['#markup' => '
Content loaded
'];
}
```
### 3. Route (Optional HTMX-Only)
```yaml
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
```php
public function buildForm(array $form, FormStateInterface $form_state) {
$form['category'] = ['#type' => 'select', '#options' => $this->getCategories()];
(new Htmx())
->post(Url::fromRoute(''))
->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
```php
// Primary element updates via target
// Secondary element updates via OOB
(new Htmx())
->swapOob('outerHTML:[data-secondary]')
->applyTo($form['secondary'], '#wrapper_attributes');
```
### URL History
```php
(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 swap
- `select(selector)` - What to extract from response
- `swap(strategy)` - How to swap (outerHTML, innerHTML, beforeend, etc.)
- `swapOob(selector)` - Out-of-band updates
- `trigger(event)` - When to trigger
- `vals(array)` - Additional values
- `onlyMainContent()` - Minimal response
### Response Headers
- `pushUrlHeader(Url)` - Update browser URL
- `redirectHeader(Url)` - Full redirect
- `triggerHeader(event)` - Fire client event
- `reswapHeader(strategy)` - Change swap
- `retargetHeader(selector)` - Change target
See: `references/quick-reference.md` for complete tables
## Detecting HTMX Requests
In forms (trait included automatically):
```php
if ($this->isHtmxRequest()) {
$trigger = $this->getHtmxTriggerName();
}
```
In controllers (add trait):
```php
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
1. Identify `#ajax` properties
2. Replace with `Htmx` class
3. Move callback logic to `buildForm()`
4. Use `getHtmxTriggerName()` for conditional logic
5. Replace `AjaxResponse` with render arrays
6. Test progressive enhancement
See: `references/migration-patterns.md` for detailed examples
## Validation Checklist
When reviewing HTMX implementations:
- [ ] `Htmx` class 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-live` for 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
- `references/quick-reference.md` - Command equivalents, method tables
- `references/htmx-implementation.md` - Full Htmx class API, detection, JS
- `references/migration-patterns.md` - 7 patterns with before/after code
- `references/ajax-reference.md` - AJAX commands for understanding existing code
## Key Files in Drupal Core
- `core/lib/Drupal/Core/Htmx/Htmx.php` - Main API
- `core/lib/Drupal/Core/Htmx/HtmxRequestInfoTrait.php` - Request detection
- `core/lib/Drupal/Core/Render/MainContent/HtmxRenderer.php` - Response renderer
- `core/modules/config/src/Form/ConfigSingleExportForm.php` - Production example
- `core/modules/system/tests/modules/test_htmx/` - Test examples