As my second year students complete their portfolios for their upcoming graduate showcase, a few have started to use framework plugins like Isotype to present their work. While I like the idea, and appreciate the work, I’m a little disturbed by the whirling dervish effect created by the plugins and the fact that they require relatively large downloads to operate.
I’ve shown a similar sorting solution for images using only CSS in the past, but that code has a scalability problem: not only does it require at least two lines of new CSS for every sort option added, but the gallery is limited to 6 ~ 9 images. I wanted to create a new version that reduced or eliminated those issues.
The Markup
The HTML code I came up with is fairly simple: I use a <button>
element for each sort option, and classes on each image to indicate group affiliation. I’ve limited the code and example to just four images in two groups for the sake of clarity.
<div id="locations">
<span>Show only</span>
<input type="checkbox" id="rome">
<label for="rome">Rome</label>
<input type="checkbox" id="kyoto">
<label for="kyoto">Kyoto</label>
<img src="katsura-river-arashiyama-kyoto.jpg" alt class="kyoto">
<img src="vatican-museum-spiral-stairs.jpg" alt class="rome">
<img src="vatican-skylight-rome.jpg" alt class="rome">
<img src="kiyomizu-dera-kyoto.jpg" alt class="kyoto">
</div>
Note that the id
value of the buttons matches the class
values for the images. Radio buttons would also be a UI option.
The Base CSS
div#locations {
font-size: 0;
background: #223;
text-align: center;
overflow: hidden;
max-width: 800px;
margin: 0 auto;
}
div#locations label {
display: inline-block;
margin: 1rem;
color: #fff;
font-size: 1rem;
border: 1px solid #000;
padding: .8rem;
border-radius: 3px;
cursor: pointer;
transition: .8s;
background: #111;
}
div#locations span {
color: #fff;
}
div#locations img {
width: 50%;
height: auto;
position: relative;
}
I’ve set the images to 50%
of the width of their container by default, but I want the first, fourth and eighth images to be full-width. Rather than trying to do so through specific classes, I’ll use an nth-of-type selector
:
div#locations img:nth-of-type(3n+1) {
width: 100%;
height: auto;
}
For the UI I’ll hide the checkboxes, leaving the <label>
elements to operate them, as previously shown in another article. When the user makes a choice the other inputs will be disabled, so I’ll make styles to cover that state also:
input[type=checkbox] {
display: none;
}
input:checked + label {
background: #333;
}
input:disabled + label {
background: rgba(122,122,133,0.5);
color: #888 !important;
box-shadow: inset 0 0 3px rgba(0,0,0,0.6);
cursor: default !important;
}
Finally, I want images that are outside the sort group to disappear in an animated sequence, which presents its own challenges.
Animating Height With CSS
CSS animates between states of known, explicit height – from 50px to 25px, for example – but cannot currently animate from or to an auto height. As this design is responsive, the actual height of the images will change constantly. As a result, the only way to know the height of an image would be to read its offsetHeight
property with JavaScript. To avoid that, I’ll animate the max-height
property instead. Code is shown sans vendor prefixes for simplicity:
@keyframes fadeandsmallify {
0% {
max-height: 400px;
opacity: 1;
}
66% {
max-height: 400px;
opacity: 0;
}
100% {
max-height: 0px;
opacity: 0;
}
}
The trick is to set max-height
higher than the image can ever achieve. That way, the animation can move towards a set “goal”, and effectively animate the height of the element. This animation sequence fades the element out, then reduces its height
to 0
.
The JavaScript
First, I’ll set up the variables I’m going to use:
var buttons = document.querySelectorAll("#locations input[type=checkbox]"),
animationstring = 'animation',
animation = false,
keyframeprefix = '',
domPrefixes = 'webkit moz o khtml'.split(' '),
pfx= '',
locations = document.getElementById("locations");
Next, do a quick test (similar to the one I do in the CSSslidy script) to determine of the browser requires a vendor prefix for CSS animations:
if (locations.style.animationName !== undefined) {
animation = true;
}
if (animation === false) {
for (var i = 0; i < domPrefixes.length; i++ ) {
if (locations.style[ domPrefixes[i] + 'AnimationName' ] !== undefined ) {
pfx = domPrefixes[ i ];
animationstring = pfx + 'Animation';
animation = true;
break;
}
}
}
Next, add a listener event to the buttons:
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click',filter,false);
}
Then, on a button click, select the images that do not fall inside its associated group, and animate the result, including a vendor prefix if necessary. Note that this portion of the script contains one problematic assumption that I’ll correct below:
function filter() {
city = this.id;
notCity = document.querySelectorAll("img:not(."+city+")");
for (var i = 0; i < notCity.length; i++) {
if (this.checked) {
notCity[i].style[animationstring] = "fadeandsmallify 1.6s forwards";
} else {
notCity[i].style[animationstring] = "fadeandsmallify 1.6s reverse";
}
}
Finally, turn off the other button(s):
notActive = document.querySelectorAll("#locations input[type=checkbox]:not(:checked)");
for (var i = 0; i < notActive.length; i++) {
if (this.checked) {
notActive[i].disabled = "disabled";
} else {
notActive[i].removeAttribute("disabled"); }
}
};
The Problem of Resetting CSS Animations
CSS animations have a bit of an issue: they can be difficult to re-engage after they’ve been run through once. You’d think that calling the same keyframe sequence in reverse would work to make the out-of-set images reappear, but it doesn’t.
Chris Coiyer has explored this issue in-depth on CSS tricks, and it turns out there are many solutions. I’ve used the simplest: creating a reverse of the initial animation under another name:.
@keyframes solidandlarger {
0% {
max-height: 0px;
opacity: 0;
}
33% {
max-height: 400px;
opacity: 0;
}
100% {
max-height: 400px;
opacity: 1;
}
}
I then call that animation in the script when I want the hidden images to reappear:
if (this.checked) {
notCity[i].style[animationstring] = "fadeandsmallify 1.6s forwards";
} else {
notCity[i].style[animationstring] = "solidandlarger 1.6s reverse";
}
As it is an animation with a new name, the sequence will re-engage and present the previously hidden content.
Conclusion
I think the result is a cleaner, more elegant, and lighter approach than Isotype. Adding new groups is easy: create a button with an id
value, and use that same value for classes in images that follow it. While I understand people’s desire to use a plugin solution, it’s nice to know there are alternatives you can easily code by hand.
Photographs by Nathan Gibbs and Trey Ratcliff, licensed under Creative Commons
Enjoy this piece? I invite you to follow me at twitter.com/dudleystorey to learn more.
Check out the CodePen demo for this article at https://codepen.io/dudleystorey/pen/KGABc