htmx and Astro View Transitions

By

Learn how to make htmx work with Astro View Transitions by calling htmx.process() after a page swap so your htmx events keep firing on astro:after-swap.

~~~

If htmx stops working after an Astro View Transition, the fix is to call htmx.process(document.body) in the astro:after-swap event. Let me explain why this happens, and how I found out.

Why does htmx break after a view transition?

htmx scans the page when it loads. It finds every element with hx-get, hx-post, hx-trigger and so on, and wires up the behavior.

With View Transitions enabled, Astro doesn’t do a full page load when you navigate. It fetches the new page and swaps its content into the current document.

htmx never sees that new content. The hx-* attributes are there in the HTML, but nothing is listening to them. They’re inert.

The problem I hit

I had an event that fired on page load, so I added a astro:after-swap listener to do the same thing after a page transition:

<script>
  const triggerFetchLastUpdated = () => {
    const contentElement = document.getElementById('fetch-last-updated')
    if (contentElement) {
      htmx.trigger(contentElement, 'click', {})
    }
  }

  htmx.onLoad(triggerFetchLastUpdated)

  document.addEventListener('astro:after-swap', () => {
    triggerFetchLastUpdated()
  })
</script>

But my htmx event didn’t fire after a transition. The element was found, htmx.trigger() ran, and nothing happened. That’s the symptom of unprocessed content.

The fix

Call htmx.process() after the page swap. This tells htmx to scan the element you pass (and everything inside it) and initialize any hx-* attributes it finds:

<script>
  const triggerFetchLastUpdated = () => {
    const contentElement = document.getElementById('fetch-last-updated')
    if (contentElement) {
      htmx.trigger(contentElement, 'click', {})
    }
  }

  htmx.onLoad(triggerFetchLastUpdated)

  document.addEventListener('astro:after-swap', () => {
    htmx.process(document.body)
    triggerFetchLastUpdated()
  })
</script>

Passing document.body re-processes the whole page. That’s fine for a small site. If you know exactly which part of the page changed, you can pass that element instead.

An alternative with a single event

You could also just have one event, astro:page-load, and skip handling 2 different events (htmx.onLoad and astro:after-swap). Astro fires it on the first page load too, so one listener covers both cases:

<script>
  document.addEventListener('astro:page-load', () => {
    const contentElement = document.getElementById('fetch-last-updated')
    if (contentElement) {
      htmx.process(document.body)
      htmx.trigger(contentElement, 'click', {})
    }
  })
</script>

…but that’s slower, as it fires at the end of page navigation. astro:after-swap instead fires immediately after the new page replaces the old page, so your htmx behavior comes back with no visible delay.

Tagged: Astro, htmx · All topics
~~~

Related posts about astro: