Skip to content

HTMX Controllers

When to Use

You're building controller routes that return dynamic content for HTMX requests.

Pattern: Basic HTMX Controller

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Htmx\Htmx;
use Drupal\Core\Url;

class MyController extends ControllerBase {
  public function htmxContent() {
    // Build content render array
    $build['content'] = [
      '#type' => 'markup',
      '#markup' => '<div>Dynamic content</div>',
    ];

    // Configure HTMX for a button
    $build['button'] = [
      '#type' => 'html_tag',
      '#tag' => 'button',
      '#value' => 'Load More',
    ];

    (new Htmx())
      ->get(Url::fromRoute('my.route'))
      ->target('#content-wrapper')
      ->swap('innerHTML')
      ->applyTo($build['button']);

    return $build;
  }
}

Reference: /core/modules/system/tests/modules/test_htmx/src/Controller/HtmxTestAttachmentsController.php lines 94-132

Pattern: Conditional Responses

Return different content based on HTMX request:

use Drupal\Core\Htmx\HtmxRequestInfoTrait;

class MyController extends ControllerBase {
  use HtmxRequestInfoTrait;

  protected function getRequest() {
    return \Drupal::request();
  }

  public function content() {
    if ($this->isHtmxRequest()) {
      // Minimal response for HTMX
      return ['#markup' => '<div>Just the content</div>'];
    }

    // Full page for initial request
    $build['#theme'] = 'my_template';
    $build['content'] = ['#markup' => '<div>Just the content</div>'];
    return $build;
  }
}

Pattern: Using Route Option

Define route with _htmx_route: TRUE to automatically invoke HtmxRenderer:

my_module.htmx_only:
  path: '/my-module/htmx-only'
  defaults:
    _controller: '\Drupal\my_module\Controller\MyController::htmxOnly'
  options:
    _htmx_route: TRUE

Controller returns standard render array — HtmxRenderer handles minimal response automatically.

Reference: /core/lib/Drupal/Core/EventSubscriber/HtmxContentViewSubscriber.php — Handles _htmx_route routes

Common Mistakes

  • Returning full render arrays without _htmx_route or onlyMainContent() — Results in full page HTML
  • Forgetting to implement getRequest() when using HtmxRequestInfoTrait — Methods will fail
  • Not testing both HTMX and non-HTMX requests — Initial page load isn't HTMX
  • Using _htmx_route for routes that serve both HTMX and full pages — Use conditional logic instead

See Also