
Introduction
Starting from Version 11.2, Drupal core will include HTMX as a part of its core dependencies, you can attach it to any twig template via the {{ attach_library('core/htmx') }} statement, for instance to achieve the following accordion component using HTMX, simply create the following SDC component:
FILE:
olivero/component/htmx_accordion.twig1 2 3 4 5 6 7 8 9 10 11{{ attach_library('core/htmx') }} {% set container_id = "content-wrapper-" ~ random() %} <div class="htmx-accordion active" id="{{ container_id }}"> <div> {{accordion_title}} <button hx-on:click="htmx.toggleClass(htmx.find('#{{ container_id }}'), 'active')">BUTTON</button> <!-- Reference: https://htmx.org/api/#toggleClass --> </div> <div> {{accordion_body}} </div> </div>FILE:
olivero/component/htmx_accordion.component.yml: only required for SDC component discovery pupose, can leave emptyFILE:
olivero/component/htmx_accordion.css: link

However, this usage case of accordion to add interactivity doesn’t quite make HTMX standout, some people may contend that this .toggleClass can also be achieved via jQuery, why bother switching to HTMX ? Well … I don’t have a perfect answer to that just yet. Also, I just can’t quite wrap my head around how to use endpoint to return a hypertext/html element to replace existing component (e.g. <button hx-post="/clicked" hx-swap="outerHTML"> Click Me </button>), I can’t think of a way to achieve it except for writing my own router / controller using custom module in Drupal.
But as I am exploring the capabilities of HTMX, I come across this idea of client-side-template extension in HTMX, that allows you to fetch things from JSON endpoint and using client-side template engine to render the data into components, and replace certain existing component with those components. And I think it is worth sharing…
Miscellaneous Setup
Declare Extra Libraries in Theme
First of all since client-side-template is an extension library outside the HTMX, we need to declare it as an library, so we can attach/import them in the twig template later:
| |
(*the hello-world.js is added to test if this htmx-client-side-templates library is loaded, if yes it will print hello world in your console; I found it very useful to test the loading behaviour of CDN libraries; please feel free to remove it if you wish)
Cross Origin Request Meta Tag
Note that in order for the HTMX (mx-get) to send cross-origin request, you’ll also need to add the following meta tag:
| |
(*for simplicity, we will directly have them in along with the component in the below demo examples)
If you forgot the add this meta tag, when requesting from external endpoint you will get error in your console:
| |
HTMX Component - using External JSON as Data

