Skip to content

Building HTMX-powered interfaces with ECA

HTMX lets you build dynamic, interactive interfaces by adding attributes to HTML elements, without writing JavaScript. Drupal core ships HTMX and a small integration layer, and the eca_htmx module exposes that integration to ECA models. This lets you build auto-refreshing regions, click-to-load fragments, and dynamic responses entirely from the modeler.

This guide explains what Drupal core provides, which ECA plugins map to those features, and how to combine them into working patterns.

How HTMX works in Drupal

HTMX is a declarative markup system. You add data-hx-* attributes to an element, and the HTMX JavaScript library turns them into AJAX requests that swap the response into the page. For example, an element with data-hx-get="/clock" and data-hx-trigger="every 1s" polls /clock once a second and replaces its own content with the response.

Drupal core provides three building blocks:

  • The core/drupal.htmx library — the HTMX runtime plus Drupal glue that re-attaches Drupal behaviors to swapped-in content and loads any new CSS and JavaScript the response requires.
  • The minimal response renderer — when a request is marked as an HTMX request, core returns only the page's main content wrapped in a bare HTML document instead of the full themed page (header, blocks, and footer are omitted). The minimal response also carries an X-Robots-Tag: noindex header so the fragment is not indexed by search engines.
  • HTMX request and response headers — HTMX sends HX-* request headers (such as HX-Request and HX-Trigger) and honors HX-* response headers (such as HX-Redirect and HX-Trigger) to coordinate client behavior.

The eca_htmx module builds entirely on these core building blocks. It never ships its own HTMX JavaScript, and it produces all attributes and headers through core's HTMX helper, so the markup stays consistent with the rest of Drupal.

The two ways to request a minimal response

A recurring source of confusion is that core offers two independent ways to get the minimal (main-content-only) response, and they take effect at different stages of the request lifecycle. They are not interchangeable.

_htmx_route route option
Set on a route while routes are being built. Any request to a route carrying this option returns the minimal response. Use the HTMX: mark route action, which reacts to the Alter route event.
_wrapper_format=drupal_htmx query parameter
Added to an individual request at runtime. Only that request returns the minimal response. Use the HTMX: minimal response action, or let the polling and element actions add it automatically through their only main content option.

Which one to use

Mark the route when you control the route and want every request to it to return a fragment (for example, a dedicated endpoint that only ever serves a fragment). Mark the request when you want a specific interaction to fetch a fragment from a route that otherwise renders a full page.

The plugins

Building HTMX elements

HTMX: render element
Builds a single element carrying the HTMX attributes you configure: the request method and URL, the trigger, the swap strategy, the target and select selectors, and options such as a confirmation dialog, indicators, pushed URLs, and request values. Use this for click-to-load fragments, infinite scroll, and similar interactions.
HTMX: poll region
A specialized element that polls a URL on an interval and swaps in the response. Use it for content that refreshes on its own, such as a clock, a status indicator, or a notification count. The only main content option is enabled by default, so the polled URL automatically returns a fragment.

Both actions render an element. Place them in a block, an entity display, a form, or any other render context, and they attach core/drupal.htmx automatically.

Reacting to HTMX requests

HTMX: is request
A condition that is true when the current request is an HTMX request. Use it to branch a model so that it builds a fragment for HTMX requests and a full page otherwise.
HTMX: compare request value
A condition that compares a value from the current HTMX request — such as the target element, the trigger, or the current URL — against an expected value.

The same request information is available as tokens, so you can read the HTMX target or trigger anywhere a token is accepted.

Identifying the source and the request type

As of ECA 3.1.x, two further request values are available, each of them both as a token and as an option on the HTMX: compare request value condition:

Token Condition value option Header Contains
[htmx:source] source HX-Source The CSS selector of the element that triggered the request, for example button[name="first_item"]
[htmx:request_type] request_type HX-Request-Type Whether the request targets a full page or a fragment: full or partial

[htmx:request_type] lets a model branch on a full page load versus a fragment swap, which no token made it possible to determine before. [htmx:source] identifies the triggering element more specifically than [htmx:trigger], which falls short when more than one element shares an id, or when the element carries none at all. The existing HTMX tokens are unchanged.

Both headers need Drupal 12

