Skip to content

Add HTMX to listing filters - #462

Merged
jhancock532 merged 8 commits into
migration-2026from
feature/htmx-listing-filters
Sep 29, 2026
Merged

jhancock532 merged 8 commits into
migration-2026from
feature/htmx-listing-filters

Conversation

@jhancock532

@jhancock532 jhancock532 commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Description

The filters on the Blog, Events and Work listing pages now update the results as soon as a checkbox changes. Users no longer have to click "Apply filters". All three pages share the same filter component, so most of the change is in one template and one JS component.

  • Added HTMX. The filter form sends a GET request on every checkbox change and swaps in the new results, pagination, active-filter pills and a screen reader result count.
  • The dropdowns are never swapped, so the open dropdown and focus stay put.
  • The URL updates on each change, so filtered views can still be shared and bookmarked.
  • Removing a pill or clicking "Clear all" updates the results in place instead of reloading the page.
  • Progressive enhancement - without JavaScript, the page works just as it did before: tick boxes, click "Apply filters", page reloads.

Technical details

HTMX requests get the full page, and hx-select picks out the parts to swap. This avoids new views and partial templates, at the cost of sending more HTML than a partial would. This reduces the complexity of the MR significantly so I think it is a worthwhile tradeoff.

The browser back and forward buttons do a full page load (I've set the HTMX historyCacheSize = 0), because HTMX's history restore would leave the site's JS components unbound.

There's no debounce. hx-sync="this:replace" cancels any request in flight, so only the last request is shown.

HTMX loads on every page from main.js (about 16 KB gzipped), with allowEval and indicator styles turned off for CSP. I've chosen an easier to review implementation here over the more complex approach that would result in minor sustainability improvements.

Caching considerations

Removing HX-Current-URL from the Cloudflare cache key. I've left this out because I'm not fully sure of the implications of updating a Cloudflare worker. As I understand it, the worker adds HX-Current-URL (the URL of the page the user is on when they make the request) to the cache key for HTMX requests. So when a user changes a filter, the response is cached against both the URL they came from and the new filtered URL. If visitors filter in different sequences, most of these requests will miss the cache and go to the origin.

For example:

  1. A user follows a link to /news/?sector=charity-non-profit&service=design. This is a normal page load, so it's cached as usual and other visitors to that URL get the cached copy.

  2. The user then removes a filter, which makes an HTMX request for /news/?sector=charity-non-profit. That response is cached against both /news/?sector=charity-non-profit&service=design and /news/?sector=charity-non-profit, so another visitor only gets it from the cache if they make exactly the same change. A direct visit to /news/?sector=charity-non-profit isn't affected, because it has no HX headers.

Users won't see incorrect results, since every cached copy is correct for its URL. The cost is more requests reaching the origin than necessary. I don't think it's a major concern, but would be nice to update / redeploy the worker to remove HX-Current-URL if that's OK.

Other considerations

  • Updating the canonical tag on HTMX swap isn't worth doing as crawlers always get the full page directly.
  • GTM tracking for filter changes is maybe worth considering in the future, if we're interested in this

How to Test

On each of Blog, Events and Work:

  1. Open a dropdown and tick a checkbox. Results, pills, pagination and URL update, and the dropdown stays open with focus on the checkbox.
  2. Tick several boxes quickly. The final results match the final selection.
  3. Remove a pill. Results update, and focus moves to the next pill (or the first dropdown toggle if none are left).
  4. Click "Clear all". Results reset and focus moves to the first dropdown toggle.
  5. Go to page 2, then change a filter. You're taken back to page 1, and pagination links keep the filters.
  6. Use back and forward. You move between filter states and the menus, mode switcher and filters still work.
  7. Reload a filtered URL. Results are the same.
  8. Disable JavaScript. The Apply button shows and filtering works as before.
  9. With VoiceOver, check the result count is announced after each change.
  10. Events only: untick every "When" option. It falls back to upcoming events and the empty-state message is correct.

Plese check out the pattern library template as well: http://localhost:8000/pattern-library/pattern/patterns/molecules/listing-filters/listing-filters.html

Screenshots

Expand to see more

JS disabled - Apply filters button shows

image

JS enabled, light mode - No apply filters button shows

image

MR Checklist

  • Add a description of your pull request and instructions for the reviewer to verify your work.
  • If your pull request is for a specific ticket, link to it in the description.
  • Stay on point and keep it small so the merge request can be easily reviewed.
  • Tests and linting passes.

Unit tests

  • Added
  • Not required

Documentation

Browser testing

  • I have tested in the following browsers and environments (edit the list as required)
    • Latest version of Chrome on mac
    • Latest version of Firefox on mac
    • Latest version of Safari on mac
  • Not required

Data protection

  • Not relevant
  • This adds new sources of PII and documents it and modifies Birdbath processors accordingly

Light and dark mode

  • I have tested the changes in both light and dark mode
  • The change is not relevant to dark and light mode

Accessibility

  • Automated WCAG 2.1 tests pass
  • HTML validation passes
  • Manual WCAG 2.1 tests completed
  • I have tested in a screen reader
  • I have tested in high-contrast mode
  • Any animations removed for prefers-reduced-motion
  • Not required

Sustainability

  • Images are optimised and lazy-loading used where appropriate
  • SVGs have been optimised
  • Performance and transfer of data considered (N.B. minor trade-offs have been made here.)
  • If JavaScript is needed alternatives have been considered
  • Not required

Pattern library

  • The pattern library component for this template displays correctly, and does not break parent templates
  • The styleguide is updated if relevant
  • Changes are not relevant the pattern library

James Hancock added 7 commits September 24, 2026 10:44
Give the results list, pagination and active-filter pills stable ids so
they can be swapped in place, add a visually hidden result count status
message, and add data attributes the listing-filters JS can use to find
the submit button, pills and clear-all link.
Pill and "Clear all" clicks now untick the matching checkboxes and fire a
single change event, so HTMX stays the only request path. Their hrefs are
kept as the no-JS fallback. Focus moves to the next pill, then the
previous pill, then the first dropdown toggle once the new pills are
swapped in. The results list is marked aria-busy while a request is in
flight.
@jhancock532
jhancock532 marked this pull request as ready for review September 24, 2026 10:27

@helenb helenb left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great piece of work James, well done. A couple of comments here. I also noticed when testing locally that I got some 404 responses in my browser. I think these were for missing images, but the browser was reporting them as text/html perhaps because of being requested via htmx.

Image Image

@jhancock532

Copy link
Copy Markdown
Contributor Author

Thank you @helenb, I've made a small fix

I also noticed when testing locally that I got some 404 responses in my browser. I think these were for missing images, but the browser was reporting them as text/html perhaps because of being requested via htmx.

From looking into this, this is just from missing images and is the default Wagtail behaviour, with & without HTMX. In the screenshot above, it looks like we see the same 404 error when the page is first loaded as normal HTML, and then again with the HTMX reloads.

@helenb helenb left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes look good.

@jhancock532
jhancock532 merged commit 1a3d097 into migration-2026 Sep 29, 2026
8 checks passed
@jhancock532
jhancock532 deleted the feature/htmx-listing-filters branch September 29, 2026 08:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants