How to build a filterable list of things

Recently, one of our clients asked us to build a filter UI for a list of things. I thought it might be fun to write up how I approach a brief like that. This will not be a technical tutorial, but there is a functional demo that you can inspect with dev tools.

What am I doing here?#

The first question I ask is, "What problem is this supposed to solve?" Sometimes the client's idea of a solution is a partial fit, but a different approach might be even better for their users. In this case, the brief is to add a filter to an existing list of things. The client wants to give customers the option to narrow down the amount of things and make the list easier to scan, which is pretty straightforward.

Do I go for a server-side or client-side solution?#

Second question: "Do I build the filter in PHP or in JavaScript"? This may be an odd question for modern front-end developers, but many of our client projects are presentational sites which (in my opinion) benefit from our PHP/Symfony-based stack that does all the heavy lifting on the server and sends finished HTML with very little JavaScript to the browser.

There's a follow-up question hidden inside: if I build the filter with JavaScript, do I take over rendering the list or do I keep it in PHP/HTML?

Here's the kind of checklist that I go through before making a decision:

  • Are there a lot of these things? Is the list going to be paginated?

    If the answer is yes, PHP scores a point for both list rendering and the filter, because I'm not a fan of juggling pagination and filtering in JavaScript (although it can be done).

  • Is the list of things the main content of the page, or is it more of an aside that could be considered "bonus content"?

    If it's the main content I give a point to PHP for list rendering. Having everything in the HTML is a robust baseline.

  • What's the initial state? Show everything and allow users to filter it down, or show nothing and reveal matches upon user interaction?

    If the default is to see all things, I have a strong preference for having all things in the server-rendered HTML payload. This answer would be a point for list rendering in PHP.

  • Do filtered results need to be persisted when users navigate away from the list page and come back via "back" button? Should filtered results be shareable via URLs?

    If yes, I keep that in mind as a filter requirement, but neither PHP nor JavaScript get a point because both are a good enough fit.

  • Would the user experience be improved by an instant filter response instead of a whole server roundtrip?

    Arguably, the answer to this is always yes – unless the search is perceived as complex and a super fast response might erode user trust (that's a thing). This point goes to a client-side filter built with JavaScript 99,9% of the time.

Holding this list against my project, I see that there are 12-20 things at most, so pagination is not necessary. The list is the main content and the default state is to show all things. The client does not care about shareable URLs.

After looking at these answers, I decide to implement the filter function as a client-side enhancement for a server-rendered list. The main purpose is to let people access the things, and at a maximum of 20 things the filter is nice-to-have, not a necessity.

There are questions left to ask at this point, but for our kind of projects they're rhetorical:

  • What about older browsers like IE11?

    We support them as much as possible. I'm confident I can build this filter in a way that it works in old IE, but I'd be okay with hiding this nice-to-have enhancement from old browsers if supporting them were to prove unreasonable.

  • How accessible are we going to make this?

    Again, as much as possible. I try to deliver accessible solutions to the best of my ability.

How am I going to build this, then?#

It's time to make a plan. Since the list is already in the HTML, I don't need a big JavaScript framework to render it. I could write the filter myself, but I think that Alpine.js is an excellent fit for my task: tiny footprint, nice reactivity and easy to use as a drop-in tool. And there is a way to support older browsers without penalizing modern ones via the module/nomodule pattern.

Alpine.js has great documentation and works a lot like Vue.js, which has even more documentation, so I won't go into implementation details here. You can inspect the demo if you're curious.

Back to the plan. The filter will be a <form> with <label> and <select> elements. Using native HTML elements will not make the filter functionality accessible by itself, but take us a good part of way there. With Alpine.js, it makes sense to build the form in the template and deliver it with the list HTML. I'm optimistic that most users will be able to use JavaScript, but I will hide the form with CSS if the browser does not remove the class no-js from the <html> element with JavaScript (a trick from the olden days). On the other hand, it's nice to have the form in the HTML at first render, which sidesteps any issues with layout shift that might occur if we created the form with JavaScript.

A slight disadvantage of the client-side filter is that we have to notify the user about list updates ourselves. With a conventional form submit you get a full page load, which is a heavy-handed kind of notification to be sure, but it's also hard to miss. The Alpine.js-powered filter will update the list instantly, but without extra work, there is no guarantee users of assistive technology (AT) will have a similar experience as sighted users who notice the list changing in their field of view. This is what ARIA live regions are for. There was a timely livestream about them last week: The Many Lives of a Notification by Sarah Higley. The most important takeaway: keep notifications concise. It would be a bad idea to make the list a live region, because then every filter update would prompt AT to read out all remaining things. I am going to add a visually hidden live region instead that will announce the number of matches after each update.

Functional demo: a filterable list of talks for a fictitious conference#

:pseudo-conference 2020 Schedule #

Filter talks
  • #Jamstack

    How to use 11ty for absolutely everything#

    Join us on Monday in Zoom.

  • #a11y

    That <div> should probably be a button#

    Join us on Tuesday in Google Meet.

  • #CSS

    Learn ASS - The Agile CSS methodology without specificity waterfalls#

    Join us on Tuesday in Google Meet.

  • #Privacy

    Join us on Monday in Zoom.

  • #CSS

    The power of rarely used CSS selectors#

    Join us on Monday in Zoom.

  • #a11y

    Why "The Last of Us Part II" is such a big deal for Accessibility#

    Join us on Tuesday in Google Meet.

Demo for a filterable list built with AlpineJS

☝️ Here's the plan put into practice. Is it done? I think it's good enough for a first iteration. I went with CSS Grid for now, meaning IE11 and others will get stacked form fields and list items instead of horizontal rows, but I think that's okay. And it could be remedied with flexbox if need be. Another possible tweak: hide the filters on small screens and show a toggle button. Right now, the stack of three <select> elements takes up a lot of screen estate on a mobile phone and as I established earlier, the filter is a nice-to-have add-on and not a baseline feature.

Now, please play around with the demo and tell me if I missed anything. I'd be happy to learn how to make it (even) more accessible.


  • Jul 28, 2020 Fix filter layout on small screens
  • Jul 27, 2020 Fix typo
  • Jul 27, 2020 Fix escaping of inline code snippet
  • Jul 27, 2020 Add ideas for the next iteration of the filter UI
Post a comment If you post a tweet with a link to this page, it will appear here as a webmention (updated daily).


  1. liked by Jens Geiling
  2. liked by Iago Barreiro
  3. liked by Quinn Dombrowski
  4. liked by Matthias Ott
  5. liked by Ricardo Blanch PM
  6. liked by Tristan
  7. liked by Manuel Matuzović
  8. liked by Elaventi
  9. liked by Trevor Adams
  10. liked by Matthias 🦄