UXcellence Exploring excellent user experience

How to Build a Handy Portfolio Filter

I recently refreshed the design of my personal site — partially because it felt a bit dated and partially so that I could explore a few new (to me) tricks. I wanted to replace older techniques like font icons, jQuery, and float-based grids with SVG icons, plain JavaScript, and CSS Grid. I also wanted to play around with modular type scales and a baseline grid, but I’ll save that for a future post.

Today I wanted to touch on how I built a small widget for my portfolio to filter projects based on my role. Because I often work as a hybrid, I tend to play different roles on different projects. I wanted to quickly show at a glance which projects involved which skills.

Getting the data first

A screenshot of Craft CMS showing a Skills section under a portfolio entry with checkboxes next to the skills used.
Unsurprisingly, UXcellence uses nearly all of my skills.

In order to build the filter, I needed data. So in my CMS (Craft), I added a field for each portfolio entry to specify which skills that project employed. On the project page itself, this translates to a literal list of skills used near the top of the project.

A screenshot of a portfolio entry where the list of skills used on that project appears as a list to the right of a brief description of the project.
Every project is different. It’s helpful to show what skills came to bear for each one.

On my portfolio home page, I added a data attribute (data-roles) to each item in the list of projects. Each individual skill is separated from the others by a space in the attribute. (I’m using a shorthand name for the skill here that’s just a few characters.) This is the data I need to build my filter.

<li data-roles="ux fed rwd gfx dev write edit ia ">
  <a href="URL">
      <img src="IMG_THUMB" alt="ALT_TEXT">
      <h5>PROJECT TITLE</h5>

The filter itself is a list of links at the top, each with a single attribute (data-role) that corresponds to one of the data-roles in the portfolio items below it.

<div class="folio-filter">
  <h6><a href="#">Filter by role: <span id="filter-type" class="muted">All</span></a></h6>
  <ul class="filter-list">
    <li><a href="#" data-role="all" class="selected">All</a></li>
    <li><a href="#" data-role="ux">UX</a></li>
    <li><a href="#" data-role="fed">Front End</a></li>
    <li><a href="#" data-role="rwd">Responsive</a></li>
    <li><a href="#" data-role="gfx">Visual Design</a></li>
    <li><a href="#" data-role="research">User Research</a></li>
    <li><a href="#" data-role="dev">Full Stack</a></li>
    <li><a href="#" data-role="ia">IA</a></li>
    <li><a href="#" data-role="write">Writing</a></li>

Making the filter work

Now that I had the data in place to build my filter, I needed to make it work using JavaScript. The basic gist of the script is that when someone clicks a link in the list of filters, it will grab the data-role attribute for that link and look for any matching items in my portfolio with that role in their data-roles attribute.

var folioFilter = document.querySelector('.folio-filter');
var filters = document.querySelectorAll('.filter-list li a');
var folio = document.querySelectorAll('.folio li');

if(folioFilter) {
  filters.forEach(function(elem) {
    elem.addEventListener('click', function(e) {
      var filterRole = this.getAttribute('data-role');
      var i = 125;
      if(filterRole == "all") {
        var selectedFolio = folio;
      } else {
        var selectedFolio = document.querySelectorAll("[data-roles*='"+ filterRole +"']");
      filters.forEach(function(elem) {
      folio.forEach(function(elem) {
      selectedFolio.forEach(function(elem) {
        setTimeout(function() {
        }, i);
        i = i + 125;

The script adds a class to hide each item in the portfolio list, then removes that class only on the matching elements. Why not add a class to only the matching elements and hide the rest? First, because then they’d all be hidden by default. But mostly, I wanted to have the selected elements animate in one after the other, and I found this was the best method to achieve that.

It also removes the selected class from the list of filters, and adds that class back to the one that was clicked.

Finally, I use a setTimeout function within the function that removes the hidden class from all of the matching filtered items. This makes each item pop in one after the other instead of simultaneously. (A completely unnecessary but fun visual bonus.)

CSS Grid throws a hiccup

Because I wanted the elements to animate into place one after the other, I couldn’t use display: none to hide the non-selected elements. (Display is a binary style. It is either on or off so there is no transition between the two states, thus no way to animate it.) Instead I decided to rely on changing the opacity to 0 and the size (via transform: scale) to practically nothing.

The downside of this is that the elements took up space in the Grid, even though they were effectively invisible and tiny. This wouldn’t happen with the old float method because the grid isn’t explicitly defined. The end result was that depending on which filter I selected, different holes would appear within my portfolio grid. This was less than ideal.

A brief video that shows how each portfolio item stays in its place on the grid when I select different filters, resulting in odd whitespace.

Fortunately, I was able to fix this with a single CSS definition within my hidden class: order: 1. This effectively pushes my hidden portfolio items to the end of the grid because the default order when it’s unspecified is 0 (which comes before 1). Here are the final styles for the .hidden class and the list item without the class

.folio li {
  opacity: 1;
  transform: scale(1);
  transition: opacity 0.25s ease-in-out, transform 0.25s ease-in-out;
.folio li.hidden {
  opacity: 0;
  order: 1;
  overflow: hidden;
  transform: scale(0);

And here’s what it looks like as a result.

Another brief video showing how the portfolio items now pop into place from top to bottom, left to right when each filter is selected.

Making it more accessible

If I left it at that, it works fine for sighted users. But if you try to navigate through using a screen reader and keyboard, you’ll notice that even the “hidden” links get read no matter what filters get applied. That’s because they’re effectively just transparent and scaled down.

To instruct screen readers to ignore the hidden links, I added another line in my JavaScript to place an aria-hidden attribute in the list item and set it to true. (I set the attribute to false when I remove the hidden class.)

One issue I encountered in testing was that Voiceover on the Mac was still reading links within list items even with that attribute set. I’m not sure if this was because they were added by JavaScript or if it has buggy support. (I tried adding aria-hidden to some of the other elements within as well, and Voiceover still seemed to find something to read every time.) I was able to solve the issue, though, by adding a little bit of CSS.

.folio li[aria-hidden="true"] a {
  visibility: hidden;

This matches all the links within the hidden portfolio list items and sets their visibility to hidden. I chose this over display: none so they will continue to occupy space, which keeps the page below my portfolio from bouncing up and down as each row animates out and back in.

Making the filter responsive

Finally while I could have left a list of links for my filter on mobile devices, it would have pushed the more important portfolio content further down the page. So instead, I decided to condense the list on mobile devices to a single item that can be expanded as a dropdown.

A screenshot of the portfolio filters on mobile, which appears to be a dropdown and the text 'Filter by Role: Full Stack'.

To do this, I added a few things:

  1. An extra span within the filter heading to tell which one is selected when the dropdown is closed.
  2. A bit to my JavaScript to update the above span and “open” or “close” the dropdown. This is handled by adding an opened class to the overarching filter container.
  3. A pseudo-element in the CSS as an arrow that appears on the right side of the dropdown.

Bringing it all together

I’ve created a stripped down CodePen sample so you can play around with the concept and tweak it for your own uses. The fully stylized version is available on my portfolio page if you want to explore the other visual aspects of it.

Play on CodePen View my portfolio

In conclusion

With only nine items in my portfolio (I’ve retired older projects over time), this filter may seem like overkill. But I like that it showcases the breadth of my skills visually and helps visitors see which skills correlate to which projects at a glance.

Eventually I’m hoping to revisit this and tweak it a little. I’d love to append a hash for each filter to the URL when it is selected. Then I could detect when the page loads if that hash is added and automatically filter the list. (That way I could share a list of specific projects by filter.)

Like this? Please share:

Explore more like this