Using Macros
Pellentesque habitant morbi tristique senectus et netus et malesuada
fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies
eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper.
Aenean ultricies mi vitae est. Mauris placerat eleifend leo.
```
The `article` elements have a number of noteworthy things on them. Firstly, they
all have a class of `c-article` that is completely separate to the filtering:
this class just carries styles specific to the articles and is not actually
required for the filtering to work.
Secondly, they all have a class of `c-filter__item`: this simply lays the items
out in the filtering context and, again, isn’t actually required for the
filtering to take place.
Finally, we see a `data-tags` attribute which is filled with one or more tags
pertaining to the content. These data attributes are necessary for the filtering
to work, because we use the values of them to decide what we need to show the
user.
The process will go a little like this:
1. The page loads showing all items.
2. The user clicks a filtering link which jumps to the correspoinding `#tags:*`
anchor, appending a fragment identifier onto the URL.
3. We’ll grab the value of the fragment identifier by using the `:target` pseudo
selector.
4. We’ll use this knowledge to show/hide the relevant content.
## The CSS
The majority of the work happens in the CSS. The first chunk isn’t really
aything exciting: it’s just cosmetic styling and laying out our items.
```scss
/**
* 1. Clearfix our filter context.
*/
.c-filter {
overflow: hidden; /* [1] */
}
.c-filter__links {
padding: 1em;
margin-bottom: 1em;
background-color: #fff;
border-bottom: 1px solid #e5e5e5;
}
.c-filter__link {
display: inline-block;
padding: 0.5em 1em;
border-radius: 100px;
background-color: rgba(0, 0, 0, 0);
transition: 0.333s;
}
.c-filter__link--reset {
border-right: 1px solid;
border-radius: 0;
}
.c-filter__item {
float: left;
width: calc(33.333333333% - 1em);
margin-right: 0.5em;
margin-bottom: 1em;
margin-left: 0.5em;
transition: 0.333s;
}
```
Now things start to get a little more interesting:
```scss
.c-filter__target:not(:target) ~ .c-filter__item {
opacity: 1;
}
```
This selector is doing a few things. Let’s break it down:
* `.c-filter__target` refers to all of our empty `a` elements at once.
* `:not(:target)` checks that none of the anchors are currently targeted.
* `~` is the adjacent sibling selector, and that looks for things later in the
DOM but that are on the same level of the DOM tree.
* `.c-filter__item` is simply looking for all of our filterable items.
So basically, if we aren’t currently targeting any anchors, look ahead in the
DOM to find filter subjects and make sure they’re all turned on.
‘Turned on’ in this case is `opacity: 1;`. This could be anything else (e.g. it
actually started off as `display: block;` in my first version of this).
The next bit of CSS looks quite similar:
```scss
.c-filter__target:target ~ .c-filter__item {
opacity: 0.25;
}
```
We’re doing almost the exact same lookup as before, only this time we’re missing
the `:not()` selector. What this means that we’re checking if any anchors _are_
targeted, and if so, turn all of our filterable items off (`opacity: 0.25;`).
So two things here: if no anchors are targetted, turn everything on; if any
anchors are targetted, turn everything off.
After we’ve turned everything off, we want to turn the specific items back
_on_. We have to get a little more involved here, because we’re looking out for
specific DOM nodes: we can’t use a blanket rule.
I’m using Sass to help me out here, and that looks like this:
```scss
@each $tag in html, css, accessibility, performance, vim {
[id="tag:#{$tag}"]:target ~ [data-tag~="#{$tag}"] {
opacity: 1;
}
...
}
```
Here we have a loop that contains every one of my tags, and goes through them
all making selectors that look like this:
```scss
[id="tag:css"]:target ~ [data-tag~="css"] { ... }
```
What’s happening here?
* `[id="tag:css"]:target` sees if the anchor with an ID exactly equal to
`tag:css` is currently targeted. **N.B.** I could have written this selector
as `#tag\:css:target` but there are two reasons why I didn’t: firstly, as you
can see, we have to escape that colon, and secondly, we don’t use IDs in CSS.
Writing this as an attribute selectors means I don’t need to put any other
CSS-specific hooks in my HTML, and it has the added advantage of being able to
select an ID using a class worth of specificity.
* `~`, as before, looks ahead in the DOM and finds certain elements on the same
level in the DOM tree.
* `[data-tag~="css"]` is the element later on, on the same level of the DOM
tree, that the `~` is looking for. What I’m doing here is using a substring
selector to interrogate the contents of the `data-tags` attribute. The `~=`
specifically means check that the attribute contains a list of space-separated
values, and that one of the values is exactly equal to (in this case) `css`.
So if we’re targeting the `css` anchor, please go ahead and find all elements
tagged with `css` later in the DOM tree. Once you’ve found them, please turn
them on:
```scss
opacity: 1;
```
This is all that is really necessary to make the filtering work—it’s actually a
very small amount of code.
### Highlighting the Current Filter Link
What I thought would be a nice touch would be to somehow style the currently
filtered link, so that the user has a visual cue as to what content they’re
looking at. This proved a little trickier than I was expecting, but was
certainly not insurmountable.
Inside of the same Sass loop as before, I’m generating a new suite of selectors
that each look like this:
```scss
[id="tag:css"]:target ~ .c-filter__links [href="#tag:css"] { ... }
```
Stepping through this one bit at a time:
* `[id="tag:css"]:target` checks to see if the `css` anchor is currently
targeted.
* `~` looks ahead in the DOM.
* `.c-filter__links` is the thing we’re looking for.
* `[href="#tag:css"]` looks for an element whose `href` attribute is exactly
equal to `#tag:css`.
One thing I found interesting here was actually getting at the links. Because
they live inside of a `nav` element (`.c-filter__links`)—therefore are not on
the same level of the DOM tree, as required by the `~` selector—I couldn’t
target them directly with the `~` selector. I almost abandoned this whole
highlighting exercise because of that very fact, but then I realised a
workaround.
Instead of trying to target the links themselves, I can target their `nav`
element wrapper because that _is_ on the same level of the DOM tree. The next
step was just a case of looking for the correct link inside of that. This is why
I had to nest the last bit of the selector.
### Awkward Page Jumps
There was another problem I was having that I managed to solve pretty
painlessly. Because we’re linking to fragment identifiers, the page would
awkwardly jump around if the content was long enough to cause the page to
scroll. The solution was a one-line fix.
Because we’re linking to empty anchor elements, they’re not actually visible at
all in the page (but they do still exist at a point in the DOM, which is why we
have the page jumps). By simply applying `position: fixed;` to all of the
anchors, they will always be positioned at the very top-left of the viewport.
The practical upshot of this is that the anchors will always be exactly where
the user has scrolled to, so the page didn’t actually have to jump anywhere.
```scss
.c-filter__target {
position: fixed;
}
```
Problem solved.
## Drawbacks
There are a few drawbacks to this technique. In no particular order:
* Adding new tags means writing and recompiling new CSS. We could get around
this by having our CMS drop a `style` block into the page and looping over our
tags there, so it’s not completely the end of the world.
* Filtering adds new history states. It could certainly get very annoying
clicking your back button a dozen times through filters. This does, however,
mean that we can link people directly to filtered results.
* Accessibility. Honestly, I’m not sure how good or bad this is for
accessibility. I have no doubt that there will be some improvements we could
make. At least it isn’t using the checkbox hack, eh?
* Should I have done this at all? It feels like it’s blurring the lines between
what CSS should be doing, and what we should defer to JavaScript.
* Ugly CSS. Nested selectors, substring selectors, heavily location dependent.
It’s not the best, is it?
Still, it’s been quite a long time since I played around with CSS like this. and
it was a lot of fun. I need to make sure I do things like this more often.