SDC Component Files Used
Here’s files I used this SDC that will use “dummyjson.com” product dummy (external) JSON endpoint to generate card components based on the data it retrieves using HTMX: htmx_card_product_external_json.zip
| |
The important bit is the htmx_card_product_external_json.twig file:
| |
Code Breakdown / Explanation
Button with HTMX attribute:
1 2 3 4 5 6<button hx-get="http://........" hx-swap="innerHTML" hx-target="#example-id-......." nunjucks-array-template="template_id_albums"> Click me to load data </button>with the added HTMX attribute, the button on-click behaviour changes, when clicked it will:
fetch from the json endpoint as declared at
hx-getuse
<tempalte>element with id#nunjucks_template_albumsto generate html component based on JSON data retrieved from the previous step using thenunjuckstemplate engine (*alternatively you can also use other template engines, at the moment there are four available : mustache, handlebars, nunjucks, xslt)finally, the
innerHTMLof component with id#example-id-.......will be replaced with the outcome of previous step (defined byhx-targetandhx-swap)
Template HTML Component :
1 2 3 4 5 6 7 8 9 10 11 12{# Template used by mustache #} {% verbatim %} <template id="nunjucks_template_albums"> {% for product in data.products %} <div class="product"> <img src="{{product.thumbnail}}" alt=""> <div class="title"><p> {{product.title}} - ${{product.price}} </p></div> <div class="brand"><p> {{product.brand}} | {{product.sku}} </p></div> </div> {% endfor %} </template> {% endverbatim %}1 2 3 4 5 6 7 8 9 10 11 12 13 14{# Template used by nunjucks #} {% verbatim %} <template id="mustache_template_albums"> {{#data}} {{#products}} <div class='product'> <img src='{{thumbnail}}' alt=''> <div class='title'><p> {{title}} - {{price}} </p></div> <div class='brand'><p> {{brand}} | {{sku}} </p></div> </div> {{/products}} {{/data}} </template> {% endverbatim %}You might want read more about this on the template engine’s document page (e.g. nunjucks/dump)
Though I have not used these unfamiliar languages yet, I find
mustacheeasier to use (as it is more primitive) for basic use case that don’t require any branching / logic, andnunjucksmore fledged and have similar syntax to the familytwig.Prevent Twig Parsing:
The
verbatimtag marks sections as being raw text that should not be parsed. (*without it, the twig template would parse all the{{data}}{% for ... %}before the template engines used by HTMX sees them. For my instance, I just get empty<template>tag without using the verbatim/raw. In addition, alternative to verbatim block, to you may also use the twigrawfilter, but it looks less clean comparing to using verbatim block)
Example Usage
using “mustache” OR “nunjucks” template engine: demo-GIF-without-preload
1 2 3{{ include('olivero:htmx_card_product_external_json', { + template_engine: "mustache", }) }}1 2 3{{ include('olivero:htmx_card_product_external_json', { + template_engine: "nunjucks", }) }}with Preload: demo-GIF-with-preload (using
hx-trigger="load")1 2 3 4 5 6 7 8{{ include('olivero:htmx_card_product_external_json', { template_engine: "mustache", + preload: true }) }} {{ include('olivero:htmx_card_product_external_json', { template_engine: "nunjucks", + preload: true }) }}
HTMX Component - using Internal JSON as Data
(via JSON-API core module)

Here’s files I used this SDC that will use drupal JSON-API modules as endpoint to generate card components based on the data it retrieves using HTMX: htmx_card_drupal_jsonapi.zip
| |
Note that: we have also created the content type “product” with its associated fields as shown below:

The important bits are in the htmx_card_drupal_jsonapi.twig file:
| |
Below are the example usage:
| |
| |
As you can see the HTMX part of the code of “HTMX SDC Component using internal Drupal JSON-API” is very similar to that of using “external JSON endpoint”, the different lies in:
The use of JSON-API’s parameter / query, especially the
includequery keywordusing a query string like
?include=field_thumbnail,field_thumbnail.field_media_image, you can get to include all the entities referenced byfield_thumbnailand all the entities referenced byfield_thumbnail.field_media_imageon those entities
thus we will be able to get the
urlattribute of the product thumbnail that lives insidefileentity, which is referenced byfield_media_imagefield, which is referenced byfield_thumbnailfield, that lives inside theproductcontent type:1 2 3 4 5 6 7 8 9 10product |__other_fields_... |__field_thumbnail |___field_media_image |___file *attributes |-------------> other_attribute_s_... |-------------> uri |----> value |----> url
The Nunjucks
<template>also need to differ to align with the structure/shape of Drupal’s JSON:API result:1 2 3 4 5 6 7 8 9{# GETTING PRODUCT IMAGE FROM "INCLUDED" RESOURCE #} {# SEE: https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module/includes #} {% for _idx_ in range(data.included|length) %} {% if data.included[_idx_]["type"]=="file--file" %} {% set product_thumbnail = data.included[_idx_] %} {% set product_thumbnail_url = product_thumbnail.attributes.uri.url %} {% set product_thumbnail_url_s = (product_thumbnail_url_s.push(product_thumbnail_url), product_thumbnail_url_s) %} {% endif %} {% endfor %}1 2 3 4 5 6 7 8{# EXTRACTING PRODUCT ATTRIBUTES #} {% for _idx_ in range(data.data|length) %} {% set product_data = data.data[_idx_] %} {% set product_title_s = (product_title_s.push(product_data.attributes.title), product_title_s) %} {% set product_price_s = (product_price_s.push(product_data.attributes.field_price), product_price_s) %} {% set product_brand_s = (product_brand_s.push(product_data.attributes.field_brand), product_brand_s) %} {% set product_sku_s = (product_sku_s.push(product_data.attributes.field_sku), product_sku_s) %} {% endfor %}
Reference
HTMX - Related Resource
Twig - Prevent Twig Template Engine from Parsing
Drupal JSON:API - Related Resource