In this tutorial, we’re going to take a bunch of photos and turn them into a filterable thumbnail layout. We’ll combine all the latest CSS goodies (CSS Grid, flexbox, and CSS variables) along with some custom JavaScript to build an amazing demo!
Here’s what we’ll be creating:
Be sure to check the demo on a large screen (>900px) because at that point the magic happens! Without further ado, grab a cup of coffee and let’s get cracking!
1. Begin With the Page Markup
We’ll start with a .container
which holds the .toolbar
element and a list of photos:
<div class="container"> <div class="toolbar">...</div> <ol class="image-list grid-view">...</ol> </div>
The toolbar layout will look like this:
Inside it we’ll place two elements:
- The search box which allows us to search for a specific photo
- a list with three options which determine the thumbnail layout. By default the photos appear in grid view, but we can switch to the list view by clicking on the icon in the right corner. Furthermore, each time we are in the grid view, we have the option to change the number of photos which appear per row. To do this, we’ll use a range slider.
Here’s the associated markup for all that:
<div class="search-wrapper"> <input type="search" placeholder="Search for photos"> <div class="counter"> Total photos: <span>12</span> </div> </div> <ul class="view-options"> <li class="zoom"> <input type="range" min="180" max="380" value="280"> </li> <li class="show-grid active"> <button disabled> <img src="IMG_SRC" alt="grid view"> </button> </li> <li class="show-list"> <button> <img src="IMG_SRC" alt="list view"> </button> </li> </ul>
Inside the image list we’ll place twelve Unsplash photos. Each photo comes with its description as well as the name of its owner. Here’s what it will look like with some basic styling (which we’ll come to shortly):
Here’s the markup for a single photo:
<li> <figure> <img src="IMG_SRC" alt=""> <figcaption> <p>...</p> <p>...</p> </figcaption> </figure> </li> <!-- 11 list items here -->
It’s important to note that the image list will always contain the image-list
class. In addition, it will also receive either the grid-view
or the list-view
class like this: <ol class="image-list grid-view">...</ol>
Its second class will depend on the layout view selected by the user. More on that in the upcoming sections.
2. Define Some Basic Styles
We first set up a few CSS variables and some reset styles:
:root { --black: #1a1a1a; --white: #fff; --gray: #ccc; --darkgreen: #18846C; --lightbrown: antiquewhite; --darkblack: rgba(0,0,0,0.8); --minRangeValue: 280px; } * { margin: 0; padding: 0; outline: none; border: none; } button { cursor: pointer; background: none; } img { display: block; max-width: 100%; height: auto; } ol, ul { list-style: none; } a { color: inherit; }
Most importantly, pay attention to the value of the minRangeValue
variable. Its value (280px) matches the default value of the range slider.
Remember the markup for our slider: <input type="range" min="180" max="380" value="280">
. Later on we’ll use that value for setting the minimum width of our photos.
Note: for simplicity I won’t walk through all the CSS rules in the tutorial. You can check the rest of them by clicking the CSS tab of the demo project.
Styling the Toolbar
As a next step, we’ll style the toolbar. Here are the key points regarding this element:
- We use flexbox to layout its contents.
- Each time a user selects a layout (grid or list), the corresponding button marks as active and receives a darkgreen border. In addition, we disable it.
- The range slider appears only when the grid view is active and for screens which have a minimum width of 901px.
The important parts of the corresponding styles are shown below:
/*CUSTOM VARIABLES HERE*/ .toolbar { display: flex; justify-content: space-between; align-items: center; } .view-options { display: flex; align-items: center; } .view-options li:not(:last-child) { margin-right: 1.2rem; } .view-options button { padding: 2px; border: 3px solid transparent; } .view-options .active button { border-color: var(--darkgreen); } @media screen and (max-width: 900px) { .toolbar input[type="range"] { display: none; } }
Styling the Image List
As already mentioned, the image list layout will depend on the layout selected by the user. In any case, we’ll take advantage of CSS Grid to build this layout.
Before doing so, let’s apply a few generic styles to the image list:
/*CUSTOM VARIABLES HERE*/ .image-list { margin: 3rem 0; } .image-list li { background: var(--lightbrown); background: var(--darkblack); } .image-list img { background: #e6e6e6; } .image-list p:first-child { font-weight: bold; font-size: 1.15rem; } .image-list p:last-child { margin-top: 0.5rem; }
In grid view, the lists items will be divided into repetitive columns/cells with gutters between them:
The number of items that will appear per row depends on the screen size. Initially, each item will have a minimum width of 280px and a maximum width that matches its container width. But as we’ll see later, we’ll add a little bit of interactivity and give users the option to modify the minimum width. For this reason we won’t hardcode its value but instead store it in the minRangeValue
variable.
We’re able to produce this truly responsive layout by combining the minmax()
CSS function with CSS Grid. Here’s how the aforementioned requirements are translated in terms of styles:
:root { --minRangeValue: 280px; } .grid-view { display: grid; grid-gap: 2rem; grid-template-columns: repeat(auto-fit, minmax(var(--minRangeValue), 1fr)); }
In list view, the list items will have the default block-level behavior:
Inside them, the image will have a fixed width of 150px and its description will cover the rest of the available space (1fr). Plus, both elements will be vertically centered with a gutter between them.
The related styles:
.list-view li + li { margin-top: 1.5rem; } .list-view figure { display: grid; grid-gap: 1.5rem; grid-template-columns: 150px 1fr; align-items: center; }
To understand more about how minmax()
works with CSS Grid Layout, here’s a beginner’s tutorial:
3. Toggle Between the List and Grid Views
Each time a user clicks on the desired pattern for content presentation, we do two things:
- Add a darkgreen border to the active button.
- Check which type of view is selected by the user.
If the user selects the grid view, we display the range slider and ensure that the image list contains the grid-view
class and not the list-view
class.
On the other hand, if the user selects the list view, we hide the range slider and ensure that the image list contains the list-view
class and not the grid-view
class.
The required JavaScript code:
const imageList = document.querySelector(".image-list"); const btns = document.querySelectorAll(".view-options button"); const imageListItems = document.querySelectorAll(".image-list li"); const active = "active"; const listView = "list-view"; const gridView = "grid-view"; const dNone = "d-none"; for (const btn of btns) { btn.addEventListener("click", function() { const parent = this.parentElement; document.querySelector(".view-options .active").classList.remove(active); parent.classList.add(active); this.disabled = true; document.querySelector('.view-options [class^="show-"]:not(.active) button').disabled = false; if (parent.classList.contains("show-list")) { parent.previousElementSibling.previousElementSibling.classList.add(dNone); imageList.classList.remove(gridView); imageList.classList.add(listView); } else { parent.previousElementSibling.classList.remove(dNone); imageList.classList.remove(listView); imageList.classList.add(gridView); } }); }
4. Update CSS Variables Through JavaScript
We have already discussed that the initial value of the range slider is 280px and matches the value of the minRangeValue
variable. Plus, its minimum value is 180px while its maximum is 380px.
We need to keep track of the slider value changes and update the minRangeValue
variable accordingly. This will make our grid view layout flexible as each row won’t contain a fixed number of columns.
The JavaScript code that will do the trick makes use of the input
event:
const rangeInput = document.querySelector('input[type = "range"]'); rangeInput.addEventListener("input", function() { document.documentElement.style.setProperty("--minRangeValue", `${this.value}px`); });
To understand this code, open your browser tools and update the slider value. You’ll notice that the html
element receives an inline style which overwrites the property value set through CSS:
5. Build the Search Functionality
Currently we only have twelve images, but image a scenario where we have dozens of images. In such a case, it would be really nice if users had the ability to search for specific photos.
So, let’s go ahead and build a custom search component like this:
Note that it will only work for image descriptions:
As a first step we do the following:
- Iterate through all photos.
- For each photo we find, we initialize an object literal with two properties.
- The first property is the
id
with an increment number which is unique for each object. The second property is thetext
which stores the target photo description. - Store all objects in an array.
The JavaScript code that implements this functionality:
const captions = document.querySelectorAll(".image-list figcaption p:first-child"); const myArray = []; let counter = 1; for (const caption of captions) { myArray.push({ id: counter++, text: caption.textContent }); }
One thing to note is that our counter starts from 1 and not from 0. We did this intentionally because this will help us easily target the desired elements later on.
User Input
Next, each time a user types something in the search input, we do the following:
- Hide all photos.
- Grab the search query.
- Check if there are array elements (objects) which include the search query in their
text
property value. - Show the elements that fulfill the requirement above.
- Print their number in the screen. If there aren’t any elements print 0.
The required JavaScript code:
const searchInput = document.querySelector('input[type="search"]'); const imageListItems = document.querySelectorAll(".image-list li"); const photosCounter = document.querySelector(".toolbar .counter span"); const dNone = "d-none"; searchInput.addEventListener("keyup", keyupHandler); function keyupHandler() { // 1 for (const item of imageListItems) { item.classList.add(dNone); } // 2 const text = this.value; // 3 const filteredArray = myArray.filter(el => el.text.includes(text)); // 4 if (filteredArray.length > 0) { for (const el of filteredArray) { document.querySelector(`.image-list li:nth-child(${el.id})`).classList.remove(dNone); } } // 5 photosCounter.textContent = filteredArray.length; }
And the CSS class used here:
.d-none { display: none; }
Note: There are different methods we can use to prevent the callback function from running every time a user releases a key (keyup
event). Although beyond the scope of this tutorial, an effective solution might be to use Lodash’s debounce function.
Conclusion
That’s it folks! It was indeed a long journey, yet I hope you learned some new things and you enjoyed the demo we built here. Play around with it and let me know if you have any questions.
As always, thanks for reading!
More CSS Layout Projects on Tuts+
-
CSS Grid LayoutSolving Problems With CSS Grid and Flexbox: The Card UIIan Yates
-
CSS Grid LayoutCreate a CSS Grid Image Gallery (With Blur Effect and Interaction Media Queries)Ian Yates
-
CSS SelectorsHow to Build a Filtering Component in Pure CSSGeorge Martsoukos
-
FlexboxHow to Build a Full-Screen Responsive Page With FlexboxGeorge Martsoukos
{excerpt}
Read More