civil-and-structural-engineering
Creating a Dynamic Photo Gallery with Filters and Sorting Using Javascript
Table of Contents
Introduction
Building a dynamic photo gallery with filtering and sorting capabilities transforms a static image collection into an engaging, user‑friendly experience. Visitors can quickly find images that match their interests—for example, filtering by nature, city, or people—and reorder results by name, date, or rating without a page reload. This article provides a comprehensive, production‑ready approach to creating such a gallery using plain JavaScript, HTML, and CSS. You’ll learn how to structure your markup, apply efficient filtering and sorting logic, enhance performance with lazy loading, and follow accessibility best practices. The final gallery will be responsive, maintainable, and easily extensible.
Setting Up the HTML Structure
A well‑structured HTML foundation makes filtering and sorting straightforward. Each image element should carry custom data-* attributes that describe its category, name, date, or any other property you intend to filter or sort by.
Base Markup for Filters and Sorting Controls
Create a container for filter buttons and another for sorting controls. Use <button> elements with data-filter and data-sort attributes:
<div id="controls">
<div id="filter-buttons" role="group" aria-label="Filter photos by category">
<button data-filter="all" class="active">Show All</button>
<button data-filter="nature">Nature</button>
<button data-filter="city">City</button>
<button data-filter="people">People</button>
</div>
<div id="sort-options" role="group" aria-label="Sort photos">
<button data-sort="name">Sort by Name</button>
<button data-sort="date">Sort by Date</button>
<button data-sort="rating">Sort by Rating</button>
</div>
</div>
Including role="group" and aria-label improves accessibility for screen reader users. The data-filter="all" button is marked with a class active to indicate the initial state; you’ll style this with CSS.
Image Gallery Container
Inside a <div id="gallery">, place each photo in its own wrapper element. Use data-category, data-name, data-date, and data-rating attributes for filtering and sorting. For example:
<div id="gallery" class="photo-grid">
<div class="photo" data-category="nature" data-name="Sunset Over the Hills" data-date="2023-06-15" data-rating="4.5">
<img src="images/sunset.jpg" alt="Sunset over rolling hills" loading="lazy">
<div class="photo-caption">Sunset Over the Hills</div>
</div>
<div class="photo" data-category="city" data-name="Downtown Skyline" data-date="2023-07-01" data-rating="4.2">
<img src="images/skyline.jpg" alt="Downtown skyline at dusk" loading="lazy">
<div class="photo-caption">Downtown Skyline</div>
</div>
<!-- more photos -->
</div>
The loading="lazy" attribute defers loading of off‑screen images, improving initial page load performance. Each photo wrapper can also include a caption or overlay for additional information.
Using Data Attributes Effectively
Data attributes (e.g., data-category, data-name) are not limited to strings. For numerical sorting, store numbers as strings; JavaScript will parse them as needed. For multiple categories, you can store a space‑separated list like data-category="nature landscape" and split it during filtering. This technique allows images to appear under multiple filters, a feature we’ll implement later. For a comprehensive reference on data attributes, see the MDN documentation on the dataset API.
Styling the Gallery with CSS
A clean, responsive layout sets the stage for the interactive features. Use CSS Grid or Flexbox for the grid of photos, and style the buttons to indicate active filters and sorting state.
Photo Grid Layout
.photo-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
padding: 1rem 0;
}
.photo {
position: relative;
overflow: hidden;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.photo img {
width: 100%;
height: auto;
display: block;
transition: transform 0.3s ease;
}
.photo:hover img {
transform: scale(1.05);
}
This grid ensures images adapt to screen size. The hover effect adds a subtle zoom interaction, making the gallery feel more polished.
Active State for Buttons
#filter-buttons button.active,
#sort-options button.active {
background-color: #007acc;
color: #fff;
border-color: #007acc;
}
button {
padding: 0.5rem 1rem;
border: 1px solid #ccc;
background: #fff;
cursor: pointer;
margin: 0.25rem;
border-radius: 4px;
transition: background 0.2s, color 0.2s;
}
button:hover {
background: #e6e6e6;
}
Applying the active class to the current filter/sort button provides visual feedback. JavaScript will manage adding and removing this class.
Responsive Considerations
Ensure the gallery works on touch devices. Increase button touch target sizes to at least 44px by 44px (WCAG 2.1 recommendation). Use media queries if necessary to switch from a multi‑column grid to a single column on very small screens.
Implementing JavaScript Filtering
Now we bring the gallery to life. The core idea: listen for clicks on filter buttons, then show or hide photo elements based on their data-category attribute.
Selecting DOM Elements and Initializing
const filterButtons = document.querySelectorAll('#filter-buttons button');
const sortButtons = document.querySelectorAll('#sort-options button');
const gallery = document.getElementById('gallery');
let photos = Array.from(gallery.children);
let activeFilter = 'all';
let activeSort = 'name'; // default sort
Storing photos as an array gives us easy access to array methods like filter() and sort(). We also keep track of the current filter and sort order so we can re‑apply sorting after a filter change.
Filtering Logic
function filterPhotos(filter) {
activeFilter = filter;
photos.forEach(photo => {
const categories = photo.dataset.category; // may be a space-separated string
const shouldShow = filter === 'all' || categories.split(' ').includes(filter);
photo.style.display = shouldShow ? '' : 'none';
});
// After filtering, re-sort the visible photos
sortPhotos(activeSort);
}
This function supports multiple categories: if data-category="nature landscape", then choosing either “nature” or “landscape” will show the photo. Note: photo.dataset.category returns the value of data-category (as a string). Using .split(' ').includes(filter) handles the multi‑category case.
Attaching Event Listeners to Filter Buttons
filterButtons.forEach(button => {
button.addEventListener('click', () => {
const filter = button.dataset.filter;
// Update active class on filter buttons
filterButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
// Apply filter
filterPhotos(filter);
});
});
Removing the active class from all buttons and adding it to the clicked one ensures visual consistency. The filterPhotos function is called with the selected filter value.
Implementing JavaScript Sorting
Sorting reorganizes the visible photos in the DOM based on a chosen attribute (e.g., name, date, rating).
Sorting Function
function sortPhotos(sortBy) {
activeSort = sortBy;
// Get currently visible photos (display not 'none')
const visiblePhotos = photos.filter(photo => photo.style.display !== 'none');
visiblePhotos.sort((a, b) => {
let valA, valB;
switch (sortBy) {
case 'name':
valA = a.dataset.name.toLowerCase();
valB = b.dataset.name.toLowerCase();
return valA.localeCompare(valB);
case 'date':
valA = new Date(a.dataset.date);
valB = new Date(b.dataset.date);
return valA - valB;
case 'rating':
valA = parseFloat(a.dataset.rating);
valB = parseFloat(b.dataset.rating);
return valB - valA; // descending: highest first
default:
return 0;
}
});
// Re‑append sorted photos back to the gallery container
visiblePhotos.forEach(photo => gallery.appendChild(photo));
}
Sorting works only on visible photos, which means after a filter change you restore the order according to the active sort. For strings, we use localeCompare for proper alphabetical ordering. Dates are compared as Date objects, and ratings are sorted in descending order (most popular first).
Event Listeners for Sort Buttons
sortButtons.forEach(button => {
button.addEventListener('click', () => {
const sortBy = button.dataset.sort;
sortButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
sortPhotos(sortBy);
});
});
When a sort button is clicked, we update the active class and call sortPhotos with the desired sort key. Because filterPhotos already calls sortPhotos after filtering, the gallery remains consistent.
Initial Setup
// Set default active buttons
document.querySelector('#filter-buttons [data-filter="all"]').classList.add('active');
document.querySelector('#sort-options [data-sort="name"]').classList.add('active');
// Load and initially sort by name
filterPhotos('all');
Calling filterPhotos('all') at start ensures the gallery is populated with all images and sorted by the default sort (name).
Advanced Filtering and Search
A simple category filter is useful, but you may want to combine multiple filters or add a text search input. The following enhancements build on the core logic.
Multiple Category Filters (Checkboxes)
Instead of exclusive buttons, use checkboxes so users can select multiple categories:
<div id="filter-checkboxes">
<label><input type="checkbox" value="nature" checked> Nature</label>
<label><input type="checkbox" value="city" checked> City</label>
<label><input type="checkbox" value="people" checked> People</label>
</div>
Then modify the filtering logic to build a set of active categories:
function filterPhotosByCheckboxes() {
const checked = document.querySelectorAll('#filter-checkboxes input:checked');
const activeCategories = Array.from(checked).map(cb => cb.value);
photos.forEach(photo => {
const photoCategories = photo.dataset.category.split(' ');
const shouldShow = activeCategories.length === 0 ||
photoCategories.some(cat => activeCategories.includes(cat));
photo.style.display = shouldShow ? '' : 'none';
});
sortPhotos(activeSort);
}
If no checkboxes are checked, you might choose to show all images (as in the code above) or show none—define the desired UX.
Text Search Input
Add an input field that searches captions or the data-name attribute:
<input type="text" id="search" placeholder="Search photos...">
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', () => {
const query = searchInput.value.toLowerCase();
photos.forEach(photo => {
const name = photo.dataset.name.toLowerCase();
// Also search in caption if present
const caption = photo.querySelector('.photo-caption')?.innerText.toLowerCase() || '';
const matches = name.includes(query) || caption.includes(query);
// If a filter is active, also check category
const filter = activeFilter;
const catMatch = filter === 'all' || photo.dataset.category.split(' ').includes(filter);
photo.style.display = (matches && catMatch) ? '' : 'none';
});
sortPhotos(activeSort);
});
Combine search with filter buttons to allow narrowing results further. Note that you need to store the current filter state to respect it during search.
Performance and Accessibility
A dynamic gallery should be fast and usable by everyone, including people using assistive technologies.
Lazy Loading Images
We already used <img loading="lazy"> which defers loading until the image is near the viewport. For older browsers, you can implement a JavaScript lazy‑loading fallback using the Intersection Observer API. This ensures that even if native lazy loading is not supported, off‑screen images are not loaded, reducing bandwidth and improving perceived performance.
Debouncing Search Input
If you attach the search listener to input, it fires on every keystroke. For large galleries, consider debouncing the handler (e.g., 200ms delay) to avoid excessive DOM manipulations. A simple debounce utility:
function debounce(func, delay) { let timeoutId; return function (...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(this, args), delay); }; } searchInput.addEventListener('input', debounce(handleSearch, 200));Keyboard Navigation
Filter and sort buttons should be focusable and operable via keyboard. Using native
<button>elements already provides keyboard interaction (Enter/Space). Ensure that after filtering or sorting, focus is managed logically; for example, you could move focus to the first photo after a filter change via a focusable element. Also, consider addingaria-pressedattributes to filter buttons to indicate toggle state.Announcing Changes to Screen Readers
Use an ARIA live region to announce filter and sort results:
<div id="gallery-announce" aria-live="polite" class="sr-only"></div>After each filter/sort action, update the text of that element, e.g., “Showing 8 nature photos sorted by name”. This helps users who rely on screen readers understand what changed.
CSS for Hidden Content
Instead of using
display: nonealone, consider using a CSS class like.hiddenthat hides the element and removes it from the accessibility tree:.hidden { display: none !important; visibility: hidden; }Using
display: nonedoes remove elements from the accessibility tree, which is appropriate. Just ensure you don’t rely solely onvisibility: hiddenwithoutdisplay: nonebecause that leaves the element occupying space.Conclusion
By combining clean semantic HTML, responsive CSS, and well‑structured JavaScript, you can build a dynamic photo gallery that is fast, accessible, and pleasant to use. The techniques covered—filtering by category (including multiple categories), sorting by various attributes, adding text search, and optimizing performance—provide a solid foundation that can be extended with features like pagination, infinite scroll, or integration with a backend API.
For further reading, the MDN documentation on Array.prototype.sort() explains the sorting algorithm, and the web.dev responsive design guide offers additional layout tips. Experiment with different data attributes, animations, and custom styling to tailor the gallery to your project’s needs. The result is a powerful, interactive component that puts users in control of their browsing experience.