HX-Source and HX-Request-Type are sent by HTMX 4, which ships with Drupal 12. On Drupal 11, which ships HTMX 3, neither header is sent and both tokens resolve to an empty string.

That emptiness is deliberate. On Drupal 11 the tokens stay empty even when an HX-Source header is present on the request: HTMX 3 never sends one, so such a value reached the site from somewhere else, such as a proxy, and must not be trusted.

If your site may run on Drupal 11, write models that cope with an empty value.

Shaping HTMX responses

HTMX: set response header
Sets any of the HTMX HX-* response headers on the current response, for example HX-Redirect to redirect the client, HX-Trigger to fire a client-side event, or HX-Retarget to change where the response is swapped. The HX-Location header supports its full form (source, event, handler, target, swap, values, headers, and select).

Because this action shapes the response, it reacts to a response event such as the Response created event or an ECA endpoint response.

Rendering HTMX elements into a form

A Drupal form is a render array, and ECA's form events expose it as one. The Build form and Process form events hand out the form array itself as the event's render array, so both HTMX actions, and every eca_render element action, work inside a form exactly as they work inside a block. No configuration differs.

That opens up patterns a form otherwise needs custom code for:

  • A polling region inside a form, for example a live lock or status indicator.
  • A click-to-load element that swaps a fragment into a container that already exists on the page.
  • An element that reacts to a change on an existing form field, using the HTMX from: trigger modifier, such as change from:#edit-field-category.

Three limits apply, and they are easier to learn here than by trial:

  • The request carries no form values. The element has no hx-include option, and Values resolves its tokens when the form is built rather than when the request is issued, so the fragment cannot depend on what the user has typed.
  • Target and Indicator take a CSS selector, and the element itself gets neither an id nor a class. Only selectors that already exist on the page can be addressed.
  • Wrapper tag button produces a <button> with no type attribute, which HTML treats as type="submit". Inside a form, clicking it submits the form. Use a or span instead.

The Machine name is a form element key in this context, and ][ nesting addresses nested form elements, so details][title places the element inside an existing details element.

Leave the machine name set

With Build mode set to Set and clear any previous value and no machine name, the action replaces the event's entire render array. Under a form event, that is the whole form. Use Set value when machine name is defined above when a token supplies the name and might resolve to nothing.

Serving fragments with ECA endpoints

The natural source for an HTMX fragment is an ECA endpoint. An endpoint gives you a URL that an ECA model fully controls: the model builds the response content and can attach HTMX response headers. It pairs naturally with a polling region or a click-to-load element that points at it.

When the "only main content" option matters

The only main content option adds Drupal's _wrapper_format=drupal_htmx hint to the request, which asks Drupal to return the page's main content instead of the full themed page. Against an ECA endpoint, whether that hint changes anything depends on how the model produces the response:

  • The model sets the response content, for example with Set response content. The endpoint returns that response unchanged and the hint has no effect, so the option makes no difference either way.
  • The model only builds a render array and sets no response content. The endpoint hands the render array back to Drupal's page pipeline, which themes it as a full page unless the request carries the hint. Here the option is required.

Against a route that is not an ECA endpoint, such as /node/1, the hint is what you want, because the route renders a full themed page.

The poll action enables the option by default, the element action does not. Set it from the URL you point at rather than from the default.

Common patterns

Auto-refreshing region

  1. Event: any event that builds a render element, such as a block build.
  2. Action: HTMX: poll region — set the URL to an endpoint that returns the fragment, and the interval in seconds.

The region polls the endpoint and swaps in the response on every interval. Leave only main content enabled, which is the default: it is what makes the endpoint return a fragment when the model builds the response as a render array, and it costs nothing when the model sets the response content directly.

Click-to-load content

  1. Event: any event that builds a render element.
  2. Action: HTMX: render element — set the method to GET, the URL to the fragment endpoint, the trigger to click, and the target to the element that should receive the content.

Serve a fragment from an endpoint

  1. Event: ECA endpoint response — define the endpoint path.
  2. Action: Set response content — build the fragment markup.
  3. Action (optional): HTMX: set response header — fire a client-side event with HX-Trigger, for example to notify other parts of the page that the fragment refreshed.

Turn an existing route into a fragment

  1. Event: Alter route.
  2. Action: HTMX: mark route — set the route name (a * wildcard is supported) to mark as an HTMX route.

Every request to a marked route then returns the minimal response.