Building a modern, responsive photo gallery is a common challenge for web developers, but with the power of CSS Flexbox and a sprinkle of vanilla JavaScript, you can create a slick gallery that feels native on any device. In this tutorial, we’ll walk through how to build a gallery that not only looks great when you hover over images but also supports full keyboard navigation, ensuring accessibility and a smooth user experience for everyone. By the end, you’ll have a production‑ready gallery that’s lazy‑loaded, ARIA‑aware, and ready for dark mode.
Why Flexbox is the Right Choice for 2026 Galleries
Flexbox offers a declarative, flexible layout that automatically adapts to different screen sizes without the need for media queries for every breakpoint. Unlike grid, which is ideal for complex designs, Flexbox shines when you want a simple row or column layout that can wrap as needed. In 2026, browsers continue to support Flexbox natively, so you can rely on it for both desktop and mobile without polyfills.
Project Structure
Keeping your files organized makes future maintenance painless. Create a simple folder structure like this:
- index.html – The main page containing the gallery markup.
- styles.css – All the CSS, including Flexbox layout, hover styles, and dark mode support.
- gallery.js – JavaScript handling lazy loading and keyboard navigation.
- assets/ – A folder for your image files.
Building the Gallery Layout with Flexbox
HTML Markup
We’ll start with a semantic <section> that holds the gallery, and each image will be wrapped in a <figure> for accessibility. Here’s the core structure:
<section class="gallery" aria-label="Photo Gallery">
<figure tabindex="0" class="gallery-item">
<img src="assets/photo1.jpg" alt="A serene lake at sunset" loading="lazy" />
<figcaption>Lake Sunset</figcaption>
</figure>
</section>
Notice the tabindex="0" on each <figure> – this makes the item focusable so the keyboard can target it.
CSS Flexbox Layout
In styles.css, we set the gallery to display flex and wrap. The gap property gives us uniform spacing:
.gallery {
display: flex;
flex-wrap: wrap;
gap: 16px;
justify-content: center;
}
.gallery-item {
flex: 1 1 200px; /* Grow, shrink, start at 200px */
max-width: 300px;
overflow: hidden;
position: relative;
cursor: pointer;
}
With flex: 1 1 200px, each item will try to be at least 200px wide, but will grow to fill available space. The max-width caps the size so images don’t get too large on big screens.
Responsive Images with srcset
To serve appropriately sized images, use srcset and sizes attributes:
<img
src="assets/photo1-400.jpg"
srcset="assets/photo1-400.jpg 400w,
assets/photo1-800.jpg 800w,
assets/photo1-1200.jpg 1200w"
sizes="(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
33vw"
alt="A serene lake at sunset"
loading="lazy"
/>
This ensures the browser picks the best resolution for the device, improving load times and clarity.
Styling Images with Hover Effects
Hover states bring a touch of interactivity that encourages users to explore the gallery. We’ll use a subtle scale and box-shadow transition to create a floating effect.
.gallery-item:hover,
.gallery-item:focus {
transform: scale(1.02);
box-shadow: 0 12px 24px rgba(0,0,0,0.15);
z-index: 1;
}
.gallery-item img {
width: 100%;
height: auto;
display: block;
transition: transform 0.3s ease;
}
The z-index: 1 ensures the hovered item appears above its neighbors, preventing visual glitches when overlapping shadows.
Implementing Keyboard Navigation
Keyboard support is crucial for accessibility. We’ll add JavaScript to handle arrow key navigation, enabling users to move focus left, right, up, and down through the grid.
Focus Management
First, we need to detect when a <figure> receives focus and add a visual outline. In styles.css:
.gallery-item:focus-visible {
outline: 3px solid #ff9800;
outline-offset: 4px;
}
Using focus-visible keeps the outline only when navigating via keyboard.
Arrow Key Handling
In gallery.js, we’ll add an event listener to the gallery container. The logic calculates the next item index based on the current focus and the grid layout.
document.querySelector('.gallery').addEventListener('keydown', (e) => {
const items = Array.from(document.querySelectorAll('.gallery-item'));
const currentIndex = items.indexOf(document.activeElement);
if (currentIndex === -1) return;
let nextIndex = currentIndex;
switch (e.key) {
case 'ArrowRight':
nextIndex = (currentIndex + 1) % items.length;
break;
case 'ArrowLeft':
nextIndex = (currentIndex - 1 + items.length) % items.length;
break;
case 'ArrowDown':
nextIndex = (currentIndex + visibleColumns()) % items.length;
break;
case 'ArrowUp':
nextIndex = (currentIndex - visibleColumns() + items.length) % items.length;
break;
default:
return;
}
e.preventDefault();
items[nextIndex].focus();
});
function visibleColumns() {
// Calculate columns based on container width
const containerWidth = document.querySelector('.gallery').clientWidth;
const itemWidth = containerWidth / 3; // assuming 3 columns at base
return Math.floor(containerWidth / itemWidth);
}
This code ensures that pressing the arrow keys moves focus logically through the grid, wrapping around when reaching the edges.
Enhancing Accessibility with ARIA
Beyond tabindex and focus-visible, we can add ARIA roles to make screen readers understand the gallery’s structure. Wrap the gallery in role="list" and each <figure> in role="listitem". Add aria-labelledby to the gallery and reference the heading ID.
<section
class="gallery"
role="list"
aria-labelledby="gallery-heading"
>
<h2 id="gallery-heading">Photo Gallery</h2>
<figure
tabindex="0"
role="listitem"
class="gallery-item">
<img src="assets/photo1.jpg" alt="A serene lake at sunset" loading="lazy" />
<figcaption>Lake Sunset</figcaption>
</figure>
</section>
Screen readers will announce “Photo Gallery, item 1 of 12” and provide intuitive navigation.
Performance Tricks: Lazy Loading and CSS Variables
Lazy loading is already handled via the loading="lazy" attribute. For browsers that don’t support it, we can fall back to a small JS snippet that observes when an image enters the viewport and sets its src attribute.
const lazyImages = document.querySelectorAll('img[loading="lazy"]');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
lazyImages.forEach(img => observer.observe(img));
CSS variables allow us to switch themes effortlessly. In styles.css, define variables for colors and use them throughout:
:root {
--primary-bg: #ffffff;
--primary-text: #111111;
--accent: #ff9800;
}
[data-theme="dark"] {
--primary-bg: #121212;
--primary-text: #e0e0e0;
--accent: #ff6f00;
}
body {
background: var(--primary-bg);
color: var(--primary-text);
}
Then, add a small button that toggles data-theme on the html element. The gallery automatically updates without needing extra CSS.
Dark Mode Support
With the variables set above, the gallery already adapts to dark mode. You can trigger dark mode via CSS media query:
@media (prefers-color-scheme: dark) {
html[data-theme="auto"] {
--primary-bg: #121212;
--primary-text: #e0e0e0;
}
}
This respects user system settings, providing a consistent experience.
Testing and Debugging
- Keyboard Tests: Use the Tab key to cycle through images, then arrow keys to navigate. Verify that focus moves predictably and the outline is visible.
- Screen Reader: Run a screen reader (NVDA, VoiceOver) and confirm that the gallery announces each item correctly.
- Responsive Checks: Resize the window to see the gallery rearrange. Use Chrome DevTools to emulate different devices.
- Performance Audits: Run Lighthouse in Chrome to measure load time, first paint, and accessibility score. Aim for a score above 90.
- Cross‑Browser Compatibility: Test on Chrome, Firefox, Safari, and Edge. Flexbox support is consistent, but verify that
focus-visibleandloading="lazy"behave as expected.
Debugging issues often stems from missing tabindex or incorrectly calculated column count. Keep the JavaScript console open and watch for errors or warnings.
Conclusion
By combining Flexbox, semantic HTML, ARIA attributes, and lightweight JavaScript, you can build a photo gallery that feels natural on every device, responds to user interactions, and respects accessibility standards. The techniques presented here—responsive srcset, keyboard navigation, dark mode via CSS variables, and lazy loading—make the gallery production‑ready while keeping the codebase clean and maintainable. Enjoy showcasing your images with confidence, knowing that every visitor can explore the gallery effortlessly.
